@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.
- package/CHANGELOG.md +50 -0
- package/README.md +73 -1
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/package.json +7 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/setup-cli.ts +14 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/launch.ts +1 -1
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-registry.ts +9 -2
- package/src/config/model-resolver.ts +13 -2
- package/src/config/settings-schema.ts +17 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +51 -47
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
- package/src/exec/bash-executor.ts +3 -1
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/gjc-plugins/injection.ts +23 -4
- package/src/extensibility/gjc-plugins/state.ts +16 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
- package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
- package/src/gjc-runtime/goal-mode-request.ts +26 -11
- package/src/gjc-runtime/launch-tmux.ts +68 -15
- package/src/gjc-runtime/ralplan-runtime.ts +79 -50
- package/src/gjc-runtime/session-layout.ts +180 -0
- package/src/gjc-runtime/session-resolution.ts +217 -0
- package/src/gjc-runtime/state-graph.ts +1 -2
- package/src/gjc-runtime/state-migrations.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +230 -121
- package/src/gjc-runtime/state-schema.ts +2 -0
- package/src/gjc-runtime/state-writer.ts +289 -41
- package/src/gjc-runtime/team-runtime.ts +43 -19
- package/src/gjc-runtime/tmux-sessions.ts +43 -2
- package/src/gjc-runtime/ultragoal-guard.ts +45 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.ts +1 -2
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +9 -6
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/welcome.ts +42 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +22 -1
- package/src/modes/prompt-action-autocomplete.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +7 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/session/agent-session.ts +15 -21
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/skill-state/active-state.ts +72 -108
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
- package/src/task/agents.ts +1 -22
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +34 -12
- package/src/tools/computer.ts +58 -4
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- 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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
281
|
-
|
|
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
|
|
328
|
-
|
|
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,
|
|
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: {
|
|
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,
|
|
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
|
-
{
|
|
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: {
|
|
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,
|
|
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,
|
|
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(
|
|
69
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
370
|
+
tmuxRuntimeSessionPath(cwd, gjcSessionId, buildGjcTmuxSessionSlug(sessionName));
|
|
321
371
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
322
372
|
if (!tmuxAvailable) return undefined;
|
|
323
|
-
const
|
|
373
|
+
const existingSessionName =
|
|
324
374
|
"existingBranchSessionName" in context
|
|
325
375
|
? (context.existingBranchSessionName ?? undefined)
|
|
326
|
-
:
|
|
327
|
-
|
|
328
|
-
|
|
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:
|
|
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
|
-
|
|
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",
|
|
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
|
}
|