@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.
- package/CHANGELOG.md +83 -0
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +2 -0
- package/dist/types/commands/harness.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +13 -1
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +32 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/setup/hermes-setup.d.ts +7 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +17 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +12 -3
- package/src/cli.ts +112 -17
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +128 -11
- package/src/commands/launch.ts +2 -2
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/session.ts +3 -1
- package/src/commands/setup.ts +4 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +427 -193
- package/src/cursor.ts +46 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +87 -28
- package/src/harness-control-plane/phase-rollup.ts +96 -0
- package/src/harness-control-plane/receipt-ingest.ts +127 -0
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +33 -1
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +7 -3
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/abort-message.ts +41 -0
- package/src/modes/utils/context-usage.ts +15 -8
- package/src/modes/utils/ui-helpers.ts +5 -6
- package/src/sdk.ts +38 -6
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +121 -25
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +328 -57
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/executor.ts +69 -6
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +7 -0
- package/src/task/render.ts +21 -1
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +15 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +4 -2
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/title-generator.ts +16 -2
- 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
|
|
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
|
|
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
|
|
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 --
|
|
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
|
|
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
|
|
package/src/export/html/index.ts
CHANGED
|
@@ -150,15 +150,19 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
|
|
|
150
150
|
throw err;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
try {
|
|
154
|
+
const sessionData: SessionData = {
|
|
155
|
+
header: sm.getHeader(),
|
|
156
|
+
entries: sm.getEntries(),
|
|
157
|
+
leafId: sm.getLeafId(),
|
|
158
|
+
};
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
const html = await generateHtml(sessionData, opts.themeName);
|
|
161
|
+
const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 (
|
|
118
|
-
|
|
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 {
|