@gajae-code/coding-agent 0.6.3 → 0.6.5

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 (140) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +73 -1
  3. package/dist/types/cli/migrate-cli.d.ts +20 -0
  4. package/dist/types/commands/migrate.d.ts +33 -0
  5. package/dist/types/config/keybindings.d.ts +4 -0
  6. package/dist/types/config/settings-schema.d.ts +27 -0
  7. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  8. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  9. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  10. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  11. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  12. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  14. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  19. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  20. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  21. package/dist/types/hooks/skill-state.d.ts +12 -4
  22. package/dist/types/migrate/action-planner.d.ts +11 -0
  23. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  24. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  25. package/dist/types/migrate/adapters/index.d.ts +45 -0
  26. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  27. package/dist/types/migrate/executor.d.ts +2 -0
  28. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  29. package/dist/types/migrate/report.d.ts +18 -0
  30. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  31. package/dist/types/migrate/types.d.ts +126 -0
  32. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  33. package/dist/types/modes/components/welcome.d.ts +3 -1
  34. package/dist/types/modes/interactive-mode.d.ts +3 -0
  35. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  37. package/dist/types/research-plan/index.d.ts +1 -0
  38. package/dist/types/research-plan/ledger.d.ts +33 -0
  39. package/dist/types/rlm/artifacts.d.ts +1 -1
  40. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  41. package/dist/types/skill-state/active-state.d.ts +6 -11
  42. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  43. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  44. package/dist/types/task/spawn-gate.d.ts +1 -10
  45. package/package.json +7 -7
  46. package/src/cli/migrate-cli.ts +106 -0
  47. package/src/cli/setup-cli.ts +14 -1
  48. package/src/cli.ts +1 -0
  49. package/src/commands/deep-interview.ts +2 -2
  50. package/src/commands/launch.ts +1 -1
  51. package/src/commands/migrate.ts +46 -0
  52. package/src/commands/state.ts +2 -1
  53. package/src/commands/team.ts +7 -3
  54. package/src/config/model-registry.ts +9 -2
  55. package/src/config/model-resolver.ts +13 -2
  56. package/src/config/settings-schema.ts +17 -0
  57. package/src/coordinator-mcp/policy.ts +10 -2
  58. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  61. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
  63. package/src/exec/bash-executor.ts +3 -1
  64. package/src/extensibility/custom-commands/loader.ts +0 -7
  65. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  66. package/src/extensibility/gjc-plugins/state.ts +16 -1
  67. package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
  68. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  69. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  70. package/src/gjc-runtime/launch-tmux.ts +68 -15
  71. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  72. package/src/gjc-runtime/session-layout.ts +180 -0
  73. package/src/gjc-runtime/session-resolution.ts +217 -0
  74. package/src/gjc-runtime/state-graph.ts +1 -2
  75. package/src/gjc-runtime/state-migrations.ts +1 -0
  76. package/src/gjc-runtime/state-runtime.ts +230 -121
  77. package/src/gjc-runtime/state-schema.ts +2 -0
  78. package/src/gjc-runtime/state-writer.ts +289 -41
  79. package/src/gjc-runtime/team-runtime.ts +43 -19
  80. package/src/gjc-runtime/tmux-sessions.ts +43 -2
  81. package/src/gjc-runtime/ultragoal-guard.ts +45 -2
  82. package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
  83. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  84. package/src/gjc-runtime/workflow-manifest.ts +1 -2
  85. package/src/harness-control-plane/storage.ts +14 -4
  86. package/src/hooks/native-skill-hook.ts +38 -12
  87. package/src/hooks/skill-state.ts +178 -83
  88. package/src/internal-urls/docs-index.generated.ts +9 -6
  89. package/src/migrate/action-planner.ts +318 -0
  90. package/src/migrate/adapters/claude-code.ts +39 -0
  91. package/src/migrate/adapters/codex.ts +70 -0
  92. package/src/migrate/adapters/index.ts +277 -0
  93. package/src/migrate/adapters/opencode.ts +52 -0
  94. package/src/migrate/executor.ts +81 -0
  95. package/src/migrate/mcp-mapper.ts +152 -0
  96. package/src/migrate/report.ts +104 -0
  97. package/src/migrate/skill-normalizer.ts +80 -0
  98. package/src/migrate/types.ts +163 -0
  99. package/src/modes/bridge/bridge-mode.ts +2 -2
  100. package/src/modes/components/custom-editor.ts +30 -20
  101. package/src/modes/components/welcome.ts +42 -9
  102. package/src/modes/controllers/input-controller.ts +21 -3
  103. package/src/modes/interactive-mode.ts +22 -1
  104. package/src/modes/prompt-action-autocomplete.ts +11 -1
  105. package/src/modes/rpc/rpc-mode.ts +2 -2
  106. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  107. package/src/prompts/agents/init.md +1 -1
  108. package/src/prompts/system/plan-mode-active.md +1 -1
  109. package/src/prompts/tools/ast-grep.md +1 -1
  110. package/src/prompts/tools/search.md +1 -1
  111. package/src/prompts/tools/task.md +1 -2
  112. package/src/research-plan/index.ts +1 -0
  113. package/src/research-plan/ledger.ts +177 -0
  114. package/src/rlm/artifacts.ts +12 -3
  115. package/src/rlm/index.ts +7 -0
  116. package/src/runtime-mcp/config-writer.ts +46 -0
  117. package/src/session/agent-session.ts +15 -21
  118. package/src/session/session-manager.ts +19 -2
  119. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  120. package/src/setup/hermes-setup.ts +1 -1
  121. package/src/skill-state/active-state.ts +72 -108
  122. package/src/skill-state/canonical-skills.ts +4 -0
  123. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  124. package/src/skill-state/workflow-hud.ts +4 -2
  125. package/src/skill-state/workflow-state-contract.ts +3 -3
  126. package/src/slash-commands/builtin-registry.ts +8 -4
  127. package/src/system-prompt.ts +11 -9
  128. package/src/task/agents.ts +1 -22
  129. package/src/task/index.ts +1 -41
  130. package/src/task/spawn-gate.ts +1 -38
  131. package/src/task/types.ts +1 -1
  132. package/src/tools/ask.ts +34 -12
  133. package/src/tools/computer.ts +58 -4
  134. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  135. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  136. package/src/prompts/agents/explore.md +0 -58
  137. package/src/prompts/agents/plan.md +0 -49
  138. package/src/prompts/agents/reviewer.md +0 -141
  139. package/src/prompts/agents/task.md +0 -16
  140. package/src/prompts/review-request.md +0 -70
@@ -7,6 +7,8 @@ import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
7
7
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
8
8
  import { normalizeDeepInterviewEnvelope } from "./deep-interview-state";
9
9
  import { runNativeRalplanCommand } from "./ralplan-runtime";
10
+ import { modeStatePath, sessionSpecsDir } from "./session-layout";
11
+ import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
10
12
  import { runNativeStateCommand } from "./state-runtime";
11
13
  import { appendJsonl, readExistingStateForMutation, writeArtifact, writeWorkflowEnvelopeAtomic } from "./state-writer";
12
14
 
@@ -76,10 +78,6 @@ function assertSafePathComponent(value: string, label: string): void {
76
78
  }
77
79
  }
78
80
 
79
- function encodeSessionSegment(value: string): string {
80
- return encodeURIComponent(value).replaceAll(".", "%2E");
81
- }
82
-
83
81
  function defaultSpecSlug(now: Date = new Date()): string {
84
82
  const yyyy = now.getUTCFullYear().toString().padStart(4, "0");
85
83
  const mm = (now.getUTCMonth() + 1).toString().padStart(2, "0");
@@ -89,14 +87,10 @@ function defaultSpecSlug(now: Date = new Date()): string {
89
87
  return `${yyyy}-${mm}-${dd}-${hh}${min}-${randomBytes(2).toString("hex")}`;
90
88
  }
91
89
 
92
- function stateDirFor(cwd: string, sessionId: string | undefined): string {
93
- return sessionId
94
- ? path.join(cwd, ".gjc", "state", "sessions", encodeSessionSegment(sessionId))
95
- : path.join(cwd, ".gjc", "state");
96
- }
97
-
98
- export function deepInterviewStatePath(cwd: string, sessionId: string | undefined): string {
99
- return path.join(stateDirFor(cwd, sessionId), "deep-interview-state.json");
90
+ export function deepInterviewStatePath(cwd: string, sessionId?: string): string {
91
+ const resolvedSessionId = sessionId?.trim() || process.env.GJC_SESSION_ID?.trim();
92
+ if (!resolvedSessionId) throw new Error("deep-interview state path requires a session id");
93
+ return modeStatePath(cwd, resolvedSessionId, "deep-interview");
100
94
  }
101
95
 
102
96
  async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string> {
@@ -117,7 +111,7 @@ interface ResolvedDeepInterviewArgs {
117
111
  resolution: DeepInterviewResolution;
118
112
  threshold: number;
119
113
  thresholdSource: string;
120
- sessionId?: string;
114
+ sessionId: string;
121
115
  idea: string;
122
116
  language?: DeepInterviewLanguagePreference;
123
117
  json: boolean;
@@ -134,7 +128,7 @@ export interface ResolvedDeepInterviewSpecWriteArgs {
134
128
  stage: "final";
135
129
  slug: string;
136
130
  spec: string;
137
- sessionId?: string;
131
+ sessionId: string;
138
132
  json: boolean;
139
133
  deliberate: boolean;
140
134
  handoff?: "ralplan";
@@ -277,8 +271,12 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
277
271
  throw new DeepInterviewCommandError(2, "--spec is required for deep-interview --write");
278
272
  }
279
273
 
280
- const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
281
- if (sessionId) assertSafePathComponent(sessionId, "session-id");
274
+ const session = resolveGjcSessionForWrite(cwd, {
275
+ flagValue: flagValue(args, "--session-id"),
276
+ envSessionId: process.env.GJC_SESSION_ID,
277
+ });
278
+ const sessionId = session.gjcSessionId;
279
+ assertSafePathComponent(sessionId, "session-id");
282
280
 
283
281
  const rawHandoff = flagValue(args, "--handoff")?.trim() || undefined;
284
282
  if (rawHandoff && rawHandoff !== "ralplan") {
@@ -324,8 +322,12 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
324
322
  }
325
323
 
326
324
  async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewArgs> {
327
- const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
328
- if (sessionId) assertSafePathComponent(sessionId, "session-id");
325
+ const session = resolveGjcSessionForWrite(cwd, {
326
+ flagValue: flagValue(args, "--session-id"),
327
+ envSessionId: process.env.GJC_SESSION_ID,
328
+ });
329
+ const sessionId = session.gjcSessionId;
330
+ assertSafePathComponent(sessionId, "session-id");
329
331
 
330
332
  const explicitResolutions = (["quick", "standard", "deep"] as const).filter(name => hasFlag(args, `--${name}`));
331
333
  if (explicitResolutions.length > 1) {
@@ -402,19 +404,34 @@ export async function persistDeepInterviewSpec(
402
404
  }
403
405
  const existing = existingRead.kind === "valid" ? existingRead.value : {};
404
406
 
405
- const specPath = path.join(cwd, ".gjc", "specs", `deep-interview-${resolved.slug}.md`);
407
+ const specPath = path.join(sessionSpecsDir(cwd, resolved.sessionId), `deep-interview-${resolved.slug}.md`);
406
408
  const content = resolved.spec.endsWith("\n") ? resolved.spec : `${resolved.spec}\n`;
407
409
  await writeArtifact(specPath, content, {
408
410
  cwd,
409
- audit: { category: "artifact", verb: "write", owner: "gjc-runtime", skill: "deep-interview" },
411
+ audit: {
412
+ category: "artifact",
413
+ verb: "write",
414
+ owner: "gjc-runtime",
415
+ skill: "deep-interview",
416
+ sessionId: resolved.sessionId,
417
+ },
410
418
  });
411
419
 
412
420
  const sha256 = createHash("sha256").update(content).digest("hex");
413
421
  const createdAt = new Date().toISOString();
414
422
  await appendJsonl(
415
- path.join(cwd, ".gjc", "specs", "deep-interview-index.jsonl"),
423
+ path.join(sessionSpecsDir(cwd, resolved.sessionId), "deep-interview-index.jsonl"),
416
424
  { slug: resolved.slug, stage: resolved.stage, path: specPath, created_at: createdAt, sha256 },
417
- { cwd, audit: { category: "ledger", verb: "append", owner: "gjc-runtime", skill: "deep-interview" } },
425
+ {
426
+ cwd,
427
+ audit: {
428
+ category: "ledger",
429
+ verb: "append",
430
+ owner: "gjc-runtime",
431
+ skill: "deep-interview",
432
+ sessionId: resolved.sessionId,
433
+ },
434
+ },
418
435
  );
419
436
 
420
437
  const payload = normalizeDeepInterviewEnvelope({
@@ -446,9 +463,11 @@ export async function persistDeepInterviewSpec(
446
463
  verb: "write",
447
464
  owner: "gjc-runtime",
448
465
  skill: "deep-interview",
466
+ sessionId: resolved.sessionId,
449
467
  forced: resolved.force,
450
468
  },
451
469
  });
470
+ await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "deep-interview-runtime", path: statePath });
452
471
  await syncDeepInterviewHud({
453
472
  cwd,
454
473
  sessionId: resolved.sessionId,
@@ -503,8 +522,15 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
503
522
  sessionId: resolved.sessionId,
504
523
  nowIso: now,
505
524
  },
506
- audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview" },
525
+ audit: {
526
+ category: "state",
527
+ verb: "write",
528
+ owner: "gjc-runtime",
529
+ skill: "deep-interview",
530
+ sessionId: resolved.sessionId,
531
+ },
507
532
  });
533
+ await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "deep-interview-runtime", path: statePath });
508
534
  await syncDeepInterviewHud({ cwd, sessionId: resolved.sessionId, payload, phase: "interviewing" });
509
535
  return statePath;
510
536
  }
@@ -8,6 +8,8 @@ import {
8
8
  type ModeChangeEntry,
9
9
  type SessionEntry,
10
10
  } from "../session/session-manager";
11
+ import { sessionStateDir, sessionUltragoalDir } from "./session-layout";
12
+ import { resolveGjcSessionForRead, resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
11
13
  import { removeFileAudited, writeJsonAtomic } from "./state-writer";
12
14
 
13
15
  export const GJC_SESSION_FILE_ENV = "GJC_SESSION_FILE";
@@ -48,12 +50,12 @@ function isEnoent(error: unknown): boolean {
48
50
  );
49
51
  }
50
52
 
51
- function requestPath(cwd: string): string {
52
- return path.join(cwd, ".gjc", "state", "goal-mode-request.json");
53
+ function requestPath(cwd: string, sessionId: string): string {
54
+ return path.join(sessionStateDir(cwd, sessionId), "goal-mode-request.json");
53
55
  }
54
56
 
55
- function ultragoalGoalsPath(cwd: string): string {
56
- return path.join(cwd, ".gjc", "ultragoal", "goals.json");
57
+ function ultragoalGoalsPath(cwd: string, sessionId: string): string {
58
+ return path.join(sessionUltragoalDir(cwd, sessionId), "goals.json");
57
59
  }
58
60
 
59
61
  function isCreateGoalsArg(value: string): boolean {
@@ -65,8 +67,14 @@ export function isUltragoalCreateGoalsInvocation(args: readonly string[]): boole
65
67
  return command !== undefined && isCreateGoalsArg(command);
66
68
  }
67
69
 
68
- export async function readUltragoalGjcObjective(cwd: string): Promise<{ objective: string; goalsPath: string }> {
69
- const goalsPath = ultragoalGoalsPath(cwd);
70
+ export async function readUltragoalGjcObjective(
71
+ cwd: string,
72
+ sessionId?: string | null,
73
+ ): Promise<{ objective: string; goalsPath: string }> {
74
+ const session = sessionId?.trim()
75
+ ? { gjcSessionId: sessionId.trim() }
76
+ : await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
77
+ const goalsPath = ultragoalGoalsPath(cwd, session.gjcSessionId);
70
78
  try {
71
79
  const plan = (await Bun.file(goalsPath).json()) as UltragoalPlanShape;
72
80
  const objective = typeof plan.gjcObjective === "string" ? plan.gjcObjective.trim() : "";
@@ -87,7 +95,10 @@ export async function writePendingGoalModeRequest(input: {
87
95
  }): Promise<PendingGoalModeRequest> {
88
96
  const objective = input.objective.trim();
89
97
  if (!objective) throw new Error("goal objective is required");
90
- const sessionId = input.sessionId?.trim();
98
+ const resolvedSessionId =
99
+ input.sessionId?.trim() ||
100
+ resolveGjcSessionForWrite(input.cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
101
+ const sessionId = resolvedSessionId;
91
102
  const request: PendingGoalModeRequest = {
92
103
  version: REQUEST_VERSION,
93
104
  kind: "goal_mode_request",
@@ -97,11 +108,12 @@ export async function writePendingGoalModeRequest(input: {
97
108
  goalsPath: input.goalsPath,
98
109
  ...(sessionId ? { sessionId } : {}),
99
110
  };
100
- const filePath = requestPath(input.cwd);
111
+ const filePath = requestPath(input.cwd, sessionId);
101
112
  await writeJsonAtomic(filePath, request, {
102
113
  cwd: input.cwd,
103
- audit: { category: "state", verb: "write", owner: "gjc-runtime" },
114
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", sessionId },
104
115
  });
116
+ await writeSessionActivityMarker(input.cwd, sessionId, { writer: "goal-mode-request", path: filePath });
105
117
  return request;
106
118
  }
107
119
 
@@ -175,7 +187,10 @@ export async function consumePendingGoalModeRequest(
175
187
  cwd: string,
176
188
  currentSessionId?: string | null,
177
189
  ): Promise<PendingGoalModeRequest | null> {
178
- const filePath = requestPath(cwd);
190
+ const session = currentSessionId?.trim()
191
+ ? { gjcSessionId: currentSessionId.trim() }
192
+ : await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
193
+ const filePath = requestPath(cwd, session.gjcSessionId);
179
194
  let raw: unknown;
180
195
  try {
181
196
  raw = await Bun.file(filePath).json();
@@ -203,7 +218,7 @@ export async function consumePendingGoalModeRequest(
203
218
  }
204
219
  await removeFileAudited(filePath, {
205
220
  cwd,
206
- audit: { category: "prune", verb: "remove", owner: "gjc-runtime" },
221
+ audit: { category: "prune", verb: "remove", owner: "gjc-runtime", sessionId: session.gjcSessionId },
207
222
  }).catch(error => {
208
223
  if (!isEnoent(error)) throw error;
209
224
  });
@@ -1,6 +1,8 @@
1
+ import { Buffer } from "node:buffer";
1
2
  import * as path from "node:path";
2
3
  import { safeStderrWrite } from "@gajae-code/utils";
3
4
  import type { Args } from "../cli/args";
5
+ import { tmuxRuntimeSessionPath } from "./session-layout";
4
6
  import { GJC_COORDINATOR_SESSION_ID_ENV, GJC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
5
7
  import {
6
8
  buildGjcTmuxProfileCommands,
@@ -14,7 +16,7 @@ import {
14
16
  type GjcTmuxProfileCommand,
15
17
  resolveGjcTmuxCommand,
16
18
  } from "./tmux-common";
17
- import { findGjcTmuxSessionByBranch } from "./tmux-sessions";
19
+ import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope } from "./tmux-sessions";
18
20
 
19
21
  export {
20
22
  buildGjcTmuxProfileCommands,
@@ -83,6 +85,20 @@ export interface TmuxLaunchPlan {
83
85
  sessionStateFile?: string | null;
84
86
  }
85
87
 
88
+ function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
89
+ return env.GJC_TMUX_SESSION?.trim() || undefined;
90
+ }
91
+
92
+ function findExistingSessionForLaunch(context: {
93
+ env: NodeJS.ProcessEnv;
94
+ project: string;
95
+ branch?: string | null;
96
+ }): string | undefined {
97
+ const explicit = explicitTmuxSessionName(context.env);
98
+ if (explicit) return findGjcTmuxSessionByName(explicit, context.env)?.name;
99
+ return findGjcTmuxSessionByScope(context.project, context.branch, context.env)?.name;
100
+ }
101
+
86
102
  export interface GjcTmuxProfileResult {
87
103
  skipped: boolean;
88
104
  commands: GjcTmuxProfileCommand[];
@@ -107,6 +123,7 @@ interface CommandResolutionContext {
107
123
  argv: string[];
108
124
  execPath: string;
109
125
  extraEnv?: Record<string, string>;
126
+ platform?: NodeJS.Platform;
110
127
  }
111
128
 
112
129
  function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
@@ -148,6 +165,26 @@ function buildEnvAssignments(values: Record<string, string> | undefined): string
148
165
  const entries = Object.entries(values ?? {});
149
166
  return entries.length === 0 ? "" : ` ${entries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}`;
150
167
  }
168
+ function powershellQuote(value: string): string {
169
+ return `'${value.replace(/'/g, "''")}'`;
170
+ }
171
+ function stripRootTmuxFlag(rawArgs: string[]): string[] {
172
+ return rawArgs.filter(arg => arg !== "--tmux");
173
+ }
174
+
175
+ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
176
+ const command = resolveCurrentGjcCommand(context);
177
+ const envLines = Object.entries({ [GJC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
178
+ ([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
179
+ );
180
+ const invocation = ["&", ...command.map(powershellQuote), ...stripRootTmuxFlag(rawArgs).map(powershellQuote)].join(
181
+ " ",
182
+ );
183
+ const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
184
+ const script = [...envLines, invocation, exitLine].join("\n");
185
+ const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
186
+ return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
187
+ }
151
188
 
152
189
  export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
153
190
  const env = context.env ?? process.env;
@@ -177,16 +214,26 @@ function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
177
214
  if (isBunVirtualPath(entrypoint)) {
178
215
  return isBunVirtualPath(context.execPath) ? ["gjc"] : [context.execPath];
179
216
  }
180
- const resolvedEntrypoint = path.isAbsolute(entrypoint) ? entrypoint : path.resolve(context.cwd, entrypoint);
217
+ const pathModule = pathModuleForPlatform(context.platform);
218
+ const resolvedEntrypoint = pathModule.isAbsolute(entrypoint)
219
+ ? entrypoint
220
+ : pathModule.resolve(context.cwd, entrypoint);
181
221
  if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
182
222
  return [context.execPath, resolvedEntrypoint];
183
223
  }
184
224
  return [resolvedEntrypoint];
185
225
  }
226
+ function isWindowsPlatform(platform: NodeJS.Platform | undefined): boolean {
227
+ return platform === "win32";
228
+ }
229
+ function pathModuleForPlatform(platform: NodeJS.Platform | undefined): typeof path.win32 | typeof path {
230
+ return isWindowsPlatform(platform) ? path.win32 : path;
231
+ }
186
232
 
187
233
  function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
234
+ if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
188
235
  const command = resolveCurrentGjcCommand(context);
189
- const quoted = [...command, ...rawArgs].map(shellQuote).join(" ");
236
+ const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
190
237
  return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
191
238
  }
192
239
 
@@ -305,7 +352,6 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
305
352
  if (!context.parsed.tmux || policy === "direct") return undefined;
306
353
  if (env.TMUX || env[GJC_TMUX_LAUNCHED_ENV] === "1") return undefined;
307
354
  const platform = context.platform ?? process.platform;
308
- if (platform === "win32") return undefined;
309
355
  const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
310
356
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
311
357
 
@@ -315,17 +361,23 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
315
361
  const sessionName = buildGjcTmuxSessionName(env, { branch });
316
362
  const tmuxCommand = resolveGjcTmuxCommand(env);
317
363
  const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
364
+ // The session ROOT is keyed by the active GJC session (GJC_SESSION_ID), NOT the
365
+ // coordinator/tmux identity. Fall back to the coordinator id only for standalone
366
+ // tmux launches with no GJC session context.
367
+ const gjcSessionId = env.GJC_SESSION_ID?.trim() || sessionId;
318
368
  const sessionStateFile =
319
369
  env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
320
- path.join(cwd, ".gjc", "runtime", "tmux-sessions", `${buildGjcTmuxSessionSlug(sessionName)}.json`);
370
+ tmuxRuntimeSessionPath(cwd, gjcSessionId, buildGjcTmuxSessionSlug(sessionName));
321
371
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
322
372
  if (!tmuxAvailable) return undefined;
323
- const existingBranchSessionName =
373
+ const existingSessionName =
324
374
  "existingBranchSessionName" in context
325
375
  ? (context.existingBranchSessionName ?? undefined)
326
- : context.worktreeBranch
327
- ? findGjcTmuxSessionByBranch(context.worktreeBranch, env, project)?.name
328
- : undefined;
376
+ : findExistingSessionForLaunch({
377
+ env,
378
+ project,
379
+ branch,
380
+ });
329
381
  const innerCommand = buildInnerCommand(
330
382
  {
331
383
  cwd,
@@ -335,6 +387,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
335
387
  [GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
336
388
  [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
337
389
  },
390
+ platform,
338
391
  },
339
392
  context.rawArgs,
340
393
  );
@@ -348,7 +401,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
348
401
  project,
349
402
  sessionId,
350
403
  sessionStateFile,
351
- attachSessionName: existingBranchSessionName,
404
+ attachSessionName: existingSessionName,
352
405
  };
353
406
  }
354
407
 
@@ -405,19 +458,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
405
458
  sessionId: plan.sessionId ?? null,
406
459
  sessionStateFile: plan.sessionStateFile ?? null,
407
460
  });
408
- if (profile.failures.length > 0) {
461
+ const ownershipFailure = profile.failures.find(item => item.command.args.includes("@gjc-profile"));
462
+ if (ownershipFailure) {
409
463
  cleanupCreatedTmuxSession(plan, spawnSync, options);
410
- const failure =
411
- profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
412
464
  (context.diagnosticWriter ?? safeStderrWrite)(
413
- formatTmuxLaunchDiagnostic("profile tagging failed", failure?.stderr),
465
+ formatTmuxLaunchDiagnostic("profile tagging failed", ownershipFailure.stderr),
414
466
  );
415
467
  return true;
416
468
  }
417
469
  }
418
470
  if (created.exitCode !== 0) return false;
419
- const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
471
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
420
472
  if (attached.exitCode === 0) return true;
473
+ cleanupCreatedTmuxSession(plan, spawnSync, options);
421
474
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
422
475
  return true;
423
476
  }