@gajae-code/coding-agent 0.4.4 → 0.5.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 (132) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/types/cli/fast-help.d.ts +1 -0
  3. package/dist/types/cli/setup-cli.d.ts +2 -0
  4. package/dist/types/commands/harness.d.ts +6 -0
  5. package/dist/types/commands/setup.d.ts +6 -0
  6. package/dist/types/config/model-profile-activation.d.ts +11 -2
  7. package/dist/types/config/model-profiles.d.ts +7 -0
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/model-resolver.d.ts +2 -0
  10. package/dist/types/config/models-config-schema.d.ts +35 -0
  11. package/dist/types/config/settings-schema.d.ts +4 -3
  12. package/dist/types/coordinator/contract.d.ts +1 -1
  13. package/dist/types/coordinator-mcp/server.d.ts +8 -2
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  16. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +1 -1
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  21. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  22. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  23. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  24. package/dist/types/harness-control-plane/types.d.ts +13 -1
  25. package/dist/types/hindsight/mental-models.d.ts +5 -5
  26. package/dist/types/main.d.ts +2 -2
  27. package/dist/types/modes/components/model-selector.d.ts +1 -12
  28. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  29. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  30. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  31. package/dist/types/sdk.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +2 -0
  33. package/dist/types/session/blob-store.d.ts +20 -1
  34. package/dist/types/session/session-manager.d.ts +32 -6
  35. package/dist/types/session/streaming-output.d.ts +3 -2
  36. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  37. package/dist/types/setup/hermes-setup.d.ts +7 -0
  38. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  39. package/dist/types/task/receipt.d.ts +2 -0
  40. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  41. package/dist/types/task/types.d.ts +17 -0
  42. package/dist/types/thinking-metadata.d.ts +16 -0
  43. package/dist/types/thinking.d.ts +3 -12
  44. package/dist/types/tools/index.d.ts +2 -0
  45. package/dist/types/tools/resolve.d.ts +0 -10
  46. package/dist/types/utils/tool-choice.d.ts +14 -1
  47. package/package.json +8 -7
  48. package/scripts/build-binary.ts +4 -0
  49. package/src/cli/fast-help.ts +80 -0
  50. package/src/cli/setup-cli.ts +12 -3
  51. package/src/cli.ts +112 -17
  52. package/src/commands/coordinator.ts +44 -1
  53. package/src/commands/harness.ts +128 -11
  54. package/src/commands/launch.ts +2 -2
  55. package/src/commands/mcp-serve.ts +3 -2
  56. package/src/commands/session.ts +3 -1
  57. package/src/commands/setup.ts +4 -0
  58. package/src/config/model-profile-activation.ts +15 -3
  59. package/src/config/model-profiles.ts +255 -56
  60. package/src/config/model-resolver.ts +9 -6
  61. package/src/config/models-config-schema.ts +2 -0
  62. package/src/config/settings-schema.ts +6 -3
  63. package/src/coordinator/contract.ts +1 -0
  64. package/src/coordinator-mcp/server.ts +427 -193
  65. package/src/cursor.ts +46 -4
  66. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  67. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  68. package/src/export/html/index.ts +13 -9
  69. package/src/gjc-runtime/launch-worktree.ts +12 -1
  70. package/src/gjc-runtime/session-state-sidecar.ts +38 -0
  71. package/src/gjc-runtime/team-runtime.ts +33 -7
  72. package/src/gjc-runtime/tmux-common.ts +15 -0
  73. package/src/gjc-runtime/tmux-sessions.ts +19 -11
  74. package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
  75. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  76. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  77. package/src/harness-control-plane/finalize.ts +39 -5
  78. package/src/harness-control-plane/owner.ts +87 -28
  79. package/src/harness-control-plane/phase-rollup.ts +96 -0
  80. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  81. package/src/harness-control-plane/receipt-spool.ts +128 -0
  82. package/src/harness-control-plane/receipts.ts +229 -1
  83. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  84. package/src/harness-control-plane/state-machine.ts +27 -6
  85. package/src/harness-control-plane/storage.ts +23 -0
  86. package/src/harness-control-plane/types.ts +33 -1
  87. package/src/hindsight/mental-models.ts +17 -16
  88. package/src/internal-urls/docs-index.generated.ts +8 -7
  89. package/src/main.ts +7 -3
  90. package/src/modes/components/assistant-message.ts +26 -14
  91. package/src/modes/components/diff.ts +97 -0
  92. package/src/modes/components/model-selector.ts +353 -181
  93. package/src/modes/components/status-line.ts +6 -6
  94. package/src/modes/components/tool-execution.ts +30 -13
  95. package/src/modes/controllers/event-controller.ts +5 -4
  96. package/src/modes/controllers/selector-controller.ts +33 -42
  97. package/src/modes/interactive-mode.ts +4 -5
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +3 -2
  100. package/src/modes/rpc/rpc-mode.ts +44 -14
  101. package/src/modes/rpc/rpc-types.ts +5 -2
  102. package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
  103. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  104. package/src/modes/theme/theme.ts +2 -2
  105. package/src/modes/utils/abort-message.ts +41 -0
  106. package/src/modes/utils/context-usage.ts +15 -8
  107. package/src/modes/utils/ui-helpers.ts +5 -6
  108. package/src/sdk.ts +38 -6
  109. package/src/secrets/obfuscator.ts +102 -27
  110. package/src/session/agent-session.ts +121 -25
  111. package/src/session/blob-store.ts +89 -3
  112. package/src/session/session-manager.ts +328 -57
  113. package/src/session/streaming-output.ts +185 -122
  114. package/src/session/tool-choice-queue.ts +23 -0
  115. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
  116. package/src/setup/hermes-setup.ts +63 -8
  117. package/src/task/executor.ts +69 -6
  118. package/src/task/fork-context-advisory.ts +99 -0
  119. package/src/task/index.ts +31 -2
  120. package/src/task/receipt.ts +7 -0
  121. package/src/task/render.ts +21 -1
  122. package/src/task/roi-reconciliation.ts +90 -0
  123. package/src/task/types.ts +15 -0
  124. package/src/thinking-metadata.ts +51 -0
  125. package/src/thinking.ts +26 -46
  126. package/src/tools/bash.ts +1 -1
  127. package/src/tools/index.ts +4 -2
  128. package/src/tools/resolve.ts +93 -18
  129. package/src/tools/subagent-render.ts +10 -1
  130. package/src/utils/edit-mode.ts +1 -1
  131. package/src/utils/title-generator.ts +16 -2
  132. package/src/utils/tool-choice.ts +45 -16
package/src/cursor.ts CHANGED
@@ -160,8 +160,35 @@ function formatMcpToolErrorMessage(toolName: string, availableTools: string[]):
160
160
  return `MCP tool "${toolName}" not found. Available tools: ${list}`;
161
161
  }
162
162
 
163
+ /**
164
+ * Cursor's wire protocol carries shell timeouts in milliseconds — the
165
+ * model-facing parameter is `block_until_ms`, and `ShellArgs.hard_timeout` is
166
+ * likewise documented in ms — while the bash tool's `timeout` is seconds.
167
+ * Passing the raw value through made a requested 30 s wait (30000 ms) arrive
168
+ * as 30000 s and clamp to the 3600 s ceiling, i.e. an accidental 1-hour
169
+ * timeout on a blocking command. Convert, rounding sub-second values up to 1 s
170
+ * so a tiny requested wait does not collapse to "no timeout".
171
+ */
172
+ function shellTimeoutSeconds(timeout: number | undefined): number | undefined {
173
+ if (!timeout || timeout <= 0) return undefined;
174
+ return Math.max(1, Math.ceil(timeout / 1000));
175
+ }
176
+
163
177
  export class CursorExecHandlers implements ICursorExecHandlers {
164
- constructor(private options: CursorExecBridgeOptions) {}
178
+ constructor(private options: CursorExecBridgeOptions) {
179
+ // Bind every native handler so methods stay instance-safe when invoked
180
+ // detached/unbound by the Cursor provider (e.g. `const read = handlers.read`).
181
+ // Without this, `this.#optionsForCall()` throws "undefined is not an object".
182
+ this.read = this.read.bind(this);
183
+ this.ls = this.ls.bind(this);
184
+ this.grep = this.grep.bind(this);
185
+ this.write = this.write.bind(this);
186
+ this.delete = this.delete.bind(this);
187
+ this.shell = this.shell.bind(this);
188
+ this.shellStream = this.shellStream.bind(this);
189
+ this.diagnostics = this.diagnostics.bind(this);
190
+ this.mcp = this.mcp.bind(this);
191
+ }
165
192
 
166
193
  #optionsForCall(): CursorExecBridgeOptions {
167
194
  return {
@@ -185,9 +212,24 @@ export class CursorExecHandlers implements ICursorExecHandlers {
185
212
 
186
213
  async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
187
214
  const toolCallId = decodeToolCallId(args.toolCallId);
215
+ // Cursor's native Glob tool arrives as a grep exec with a glob but no content
216
+ // pattern. The search tool requires a non-empty pattern, so an empty pattern
217
+ // means "list files matching this glob" — route that to find instead of
218
+ // throwing "Pattern must not be empty".
219
+ const pattern = typeof args.pattern === "string" ? args.pattern : "";
220
+ if (pattern.trim().length === 0) {
221
+ if (args.glob) {
222
+ const globPath = `${args.path || "."}/${args.glob}`;
223
+ return executeTool(this.#optionsForCall(), "find", toolCallId, { paths: [globPath] });
224
+ }
225
+ const result = buildToolErrorResult(
226
+ "Cursor grep request rejected: pattern must not be empty. Provide a non-empty search pattern.",
227
+ );
228
+ return createToolResultMessage(toolCallId, "search", result, true);
229
+ }
188
230
  const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
189
231
  const toolResultMessage = await executeTool(this.#optionsForCall(), "search", toolCallId, {
190
- pattern: args.pattern,
232
+ pattern,
191
233
  paths: [searchPath],
192
234
  i: args.caseInsensitive || undefined,
193
235
  });
@@ -212,7 +254,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
212
254
 
213
255
  async shell(args: Parameters<NonNullable<ICursorExecHandlers["shell"]>>[0]) {
214
256
  const toolCallId = decodeToolCallId(args.toolCallId);
215
- const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
257
+ const timeoutSeconds = shellTimeoutSeconds(args.timeout);
216
258
  const toolResultMessage = await executeTool(this.#optionsForCall(), "bash", toolCallId, {
217
259
  command: args.command,
218
260
  cwd: args.workingDirectory || undefined,
@@ -234,7 +276,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
234
276
  return createToolResultMessage(toolCallId, toolName, result, true);
235
277
  }
236
278
 
237
- const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
279
+ const timeoutSeconds = shellTimeoutSeconds(args.timeout);
238
280
  const toolArgs: Record<string, unknown> = {
239
281
  command: args.command,
240
282
  cwd: args.workingDirectory || undefined,
@@ -306,8 +306,9 @@ Worker protocol:
306
306
 
307
307
  Useful runtime env vars:
308
308
 
309
- - `GJC_TEAM_TMUX_COMMAND`
310
- - tmux binary/command override (default `tmux`)
309
+ - `GJC_TMUX_COMMAND` / `GJC_TEAM_TMUX_COMMAND`
310
+ - tmux binary/command override (default `tmux`). `GJC_TMUX_COMMAND` applies to every GJC tmux flow; `GJC_TEAM_TMUX_COMMAND` is honored as an alias by the team path. Both resolve through the same resolver, so the team leader and `gjc session ...` always target the same multiplexer.
311
+ - Multiplexer support boundary: GJC-managed sessions and the team leader are detected via tmux user options (`@gjc-profile`, written with `set-option` and read back with `show-options` / `list-sessions -F`). A provider must round-trip those user options to be supported. Real tmux works. Alternative multiplexers such as psmux on Windows do not reliably persist tmux user options yet, so `gjc session status` reports `gjc_tmux_session_untagged` (the session exists in the multiplexer but is not GJC-tagged) and team startup rejects the leader as `unmanaged_tmux_session`. The Windows-native psmux path is therefore not fully supported; use real tmux for GJC-managed session and team flows.
311
312
  - `GJC_TEAM_WORKER_COMMAND`
312
313
  - worker command override (default resolves to active GJC entrypoint or `gjc`)
313
314
  - `GJC_TEAM_STATE_ROOT`
@@ -120,9 +120,15 @@ Examples:
120
120
 
121
121
  ```sh
122
122
  gjc ultragoal steer --kind add_subgoal --title "Investigate blocker" --objective "Validate the blocker and report evidence." --evidence "log/test output" --rationale "The blocker changes the safe execution order." --json
123
- gjc ultragoal steer --directive-json ./steering.json --json
123
+ gjc ultragoal steer --kind split_subgoal --goal-id G002 --replacements-json '[{"title":"Fix parser","objective":"Resolve parser blocker."},{"title":"Verify parser","objective":"Run focused parser verification."}]' --evidence "Implementation split found two separable risks" --rationale "Splitting keeps each sub-goal independently verifiable." --json
124
+ gjc ultragoal steer --kind reorder_pending --order-json '["G003","G002"]' --evidence "Dependency order changed after investigation" --rationale "G003 must land before G002 can proceed safely." --json
125
+ gjc ultragoal steer --kind revise_pending_wording --goal-id G002 --title "Clarify blocker story" --evidence "The current title hides the actual blocker" --rationale "Clear wording keeps the ledger auditable." --json
126
+ gjc ultragoal steer --kind annotate_ledger --evidence "User changed release ordering at runtime" --rationale "The aggregate objective is unchanged, but the execution history needs an audit note." --json
127
+ gjc ultragoal steer --kind mark_blocked_superseded --goal-id G004 --evidence "The blocked work is no longer required because replacement evidence covers it" --rationale "No replacement sub-goal is needed; superseding only the blocked sub-goal unblocks final completion without changing the aggregate objective." --json
124
128
  ```
125
129
 
130
+ `--directive-json` and UserPromptSubmit structured steering are planned/deferred routing surfaces, not part of the native typed `--kind` CLI path described above.
131
+
126
132
  Steering invariants:
127
133
 
128
134
  - Do not edit the aggregate goal objective, original brief constraints, quality gates, or completion status. The aggregate objective is a stable pointer to `.gjc/ultragoal/goals.json` and `.gjc/ultragoal/ledger.jsonl`, not an enumeration of initial goal ids.
@@ -131,7 +137,7 @@ Steering invariants:
131
137
  - Superseded goals remain in `goals.json` with steering metadata and are skipped for scheduling.
132
138
  - Blocked goals without replacements are skipped for scheduling but still block final completion until later explicit steering replaces or supersedes them.
133
139
 
134
- UserPromptSubmit uses the same steering API only for structured directives such as `GJC_ULTRAGOAL_STEER: { ... }`, `gjc.ultragoal.steer: { ... }`, or `gjc ultragoal steer: { ... }`. Normal prose does not mutate state, and repeated prompt-submit directives dedupe by prompt signature or idempotency key.
140
+ UserPromptSubmit structured steering directives are a planned/deferred routing surface. Normal prose does not mutate state.
135
141
 
136
142
  ## Role-agent delegation guidance
137
143
 
@@ -150,15 +150,19 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
150
150
  throw err;
151
151
  }
152
152
 
153
- const sessionData: SessionData = {
154
- header: sm.getHeader(),
155
- entries: sm.getEntries(),
156
- leafId: sm.getLeafId(),
157
- };
153
+ try {
154
+ const sessionData: SessionData = {
155
+ header: sm.getHeader(),
156
+ entries: sm.getEntries(),
157
+ leafId: sm.getLeafId(),
158
+ };
158
159
 
159
- const html = await generateHtml(sessionData, opts.themeName);
160
- const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
160
+ const html = await generateHtml(sessionData, opts.themeName);
161
+ const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
161
162
 
162
- await Bun.write(outputPath, html);
163
- return outputPath;
163
+ await Bun.write(outputPath, html);
164
+ return outputPath;
165
+ } finally {
166
+ await sm.close();
167
+ }
164
168
  }
@@ -140,6 +140,17 @@ function readWorktreeEntryFromPath(repoRoot: string, worktreePath: string): GitW
140
140
  return { path: path.resolve(worktreePath), head, branchRef, detached: !branchRef };
141
141
  }
142
142
 
143
+ function resolveCanonicalRepoRoot(cwd: string): string {
144
+ const repoRoot = runGit(cwd, ["rev-parse", "--show-toplevel"]);
145
+ const commonDir = tryRunGit(repoRoot, ["rev-parse", "--git-common-dir"]);
146
+ if (!commonDir) return repoRoot;
147
+ const resolvedCommonDir = path.resolve(repoRoot, commonDir);
148
+ if (path.basename(resolvedCommonDir) !== ".git") return repoRoot;
149
+ const ownerRoot = path.dirname(resolvedCommonDir);
150
+ if (tryRunGit(ownerRoot, ["rev-parse", "--is-inside-work-tree"]) !== "true") return repoRoot;
151
+ return ownerRoot;
152
+ }
153
+
143
154
  function isWorktreeDirty(worktreePath: string): boolean {
144
155
  return runGit(worktreePath, ["status", "--porcelain"]).length > 0;
145
156
  }
@@ -187,7 +198,7 @@ export function planLaunchWorktree(
187
198
  mode: GjcLaunchWorktreeMode,
188
199
  ): GjcLaunchWorktreePlan | { enabled: false } {
189
200
  if (!mode.enabled) return { enabled: false };
190
- const repoRoot = runGit(cwd, ["rev-parse", "--show-toplevel"]);
201
+ const repoRoot = resolveCanonicalRepoRoot(cwd);
191
202
  const baseRef = runGit(repoRoot, ["rev-parse", "HEAD"]);
192
203
  const branchName = mode.detached ? null : mode.name;
193
204
  if (branchName) validateBranchName(repoRoot, branchName);
@@ -30,6 +30,33 @@ function lastAssistant(messages: unknown[] | undefined): AssistantMessage | unde
30
30
  return undefined;
31
31
  }
32
32
 
33
+ function assistantText(assistant: AssistantMessage | undefined): string | null {
34
+ if (!assistant) return null;
35
+ const text = assistant.content
36
+ .filter(part => part.type === "text")
37
+ .map(part => part.text)
38
+ .join("\n")
39
+ .trim();
40
+ return text.length > 0 ? text : null;
41
+ }
42
+
43
+ function finalResponseForEvent(event: RuntimeStateEvent): {
44
+ text: string | null;
45
+ format: "markdown";
46
+ source: "agent_end";
47
+ artifact_path: null;
48
+ truncated: false;
49
+ } | null {
50
+ if (event.type !== "agent_end") return null;
51
+ return {
52
+ text: assistantText(lastAssistant(event.messages)),
53
+ format: "markdown",
54
+ source: "agent_end",
55
+ artifact_path: null,
56
+ truncated: false,
57
+ };
58
+ }
59
+
33
60
  function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
34
61
  if (event.type === "agent_start" || event.type === "turn_start") return "running";
35
62
  if (event.type === "agent_end") {
@@ -55,6 +82,7 @@ export async function persistCoordinatorRuntimeStateFromEvent(
55
82
  } catch {
56
83
  previous = {};
57
84
  }
85
+ const finalResponse = finalResponseForEvent(event);
58
86
  const payload = {
59
87
  schema_version: 1,
60
88
  session_id: process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId,
@@ -69,6 +97,16 @@ export async function persistCoordinatorRuntimeStateFromEvent(
69
97
  event: event.type,
70
98
  cwd: context.cwd,
71
99
  session_file: context.sessionFile ?? null,
100
+ ...(finalResponse ? { final_response: finalResponse } : {}),
101
+ ...(state === "errored"
102
+ ? {
103
+ error: {
104
+ code: "agent_error",
105
+ message: lastAssistant(event.messages)?.errorMessage ?? "agent_error",
106
+ recoverable: true,
107
+ },
108
+ }
109
+ : {}),
72
110
  };
73
111
  try {
74
112
  await fs.mkdir(path.dirname(stateFile), { recursive: true });
@@ -5,7 +5,7 @@ import type { WorkflowHudSummary } from "../skill-state/active-state";
5
5
  import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
6
6
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
7
7
 
8
- import { applyGjcTmuxProfile } from "./launch-tmux";
8
+ import { applyGjcTmuxProfile, GJC_TMUX_LAUNCHED_ENV } from "./launch-tmux";
9
9
  import {
10
10
  AlreadyExistsError,
11
11
  appendJsonl as appendJsonlAudited,
@@ -17,7 +17,12 @@ import {
17
17
  writeReport,
18
18
  writeWorkflowEnvelopeAtomic,
19
19
  } from "./state-writer";
20
- import { GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
20
+ import {
21
+ buildGjcTmuxUntaggedSessionHint,
22
+ GJC_TMUX_PROFILE_OPTION,
23
+ GJC_TMUX_PROFILE_VALUE,
24
+ resolveGjcTmuxCommand,
25
+ } from "./tmux-common";
21
26
 
22
27
  export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
23
28
  export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
@@ -1622,9 +1627,6 @@ async function ensureWorkerWorktree(
1622
1627
  };
1623
1628
  }
1624
1629
 
1625
- export function resolveGjcTmuxCommand(env: NodeJS.ProcessEnv = process.env): string {
1626
- return env.GJC_TEAM_TMUX_COMMAND?.trim() || "tmux";
1627
- }
1628
1630
  function buildTeamTmuxLeaderRequirementMessage(detail?: string): string {
1629
1631
  const suffix = detail?.trim() ? `:${detail.trim()}` : "";
1630
1632
  return `gjc_team_requires_tmux_leader: run \`gjc --tmux\` first, then run \`gjc team ...\` inside that tmux-backed leader session, or use \`gjc team --dry-run\` for state-only smoke tests${suffix}`;
@@ -1641,6 +1643,17 @@ function readGjcTmuxProfileValue(tmuxCommand: string, sessionName: string): stri
1641
1643
  return result.stdout.toString().trim();
1642
1644
  }
1643
1645
 
1646
+ function retagGjcLaunchedTmuxSession(tmuxCommand: string, sessionName: string): boolean {
1647
+ const result = Bun.spawnSync(
1648
+ [tmuxCommand, "set-option", "-t", `=${sessionName}`, GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE],
1649
+ {
1650
+ stdout: "pipe",
1651
+ stderr: "pipe",
1652
+ },
1653
+ );
1654
+ return result.exitCode === 0;
1655
+ }
1656
+
1644
1657
  function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): GjcTmuxLeaderContext {
1645
1658
  const paneTarget = env.TMUX_PANE?.trim();
1646
1659
  const args = paneTarget
@@ -1652,8 +1665,21 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
1652
1665
  const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
1653
1666
  if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
1654
1667
  throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
1655
- if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE)
1656
- throw new Error(buildTeamTmuxLeaderRequirementMessage(`unmanaged_tmux_session:${sessionName}`));
1668
+ if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE) {
1669
+ // Self-heal: a pane launched through `gjc --tmux` exports
1670
+ // GJC_TMUX_LAUNCHED=1, but the session can lose (or never receive) the
1671
+ // @gjc-profile user-option tag when startup attach fails mid-way or the
1672
+ // registry write races. That stranded-but-genuinely-GJC leader pane
1673
+ // previously hard-failed as unmanaged_tmux_session; re-tag it instead.
1674
+ const launchedByGjc = env[GJC_TMUX_LAUNCHED_ENV] === "1";
1675
+ const retagged = launchedByGjc && retagGjcLaunchedTmuxSession(tmuxCommand, sessionName);
1676
+ if (!retagged || readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE)
1677
+ throw new Error(
1678
+ buildTeamTmuxLeaderRequirementMessage(
1679
+ `unmanaged_tmux_session:${sessionName} — ${buildGjcTmuxUntaggedSessionHint(tmuxCommand)}`,
1680
+ ),
1681
+ );
1682
+ }
1657
1683
  return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
1658
1684
  }
1659
1685
  export function resolveGjcWorkerCommand(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
@@ -32,6 +32,21 @@ export function resolveGjcTmuxCommand(env: NodeJS.ProcessEnv = process.env): str
32
32
  return env[GJC_TMUX_COMMAND_ENV]?.trim() || env.GJC_TEAM_TMUX_COMMAND?.trim() || "tmux";
33
33
  }
34
34
 
35
+ export const GJC_TMUX_UNTAGGED_REASON = "gjc_tmux_session_untagged";
36
+
37
+ export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
38
+ return (
39
+ `the active multiplexer "${tmuxCommand}" lists this session but did not return GJC's ${GJC_TMUX_PROFILE_OPTION} ownership tag; ` +
40
+ "GJC-managed sessions and `gjc team` require a tmux provider that round-trips tmux user options. " +
41
+ "Alternative multiplexers such as psmux on Windows do not persist user options yet, so the Windows-native psmux path is not fully supported; " +
42
+ "use real tmux for GJC-managed session and team flows."
43
+ );
44
+ }
45
+
46
+ export function buildGjcTmuxUntaggedSessionError(sessionName: string, tmuxCommand: string): string {
47
+ return `${GJC_TMUX_UNTAGGED_REASON}:${sessionName} — ${buildGjcTmuxUntaggedSessionHint(tmuxCommand)}`;
48
+ }
49
+
35
50
  export function sanitizeTmuxToken(value: string): string {
36
51
  return (
37
52
  value
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  buildGjcTmuxProfileCommands,
3
3
  buildGjcTmuxSessionName,
4
+ buildGjcTmuxUntaggedSessionError,
4
5
  GJC_TMUX_BRANCH_OPTION,
5
6
  GJC_TMUX_BRANCH_SLUG_OPTION,
6
7
  GJC_TMUX_PROFILE_OPTION,
@@ -73,17 +74,10 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
73
74
  };
74
75
  }
75
76
 
76
- function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
77
+ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env): string[] {
77
78
  let output = "";
78
79
  try {
79
- output = runTmux(
80
- [
81
- "list-sessions",
82
- "-F",
83
- `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}`,
84
- ],
85
- env,
86
- );
80
+ output = runTmux(["list-sessions", "-F", format], env);
87
81
  } catch (error) {
88
82
  const message = error instanceof Error ? error.message : String(error);
89
83
  if (message.includes("no server running") || message.includes("failed to connect to server")) return [];
@@ -95,6 +89,17 @@ function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
95
89
  .filter(Boolean);
96
90
  }
97
91
 
92
+ function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
93
+ return runListSessions(
94
+ `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}`,
95
+ env,
96
+ );
97
+ }
98
+
99
+ function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[] {
100
+ return runListSessions("#{session_name}", env).map(line => line.split("\t")[0] ?? line);
101
+ }
102
+
98
103
  export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
99
104
  return listSessionLines(env)
100
105
  .map(parseSessionLine)
@@ -114,8 +119,11 @@ export function findGjcTmuxSessionByBranch(
114
119
 
115
120
  export function statusGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
116
121
  const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
117
- if (!session) throw new Error(`gjc_tmux_session_not_found:${sessionName}`);
118
- return session;
122
+ if (session) return session;
123
+ if (listRawTmuxSessionNames(env).includes(sessionName)) {
124
+ throw new Error(buildGjcTmuxUntaggedSessionError(sessionName, resolveGjcTmuxCommand(env)));
125
+ }
126
+ throw new Error(`gjc_tmux_session_not_found:${sessionName}`);
119
127
  }
120
128
 
121
129
  export function createGjcTmuxSession(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {