@gajae-code/coding-agent 0.9.1 → 0.9.3
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 +36 -0
- package/dist/types/capability/mcp.d.ts +3 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +7 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/status-line/types.d.ts +6 -0
- package/dist/types/modes/components/status-line.d.ts +1 -0
- package/dist/types/modes/components/thinking-selector.d.ts +6 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
- package/dist/types/notifications/config-commands.d.ts +29 -0
- package/dist/types/notifications/index.d.ts +12 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/threaded-render.d.ts +2 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
- package/dist/types/runtime-mcp/types.d.ts +5 -4
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/dist/types/tools/skill.d.ts +9 -8
- package/dist/types/utils/linkedom.d.ts +4 -0
- package/dist/types/utils/pasted-image-path.d.ts +5 -3
- package/package.json +11 -7
- package/scripts/build-binary.ts +2 -40
- package/scripts/compile-args.ts +91 -0
- package/scripts/generate-docs-index.ts +1 -0
- package/src/capability/mcp.ts +3 -1
- package/src/cli/args.ts +8 -0
- package/src/cli/mcp-cli.ts +108 -26
- package/src/cli/notify-cli.ts +27 -1
- package/src/cli.ts +26 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/mcp.ts +8 -8
- package/src/commands/notify.ts +2 -2
- package/src/commands/stats.ts +6 -2
- package/src/config/mcp-schema.json +21 -17
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +21 -4
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +3 -2
- package/src/config/settings.ts +24 -1
- package/src/coordinator-mcp/server.ts +7 -4
- package/src/discovery/builtin.ts +14 -0
- package/src/discovery/mcp-json.ts +14 -0
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/gjc-runtime/launch-tmux.ts +72 -12
- package/src/gjc-runtime/psmux-detect.ts +17 -2
- package/src/gjc-runtime/state-runtime.ts +105 -12
- package/src/gjc-runtime/team-runtime.ts +35 -3
- package/src/gjc-runtime/workflow-manifest.generated.json +4 -28
- package/src/gjc-runtime/workflow-manifest.ts +1 -7
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/gjc-protocol.ts +14 -1
- package/src/main.ts +26 -3
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/status-line/presets.ts +5 -7
- package/src/modes/components/status-line/segments.ts +14 -0
- package/src/modes/components/status-line/types.ts +6 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/thinking-selector.ts +79 -18
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/controllers/event-controller.ts +9 -9
- package/src/modes/controllers/input-controller.ts +52 -11
- package/src/modes/controllers/selector-controller.ts +7 -5
- package/src/modes/interactive-mode.ts +12 -4
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +202 -47
- package/src/notifications/config-commands.ts +70 -0
- package/src/notifications/index.ts +156 -9
- package/src/notifications/rich-render.ts +1 -0
- package/src/notifications/telegram-daemon-cli.ts +13 -5
- package/src/notifications/telegram-daemon.ts +87 -4
- package/src/notifications/threaded-render.ts +14 -0
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/prompts/tools/skill.md +2 -2
- package/src/runtime-credential-selector.ts +35 -0
- package/src/runtime-mcp/config-writer.ts +4 -3
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/types.ts +5 -4
- package/src/sdk.ts +68 -5
- package/src/session/agent-session.ts +159 -21
- package/src/session/session-manager.ts +68 -40
- package/src/system-prompt.ts +4 -6
- package/src/tools/bash-interactive.ts +9 -4
- package/src/tools/browser/readable.ts +2 -2
- package/src/tools/fetch.ts +5 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +29 -13
- package/src/utils/linkedom.ts +14 -0
- package/src/utils/pasted-image-path.ts +23 -9
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +2 -2
- package/src/web/scrapers/twitter.ts +3 -2
- package/src/web/scrapers/wikipedia.ts +2 -2
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2314
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
|
@@ -30,6 +30,7 @@ interface MCPConfigFile {
|
|
|
30
30
|
command?: string;
|
|
31
31
|
args?: string[];
|
|
32
32
|
env?: Record<string, string>;
|
|
33
|
+
noInheritEnv?: boolean;
|
|
33
34
|
cwd?: string;
|
|
34
35
|
url?: string;
|
|
35
36
|
headers?: Record<string, string>;
|
|
@@ -92,6 +93,18 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
let noInheritEnv: boolean | undefined;
|
|
97
|
+
if (serverConfig.noInheritEnv !== undefined) {
|
|
98
|
+
if (typeof serverConfig.noInheritEnv === "boolean") {
|
|
99
|
+
noInheritEnv = serverConfig.noInheritEnv;
|
|
100
|
+
} else {
|
|
101
|
+
logger.warn("MCP server has invalid 'noInheritEnv' value, ignoring", {
|
|
102
|
+
name,
|
|
103
|
+
value: serverConfig.noInheritEnv,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
const server: MCPServer = {
|
|
96
109
|
name,
|
|
97
110
|
enabled,
|
|
@@ -100,6 +113,7 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
|
|
|
100
113
|
command: serverConfig.command,
|
|
101
114
|
args: serverConfig.args,
|
|
102
115
|
env: serverConfig.env,
|
|
116
|
+
noInheritEnv,
|
|
103
117
|
cwd: serverConfig.cwd,
|
|
104
118
|
url: serverConfig.url,
|
|
105
119
|
headers: serverConfig.headers,
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { findRepoRoot } from "../capability/fs";
|
|
5
|
+
import type { Skill as CapabilitySkill } from "../capability/skill";
|
|
6
|
+
import type { SkillsSettings } from "../config/settings-schema";
|
|
7
|
+
import { compareSkillOrder, scanSkillsFromDir } from "../discovery/helpers";
|
|
8
|
+
import type { Skill } from "./skills";
|
|
9
|
+
|
|
10
|
+
export type RuntimeSkillDiscoverySource = "project" | "user";
|
|
11
|
+
|
|
12
|
+
export interface RuntimeSkillDiscoveryCandidate {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
source: RuntimeSkillDiscoverySource;
|
|
16
|
+
path: string;
|
|
17
|
+
useWhen?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DiscoverRuntimeSkillsOptions {
|
|
21
|
+
cwd: string;
|
|
22
|
+
home?: string;
|
|
23
|
+
query?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
source?: RuntimeSkillDiscoverySource | "all";
|
|
26
|
+
policy?: SkillsSettings;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getRuntimeHome(): string {
|
|
30
|
+
return process.env.HOME || os.homedir();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_LIMIT = 20;
|
|
34
|
+
const MAX_LIMIT = 50;
|
|
35
|
+
|
|
36
|
+
function normalizeLimit(limit: number | undefined): number {
|
|
37
|
+
if (limit === undefined || !Number.isFinite(limit)) return DEFAULT_LIMIT;
|
|
38
|
+
return Math.max(1, Math.min(MAX_LIMIT, Math.trunc(limit)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getProjectSkillDirs(cwd: string, home: string): Promise<{ dirs: string[]; repoRoot: string | null }> {
|
|
42
|
+
const dirs: string[] = [];
|
|
43
|
+
let current = path.resolve(cwd);
|
|
44
|
+
const resolvedHome = path.resolve(home);
|
|
45
|
+
const repoRoot = await findRepoRoot(current);
|
|
46
|
+
const stop = path.resolve(repoRoot ?? current);
|
|
47
|
+
while (true) {
|
|
48
|
+
if (current !== resolvedHome) {
|
|
49
|
+
dirs.push(path.join(current, ".gjc", "skills"));
|
|
50
|
+
}
|
|
51
|
+
if (current === stop) break;
|
|
52
|
+
const parent = path.dirname(current);
|
|
53
|
+
if (parent === current) break;
|
|
54
|
+
current = parent;
|
|
55
|
+
}
|
|
56
|
+
return { dirs, repoRoot };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getUseWhen(skill: CapabilitySkill): string[] | undefined {
|
|
60
|
+
const frontmatter = skill.frontmatter as Record<string, unknown> | undefined;
|
|
61
|
+
const values: string[] = [];
|
|
62
|
+
const globs = frontmatter?.globs;
|
|
63
|
+
if (Array.isArray(globs)) {
|
|
64
|
+
values.push(...globs.filter((value): value is string => typeof value === "string"));
|
|
65
|
+
} else if (typeof globs === "string") {
|
|
66
|
+
values.push(globs);
|
|
67
|
+
}
|
|
68
|
+
for (const key of ["use_when", "useWhen", "conditions"]) {
|
|
69
|
+
const raw = frontmatter?.[key];
|
|
70
|
+
if (typeof raw === "string") values.push(raw);
|
|
71
|
+
if (Array.isArray(raw)) values.push(...raw.filter((value): value is string => typeof value === "string"));
|
|
72
|
+
}
|
|
73
|
+
return values.length > 0 ? values : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toRuntimeSkill(skill: CapabilitySkill, source: RuntimeSkillDiscoverySource): Skill {
|
|
77
|
+
return {
|
|
78
|
+
name: skill.name,
|
|
79
|
+
description: typeof skill.frontmatter?.description === "string" ? skill.frontmatter.description : "",
|
|
80
|
+
filePath: skill.path,
|
|
81
|
+
baseDir: skill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
82
|
+
source: `runtime:${source}`,
|
|
83
|
+
hide: skill.frontmatter?.hide === true,
|
|
84
|
+
_source: { ...skill._source, providerName: "Runtime skill discovery" },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function sourceEnabled(source: RuntimeSkillDiscoverySource, policy: SkillsSettings | undefined): boolean {
|
|
88
|
+
if (policy?.enabled !== true) return false;
|
|
89
|
+
if (source === "project") return policy.enablePiProject === true;
|
|
90
|
+
if (source === "user") return policy.enablePiUser === true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function matchesIncludePatterns(name: string, includeSkills: string[] | undefined): boolean {
|
|
95
|
+
if (!includeSkills || includeSkills.length === 0) return true;
|
|
96
|
+
return includeSkills.some(pattern => new Bun.Glob(pattern).match(name));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function matchesIgnorePatterns(name: string, ignoredSkills: string[] | undefined): boolean {
|
|
100
|
+
if (!ignoredSkills || ignoredSkills.length === 0) return false;
|
|
101
|
+
return ignoredSkills.some(pattern => new Bun.Glob(pattern).match(name));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isDisabledSkill(name: string, disabledExtensions: string[] | undefined): boolean {
|
|
105
|
+
return (disabledExtensions ?? []).some(id => id === `skill:${name}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isAllowedByPolicy(
|
|
109
|
+
skill: CapabilitySkill,
|
|
110
|
+
source: RuntimeSkillDiscoverySource,
|
|
111
|
+
policy: SkillsSettings | undefined,
|
|
112
|
+
): boolean {
|
|
113
|
+
if (!sourceEnabled(source, policy)) return false;
|
|
114
|
+
if (isDisabledSkill(skill.name, policy?.disabledExtensions)) return false;
|
|
115
|
+
if (matchesIgnorePatterns(skill.name, policy?.ignoredSkills)) return false;
|
|
116
|
+
if (!matchesIncludePatterns(skill.name, policy?.includeSkills)) return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function matchesQuery(candidate: RuntimeSkillDiscoveryCandidate, query: string): boolean {
|
|
120
|
+
const normalized = query.trim().toLowerCase();
|
|
121
|
+
if (!normalized) return true;
|
|
122
|
+
const haystack = [candidate.name, candidate.description, candidate.source, ...(candidate.useWhen ?? [])]
|
|
123
|
+
.join("\n")
|
|
124
|
+
.toLowerCase();
|
|
125
|
+
return normalized
|
|
126
|
+
.split(/\s+/)
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.every(term => haystack.includes(term));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function realPathOrSelf(filePath: string): Promise<string> {
|
|
132
|
+
try {
|
|
133
|
+
return await fs.realpath(filePath);
|
|
134
|
+
} catch {
|
|
135
|
+
return filePath;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function discoverRuntimeSkills(
|
|
140
|
+
options: DiscoverRuntimeSkillsOptions,
|
|
141
|
+
): Promise<RuntimeSkillDiscoveryCandidate[]> {
|
|
142
|
+
const home = options.home ?? getRuntimeHome();
|
|
143
|
+
const source = options.source ?? "all";
|
|
144
|
+
const policy = options.policy;
|
|
145
|
+
const scanJobs: Array<Promise<{ skill: CapabilitySkill; source: RuntimeSkillDiscoverySource }[]>> = [];
|
|
146
|
+
const projectSkills = await getProjectSkillDirs(options.cwd, home);
|
|
147
|
+
const projectContext = { cwd: options.cwd, home, repoRoot: projectSkills.repoRoot };
|
|
148
|
+
if ((source === "all" || source === "project") && sourceEnabled("project", policy)) {
|
|
149
|
+
for (const dir of projectSkills.dirs) {
|
|
150
|
+
scanJobs.push(
|
|
151
|
+
scanSkillsFromDir(projectContext, {
|
|
152
|
+
dir,
|
|
153
|
+
providerId: "runtime",
|
|
154
|
+
level: "project",
|
|
155
|
+
requireDescription: true,
|
|
156
|
+
}).then(result => result.items.map(skill => ({ skill, source: "project" as const }))),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if ((source === "all" || source === "user") && sourceEnabled("user", policy)) {
|
|
161
|
+
scanJobs.push(
|
|
162
|
+
scanSkillsFromDir(
|
|
163
|
+
{ cwd: options.cwd, home, repoRoot: home },
|
|
164
|
+
{ dir: path.join(home, ".gjc", "skills"), providerId: "runtime", level: "user", requireDescription: true },
|
|
165
|
+
).then(result => result.items.map(skill => ({ skill, source: "user" as const }))),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const seenNames = new Set<string>();
|
|
170
|
+
const seenPaths = new Set<string>();
|
|
171
|
+
const candidates: RuntimeSkillDiscoveryCandidate[] = [];
|
|
172
|
+
for (const entry of (await Promise.all(scanJobs)).flat()) {
|
|
173
|
+
if (!isAllowedByPolicy(entry.skill, entry.source, policy)) continue;
|
|
174
|
+
const realPath = await realPathOrSelf(entry.skill.path);
|
|
175
|
+
if (seenPaths.has(realPath) || seenNames.has(entry.skill.name)) continue;
|
|
176
|
+
seenPaths.add(realPath);
|
|
177
|
+
seenNames.add(entry.skill.name);
|
|
178
|
+
const candidate: RuntimeSkillDiscoveryCandidate = {
|
|
179
|
+
name: entry.skill.name,
|
|
180
|
+
description:
|
|
181
|
+
typeof entry.skill.frontmatter?.description === "string" ? entry.skill.frontmatter.description : "",
|
|
182
|
+
source: entry.source,
|
|
183
|
+
path: entry.skill.path,
|
|
184
|
+
useWhen: getUseWhen(entry.skill),
|
|
185
|
+
};
|
|
186
|
+
if (matchesQuery(candidate, options.query ?? "")) candidates.push(candidate);
|
|
187
|
+
}
|
|
188
|
+
candidates.sort((a, b) => compareSkillOrder(a.name, a.path, b.name, b.path));
|
|
189
|
+
return candidates.slice(0, normalizeLimit(options.limit));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function findRuntimeSkillByName(
|
|
193
|
+
cwd: string,
|
|
194
|
+
name: string,
|
|
195
|
+
policy?: SkillsSettings,
|
|
196
|
+
home = getRuntimeHome(),
|
|
197
|
+
): Promise<Skill | undefined> {
|
|
198
|
+
const normalized = name.trim();
|
|
199
|
+
if (!normalized) return undefined;
|
|
200
|
+
const scanJobs: Array<Promise<{ skill: CapabilitySkill; source: RuntimeSkillDiscoverySource }[]>> = [];
|
|
201
|
+
const projectSkills = await getProjectSkillDirs(cwd, home);
|
|
202
|
+
const projectContext = { cwd, home, repoRoot: projectSkills.repoRoot };
|
|
203
|
+
if (sourceEnabled("project", policy)) {
|
|
204
|
+
scanJobs.push(
|
|
205
|
+
...projectSkills.dirs.map(dir =>
|
|
206
|
+
scanSkillsFromDir(projectContext, {
|
|
207
|
+
dir,
|
|
208
|
+
providerId: "runtime",
|
|
209
|
+
level: "project",
|
|
210
|
+
requireDescription: true,
|
|
211
|
+
}).then(result => result.items.map(skill => ({ skill, source: "project" as const }))),
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (sourceEnabled("user", policy)) {
|
|
216
|
+
scanJobs.push(
|
|
217
|
+
scanSkillsFromDir(
|
|
218
|
+
{ cwd, home, repoRoot: home },
|
|
219
|
+
{ dir: path.join(home, ".gjc", "skills"), providerId: "runtime", level: "user", requireDescription: true },
|
|
220
|
+
).then(result => result.items.map(skill => ({ skill, source: "user" as const }))),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
for (const entry of (await Promise.all(scanJobs)).flat()) {
|
|
224
|
+
if (entry.skill.name === normalized && isAllowedByPolicy(entry.skill, entry.source, policy)) {
|
|
225
|
+
return toRuntimeSkill(entry.skill, entry.source);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
@@ -158,11 +158,43 @@ export interface GjcTmuxProfileContext {
|
|
|
158
158
|
version?: string | null;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function tmuxExitMarkerPath(sessionStateFile: string): string {
|
|
162
|
+
return path.join(path.dirname(sessionStateFile), "tmux-exit.json");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildPosixTmuxExitMarkerPrefix(markerPath: string): string {
|
|
166
|
+
const markerDir = path.dirname(markerPath);
|
|
167
|
+
return [
|
|
168
|
+
`__gjc_tmux_exit_marker=${shellQuote(markerPath)}`,
|
|
169
|
+
"__gjc_tmux_write_exit_marker() { __gjc_tmux_status=$?",
|
|
170
|
+
"__gjc_tmux_ended_at=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)",
|
|
171
|
+
`mkdir -p ${shellQuote(markerDir)} 2>/dev/null || true`,
|
|
172
|
+
'printf \'{"schema_version":1,"source":"tmux_inner_shell","ended_at":"%s","exit_code":%s}\\n\' "$__gjc_tmux_ended_at" "$__gjc_tmux_status" > "$__gjc_tmux_exit_marker" 2>/dev/null || true',
|
|
173
|
+
"}",
|
|
174
|
+
"trap __gjc_tmux_write_exit_marker EXIT",
|
|
175
|
+
].join("; ");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildPowerShellTmuxExitMarkerFinally(markerPath: string): string {
|
|
179
|
+
const markerDir = path.win32.dirname(markerPath);
|
|
180
|
+
return [
|
|
181
|
+
"} finally {",
|
|
182
|
+
"\ttry {",
|
|
183
|
+
`\t\tNew-Item -ItemType Directory -Force -Path ${powershellQuote(markerDir)} -ErrorAction Stop | Out-Null`,
|
|
184
|
+
"\t\t$__gjcTmuxExitMarker = @{ schema_version = 1; source = 'tmux_inner_shell'; ended_at = (Get-Date).ToUniversalTime().ToString('o'); exit_code = $__gjcTmuxExitCode } | ConvertTo-Json -Compress",
|
|
185
|
+
`\t\tSet-Content -LiteralPath ${powershellQuote(markerPath)} -Value $__gjcTmuxExitMarker -Encoding UTF8 -ErrorAction Stop`,
|
|
186
|
+
"\t} catch {",
|
|
187
|
+
"\t}",
|
|
188
|
+
"}",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
interface CommandResolutionContext {
|
|
162
193
|
cwd: string;
|
|
163
194
|
argv: string[];
|
|
164
195
|
execPath: string;
|
|
165
196
|
extraEnv?: Record<string, string>;
|
|
197
|
+
tmuxExitMarkerPath?: string;
|
|
166
198
|
platform?: NodeJS.Platform;
|
|
167
199
|
}
|
|
168
200
|
|
|
@@ -286,7 +318,17 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
|
|
|
286
318
|
const innerArgs = stripRootTmuxFlag(rawArgs).map(powershellQuote).join(" ");
|
|
287
319
|
const invocation = `& ${resolvedCommand} ${innerArgs}`;
|
|
288
320
|
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
289
|
-
const script =
|
|
321
|
+
const script = context.tmuxExitMarkerPath
|
|
322
|
+
? [
|
|
323
|
+
...envLines,
|
|
324
|
+
"$__gjcTmuxExitCode = 1",
|
|
325
|
+
"try {",
|
|
326
|
+
`\t${invocation}`,
|
|
327
|
+
"\tif ($null -ne $LASTEXITCODE) { $__gjcTmuxExitCode = $LASTEXITCODE } else { $__gjcTmuxExitCode = 1 }",
|
|
328
|
+
buildPowerShellTmuxExitMarkerFinally(context.tmuxExitMarkerPath),
|
|
329
|
+
"exit $__gjcTmuxExitCode",
|
|
330
|
+
].join("\n")
|
|
331
|
+
: [...envLines, invocation, exitLine].join("\n");
|
|
290
332
|
// Encode the script as UTF-16LE base64 for pwsh -EncodedCommand. Do NOT
|
|
291
333
|
// prepend a UTF-16LE BOM (0xFF 0xFE): the BOM survives the decode and is
|
|
292
334
|
// inserted as a literal U+FEFF character in front of the first script
|
|
@@ -366,7 +408,9 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
|
|
|
366
408
|
if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
|
|
367
409
|
const command = resolveCurrentGjcCommand(context);
|
|
368
410
|
const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
|
|
369
|
-
|
|
411
|
+
const invocation = `env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
|
|
412
|
+
if (!context.tmuxExitMarkerPath) return `exec ${invocation}`;
|
|
413
|
+
return `${buildPosixTmuxExitMarkerPrefix(context.tmuxExitMarkerPath)}; ${invocation}; exit $?`;
|
|
370
414
|
}
|
|
371
415
|
|
|
372
416
|
function visibleWidth(value: string): number {
|
|
@@ -404,6 +448,9 @@ function truncateVisibleTail(value: string, maxWidth: number): string {
|
|
|
404
448
|
const GJC_TMUX_WINDOW_BRANCH_SEPARATOR = "-";
|
|
405
449
|
const GJC_TMUX_WINDOW_TITLE_PREFIX = "GJC-";
|
|
406
450
|
const GJC_TMUX_TERMINAL_TITLE_PREFIX = "GJC: ";
|
|
451
|
+
const GJC_TMUX_ROOT_TERMINAL_TITLE_OPTION = "@gjc-root-terminal-title";
|
|
452
|
+
const GJC_TMUX_ROOT_TERMINAL_TITLE_SESSION_OPTION = "@gjc-root-terminal-title-session";
|
|
453
|
+
const GJC_TMUX_DYNAMIC_SESSION_TITLE = "GJC: #{session_name}";
|
|
407
454
|
|
|
408
455
|
function sanitizeTmuxWindowTitleSegment(value: string): string {
|
|
409
456
|
return value.replace(/:+/g, "-");
|
|
@@ -445,17 +492,31 @@ function sanitizeGjcTmuxRootTerminalTitle(title: string): string {
|
|
|
445
492
|
return title.replace(TERMINAL_TITLE_CONTROL_CHARS, "").trim() || "GJC";
|
|
446
493
|
}
|
|
447
494
|
|
|
448
|
-
function
|
|
449
|
-
|
|
495
|
+
function buildGjcTmuxRootTerminalTitleFormat(sessionName: string): string {
|
|
496
|
+
if (!sessionName.startsWith(GJC_TMUX_SESSION_PREFIX)) return GJC_TMUX_DYNAMIC_SESSION_TITLE;
|
|
497
|
+
return `#{?#{==:#{${GJC_TMUX_ROOT_TERMINAL_TITLE_SESSION_OPTION}},#{session_name}},#{${GJC_TMUX_ROOT_TERMINAL_TITLE_OPTION}},${GJC_TMUX_DYNAMIC_SESSION_TITLE}}`;
|
|
450
498
|
}
|
|
451
499
|
|
|
452
|
-
function buildGjcTmuxRootTerminalTitleCommands(
|
|
453
|
-
|
|
500
|
+
function buildGjcTmuxRootTerminalTitleCommands(
|
|
501
|
+
target: string,
|
|
502
|
+
sessionName: string,
|
|
503
|
+
title: string,
|
|
504
|
+
): GjcTmuxProfileCommand[] {
|
|
505
|
+
const sanitized = sanitizeGjcTmuxRootTerminalTitle(title);
|
|
506
|
+
const format = buildGjcTmuxRootTerminalTitleFormat(sessionName);
|
|
454
507
|
return [
|
|
508
|
+
{
|
|
509
|
+
description: "remember tmux client terminal title fallback",
|
|
510
|
+
args: ["set-option", "-t", target, GJC_TMUX_ROOT_TERMINAL_TITLE_OPTION, sanitized],
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
description: "remember tmux client terminal title session",
|
|
514
|
+
args: ["set-option", "-t", target, GJC_TMUX_ROOT_TERMINAL_TITLE_SESSION_OPTION, sessionName],
|
|
515
|
+
},
|
|
455
516
|
{ description: "enable tmux client terminal title", args: ["set-option", "-t", target, "set-titles", "on"] },
|
|
456
517
|
{
|
|
457
|
-
description: "set tmux client terminal title",
|
|
458
|
-
args: ["set-option", "-t", target, "set-titles-string",
|
|
518
|
+
description: "set dynamic tmux client terminal title",
|
|
519
|
+
args: ["set-option", "-t", target, "set-titles-string", format],
|
|
459
520
|
},
|
|
460
521
|
];
|
|
461
522
|
}
|
|
@@ -468,10 +529,8 @@ function applyGjcTmuxRootTerminalTitleProfile(context: {
|
|
|
468
529
|
options: TmuxSpawnOptions;
|
|
469
530
|
}): void {
|
|
470
531
|
if (!context.title) return;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
context.title,
|
|
474
|
-
)) {
|
|
532
|
+
const optionTarget = buildGjcTmuxExactOptionTarget(context.target, { env: context.options.env });
|
|
533
|
+
for (const command of buildGjcTmuxRootTerminalTitleCommands(optionTarget, context.target, context.title)) {
|
|
475
534
|
context.spawnSync(context.tmuxCommand, command.args, context.options);
|
|
476
535
|
}
|
|
477
536
|
}
|
|
@@ -695,6 +754,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
695
754
|
// session, which would split/send workers into the wrong session.
|
|
696
755
|
[GJC_TMUX_ACTIVE_SESSION_ENV]: sessionName,
|
|
697
756
|
},
|
|
757
|
+
tmuxExitMarkerPath: tmuxExitMarkerPath(sessionStateFile),
|
|
698
758
|
platform,
|
|
699
759
|
},
|
|
700
760
|
context.rawArgs,
|
|
@@ -115,6 +115,20 @@ function outputMentionsPsmux(output: string): boolean {
|
|
|
115
115
|
return PSMUX_VERSION_MARKERS.some(marker => output.includes(marker));
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function normalizedCommandBaseName(command: string): string {
|
|
119
|
+
const normalized = command
|
|
120
|
+
.trim()
|
|
121
|
+
.replace(/^['"]|['"]$/g, "")
|
|
122
|
+
.replace(/\\/g, "/");
|
|
123
|
+
const basename = normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
124
|
+
return basename.endsWith(".exe") ? basename.slice(0, -4) : basename;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isNamedPsmuxCommand(command: string): boolean {
|
|
128
|
+
const basename = normalizedCommandBaseName(command);
|
|
129
|
+
return basename === "psmux" || basename === "pmux";
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
function resolveBinaryPath(candidate: string): string | null {
|
|
119
133
|
return activeBinaryResolver(candidate);
|
|
120
134
|
}
|
|
@@ -194,13 +208,14 @@ export function resolveGjcTmuxBinary(options: ResolveGjcTmuxBinaryOptions = {}):
|
|
|
194
208
|
const runner = options.runner ?? readSpawnRunner();
|
|
195
209
|
const explicit = env.GJC_TMUX_COMMAND?.trim() || env.GJC_TEAM_TMUX_COMMAND?.trim();
|
|
196
210
|
if (explicit) {
|
|
197
|
-
const isPsmux =
|
|
211
|
+
const isPsmux =
|
|
212
|
+
platform === "win32" && isNamedPsmuxCommand(explicit) ? true : detectPsmux(explicit, { env, runner });
|
|
198
213
|
return { command: explicit, isPsmux, viaExplicitOverride: true };
|
|
199
214
|
}
|
|
200
215
|
if (platform === "win32") {
|
|
201
216
|
for (const candidate of PSMUX_BINARY_NAMES) {
|
|
202
217
|
if (resolveBinaryPath(candidate)) {
|
|
203
|
-
const isPsmux = detectPsmux(candidate, { env, runner });
|
|
218
|
+
const isPsmux = isNamedPsmuxCommand(candidate) ? true : detectPsmux(candidate, { env, runner });
|
|
204
219
|
return { command: candidate, isPsmux, viaExplicitOverride: false };
|
|
205
220
|
}
|
|
206
221
|
}
|
|
@@ -242,9 +242,12 @@ function assertSafePathComponent(value: string, label: string): void {
|
|
|
242
242
|
throw new StateCommandError(2, `invalid path component for --${label}: ${value}`);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
+
function isKnownMode(mode: string): mode is CanonicalGjcWorkflowSkill {
|
|
246
|
+
return KNOWN_MODES.includes(mode);
|
|
247
|
+
}
|
|
245
248
|
|
|
246
249
|
function assertKnownMode(mode: string): asserts mode is CanonicalGjcWorkflowSkill {
|
|
247
|
-
if (!
|
|
250
|
+
if (!isKnownMode(mode)) {
|
|
248
251
|
throw new StateCommandError(2, `unknown --mode: ${mode}. Expected one of: ${KNOWN_MODES.join(", ")}.`);
|
|
249
252
|
}
|
|
250
253
|
}
|
|
@@ -1580,14 +1583,15 @@ async function handleHandoff(
|
|
|
1580
1583
|
if (!calleeRaw) {
|
|
1581
1584
|
throw new StateCommandError(2, "gjc state handoff requires --to <callee>");
|
|
1582
1585
|
}
|
|
1583
|
-
|
|
1584
|
-
const callee = calleeRaw
|
|
1586
|
+
assertSafePathComponent(calleeRaw, "to");
|
|
1587
|
+
const callee = calleeRaw;
|
|
1588
|
+
const calleeIsWorkflow = isKnownMode(callee);
|
|
1585
1589
|
if (callee === caller) {
|
|
1586
1590
|
throw new StateCommandError(2, `gjc state handoff: --to must differ from caller (both are "${caller}")`);
|
|
1587
1591
|
}
|
|
1588
1592
|
|
|
1589
1593
|
const callerPath = modeStateFile(cwd, caller, sessionId);
|
|
1590
|
-
const calleePath = modeStateFile(cwd, callee, sessionId);
|
|
1594
|
+
const calleePath = calleeIsWorkflow ? modeStateFile(cwd, callee, sessionId) : undefined;
|
|
1591
1595
|
const forced = hasFlag(args, "--force");
|
|
1592
1596
|
const callerRead = await readExistingStateForMutation(callerPath);
|
|
1593
1597
|
if (callerRead.kind === "corrupt" && !forced) {
|
|
@@ -1602,15 +1606,7 @@ async function handleHandoff(
|
|
|
1602
1606
|
`gjc state ${caller} handoff: caller is not active (no mode-state file at ${callerPath})`,
|
|
1603
1607
|
);
|
|
1604
1608
|
}
|
|
1605
|
-
const calleeRead = await readExistingStateForMutation(calleePath);
|
|
1606
|
-
if (calleeRead.kind === "corrupt" && !forced) {
|
|
1607
|
-
throw new StateCommandError(
|
|
1608
|
-
2,
|
|
1609
|
-
`existing state for ${callee} is corrupt or tampered (${calleeRead.error}); use --force to overwrite`,
|
|
1610
|
-
);
|
|
1611
|
-
}
|
|
1612
1609
|
const existingCaller = callerRead.kind === "valid" ? callerRead.value : {};
|
|
1613
|
-
const existingCallee = calleeRead.kind === "valid" ? calleeRead.value : {};
|
|
1614
1610
|
|
|
1615
1611
|
const handoffAt = nowIso();
|
|
1616
1612
|
const mutationId = `${caller}:handoff:${callee}:${handoffAt}`;
|
|
@@ -1623,6 +1619,103 @@ async function handleHandoff(
|
|
|
1623
1619
|
nowIso: handoffAt,
|
|
1624
1620
|
mutationId,
|
|
1625
1621
|
});
|
|
1622
|
+
|
|
1623
|
+
// Runtime callees have no native mode-state to clear later, so do not
|
|
1624
|
+
// persist them as active-state entries; the prompt observer tracks them
|
|
1625
|
+
// in memory the same way direct `/skill:<runtime>` invocation does.
|
|
1626
|
+
if (!calleeIsWorkflow) {
|
|
1627
|
+
const normalizedCaller =
|
|
1628
|
+
caller === "deep-interview"
|
|
1629
|
+
? (normalizeDeepInterviewEnvelope(migrateWorkflowState(existingCaller, caller).state) as Record<
|
|
1630
|
+
string,
|
|
1631
|
+
unknown
|
|
1632
|
+
>)
|
|
1633
|
+
: migrateWorkflowState(existingCaller, caller).state;
|
|
1634
|
+
const mergedCallerState: Record<string, unknown> = {
|
|
1635
|
+
...normalizedCaller,
|
|
1636
|
+
skill: caller,
|
|
1637
|
+
version: WORKFLOW_STATE_VERSION,
|
|
1638
|
+
active: false,
|
|
1639
|
+
current_phase: "handoff",
|
|
1640
|
+
handoff_to: callee,
|
|
1641
|
+
handoff_at: handoffAt,
|
|
1642
|
+
updated_at: handoffAt,
|
|
1643
|
+
receipt: callerReceipt,
|
|
1644
|
+
};
|
|
1645
|
+
const force = hasFlag(args, "--force");
|
|
1646
|
+
await beginWorkflowTransactionJournal({
|
|
1647
|
+
cwd,
|
|
1648
|
+
sessionId,
|
|
1649
|
+
mutationId,
|
|
1650
|
+
caller,
|
|
1651
|
+
paths: [callerPath, activeStateFile(cwd, sessionId)],
|
|
1652
|
+
});
|
|
1653
|
+
const callerWrite = await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
|
|
1654
|
+
sessionId,
|
|
1655
|
+
skill: caller,
|
|
1656
|
+
mutationId,
|
|
1657
|
+
force,
|
|
1658
|
+
fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
|
|
1659
|
+
toPhase: "handoff",
|
|
1660
|
+
});
|
|
1661
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["caller-mode-state"] });
|
|
1662
|
+
if (callerWrite.warning) process.stderr.write(`${callerWrite.warning}\n`);
|
|
1663
|
+
const stampedCallerReceipt = isPlainObject(callerWrite.stamped.receipt) ? callerWrite.stamped.receipt : {};
|
|
1664
|
+
await syncSkillActiveState({
|
|
1665
|
+
cwd,
|
|
1666
|
+
skill: caller,
|
|
1667
|
+
active: false,
|
|
1668
|
+
phase: "handoff",
|
|
1669
|
+
sessionId,
|
|
1670
|
+
threadId,
|
|
1671
|
+
turnId,
|
|
1672
|
+
source: "gjc-state-cli",
|
|
1673
|
+
hud: buildHudForMode(caller, mergedCallerState),
|
|
1674
|
+
handoff_to: callee,
|
|
1675
|
+
handoff_at: handoffAt,
|
|
1676
|
+
receipt: callerReceipt,
|
|
1677
|
+
});
|
|
1678
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
|
|
1679
|
+
steps: ["caller-mode-state", "active-state"],
|
|
1680
|
+
});
|
|
1681
|
+
await completeWorkflowTransactionJournal(cwd, sessionId, mutationId);
|
|
1682
|
+
await touchStateActivityMarker(cwd, sessionId, callerPath);
|
|
1683
|
+
return {
|
|
1684
|
+
status: 0,
|
|
1685
|
+
stdout: renderCliWriteReceipt({
|
|
1686
|
+
ok: true,
|
|
1687
|
+
from: caller,
|
|
1688
|
+
to: callee,
|
|
1689
|
+
handoff_at: handoffAt,
|
|
1690
|
+
phases: {
|
|
1691
|
+
from: mergedCallerState.current_phase,
|
|
1692
|
+
},
|
|
1693
|
+
receipts: {
|
|
1694
|
+
from: {
|
|
1695
|
+
mutation_id: stampedCallerReceipt.mutation_id,
|
|
1696
|
+
status: stampedCallerReceipt.status,
|
|
1697
|
+
content_sha256: stampedCallerReceipt.content_sha256,
|
|
1698
|
+
},
|
|
1699
|
+
},
|
|
1700
|
+
paths: {
|
|
1701
|
+
from: callerPath,
|
|
1702
|
+
active_state: activeStateFile(cwd, sessionId),
|
|
1703
|
+
},
|
|
1704
|
+
}),
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
if (!calleePath) {
|
|
1709
|
+
throw new StateCommandError(2, `gjc state handoff failed to resolve workflow callee path for ${callee}`);
|
|
1710
|
+
}
|
|
1711
|
+
const calleeRead = await readExistingStateForMutation(calleePath);
|
|
1712
|
+
if (calleeRead.kind === "corrupt" && !forced) {
|
|
1713
|
+
throw new StateCommandError(
|
|
1714
|
+
2,
|
|
1715
|
+
`existing state for ${callee} is corrupt or tampered (${calleeRead.error}); use --force to overwrite`,
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
const existingCallee = calleeRead.kind === "valid" ? calleeRead.value : {};
|
|
1626
1719
|
const calleeReceipt = buildWorkflowStateReceipt({
|
|
1627
1720
|
cwd,
|
|
1628
1721
|
skill: callee,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { getWorktreesDir } from "@gajae-code/utils/dirs";
|
|
4
5
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
6
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
7
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
GJC_TMUX_ACTIVE_SESSION_ENV,
|
|
26
27
|
GJC_TMUX_PROFILE_OPTION,
|
|
27
28
|
GJC_TMUX_PROFILE_VALUE,
|
|
29
|
+
resolveGjcTmuxBinary,
|
|
28
30
|
resolveGjcTmuxCommand,
|
|
29
31
|
} from "./tmux-common";
|
|
30
32
|
|
|
@@ -288,6 +290,7 @@ export interface GjcTeamStartOptions {
|
|
|
288
290
|
cwd?: string;
|
|
289
291
|
env?: NodeJS.ProcessEnv;
|
|
290
292
|
dryRun?: boolean;
|
|
293
|
+
platform?: NodeJS.Platform;
|
|
291
294
|
mailboxDeliveryTransport?: GjcTeamMailboxDeliveryTransport;
|
|
292
295
|
}
|
|
293
296
|
|
|
@@ -1844,18 +1847,41 @@ function findWorktreePath(repoRoot: string, worktreePath: string): string | null
|
|
|
1844
1847
|
if (line.startsWith("worktree ") && path.resolve(line.slice("worktree ".length)) === resolved) return resolved;
|
|
1845
1848
|
return null;
|
|
1846
1849
|
}
|
|
1850
|
+
export function resolveWorkerWorktreePath(input: {
|
|
1851
|
+
repoRoot: string;
|
|
1852
|
+
stateDir: string;
|
|
1853
|
+
teamName: string;
|
|
1854
|
+
workerId: string;
|
|
1855
|
+
platform: NodeJS.Platform;
|
|
1856
|
+
isPsmux: boolean;
|
|
1857
|
+
}): string {
|
|
1858
|
+
if (input.platform === "win32" && input.isPsmux) {
|
|
1859
|
+
const slug = stableHash([input.repoRoot, input.stateDir, input.teamName].join("\0")).slice(0, 12);
|
|
1860
|
+
return path.join(getWorktreesDir(), `team-${slug}-${input.workerId}`);
|
|
1861
|
+
}
|
|
1862
|
+
return path.join(input.stateDir, "worktrees", input.workerId);
|
|
1863
|
+
}
|
|
1847
1864
|
async function ensureWorkerWorktree(
|
|
1848
1865
|
cwd: string,
|
|
1849
1866
|
dir: string,
|
|
1850
1867
|
teamName: string,
|
|
1851
1868
|
worker: GjcTeamWorker,
|
|
1852
1869
|
mode: GjcTeamWorktreeMode,
|
|
1870
|
+
platform: NodeJS.Platform,
|
|
1871
|
+
isPsmux: boolean,
|
|
1853
1872
|
): Promise<GjcTeamWorker> {
|
|
1854
1873
|
if (!mode.enabled) return worker;
|
|
1855
1874
|
if (!isGitRepository(cwd)) throw new Error(`team_worktree_requires_git_repo:${cwd}`);
|
|
1856
1875
|
const repoRoot = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1857
1876
|
const baseRef = runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1858
|
-
const worktreePath =
|
|
1877
|
+
const worktreePath = resolveWorkerWorktreePath({
|
|
1878
|
+
repoRoot,
|
|
1879
|
+
stateDir: dir,
|
|
1880
|
+
teamName,
|
|
1881
|
+
workerId: worker.id,
|
|
1882
|
+
platform,
|
|
1883
|
+
isPsmux,
|
|
1884
|
+
});
|
|
1859
1885
|
const existing = findWorktreePath(repoRoot, worktreePath);
|
|
1860
1886
|
let created = false;
|
|
1861
1887
|
const branchName = mode.detached
|
|
@@ -2994,7 +3020,9 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2994
3020
|
const dir = teamDir(stateRoot, teamName);
|
|
2995
3021
|
const createdAt = now();
|
|
2996
3022
|
const worktreeMode = resolveDefaultWorktreeMode(options.worktreeMode);
|
|
2997
|
-
const
|
|
3023
|
+
const platform = options.platform ?? process.platform;
|
|
3024
|
+
const tmuxBinary = resolveGjcTmuxBinary({ env, platform });
|
|
3025
|
+
const tmuxCommand = tmuxBinary.command;
|
|
2998
3026
|
const tmuxContext = options.dryRun
|
|
2999
3027
|
? { sessionName: "dry-run", windowIndex: "0", leaderPaneId: "%dry-run-leader", target: "dry-run:0" }
|
|
3000
3028
|
: readCurrentTmuxLeaderContext(tmuxCommand, env);
|
|
@@ -3003,7 +3031,11 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
3003
3031
|
const workers: GjcTeamWorker[] = [];
|
|
3004
3032
|
try {
|
|
3005
3033
|
for (const worker of initialWorkers)
|
|
3006
|
-
workers.push(
|
|
3034
|
+
workers.push(
|
|
3035
|
+
options.dryRun
|
|
3036
|
+
? worker
|
|
3037
|
+
: await ensureWorkerWorktree(cwd, dir, teamName, worker, worktreeMode, platform, tmuxBinary.isPsmux),
|
|
3038
|
+
);
|
|
3007
3039
|
} catch (error) {
|
|
3008
3040
|
await rollbackCreatedWorktrees(workers);
|
|
3009
3041
|
throw error;
|