@gajae-code/coding-agent 0.9.0 → 0.9.2
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 +48 -0
- 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/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 +15 -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/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/thinking-selector.d.ts +7 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/notifications/config-commands.d.ts +38 -0
- package/dist/types/notifications/config.d.ts +16 -0
- package/dist/types/notifications/index.d.ts +14 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +22 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +10 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +6 -3
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/package.json +8 -11
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +38 -10
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/keybindings.ts +1 -1
- 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 +11 -2
- package/src/config/settings.ts +24 -1
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/gjc-runtime/launch-tmux.ts +78 -12
- package/src/gjc-runtime/psmux-detect.ts +17 -2
- package/src/gjc-runtime/team-runtime.ts +46 -7
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +26 -3
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +83 -20
- package/src/modes/controllers/event-controller.ts +11 -2
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +62 -26
- package/src/modes/controllers/selector-controller.ts +43 -0
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/rpc/rpc-mode.ts +3 -9
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +90 -0
- package/src/notifications/config.ts +27 -0
- package/src/notifications/index.ts +184 -14
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +143 -0
- package/src/notifications/telegram-daemon-cli.ts +60 -5
- package/src/notifications/telegram-daemon.ts +431 -91
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +42 -1
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +104 -13
- package/src/session/agent-session.ts +48 -22
- package/src/slash-commands/builtin-registry.ts +78 -1
- package/src/system-prompt.ts +4 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +15 -2
- package/src/utils/pasted-image-path.ts +170 -0
|
@@ -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
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildGjcTmuxSessionName,
|
|
14
14
|
buildGjcTmuxSessionSlug,
|
|
15
15
|
GJC_DEFAULT_TMUX_SESSION,
|
|
16
|
+
GJC_TMUX_ACTIVE_SESSION_ENV,
|
|
16
17
|
GJC_TMUX_COMMAND_ENV,
|
|
17
18
|
GJC_TMUX_MOUSE_ENV,
|
|
18
19
|
GJC_TMUX_PROFILE_ENV,
|
|
@@ -157,11 +158,43 @@ export interface GjcTmuxProfileContext {
|
|
|
157
158
|
version?: string | null;
|
|
158
159
|
}
|
|
159
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
|
+
|
|
160
192
|
interface CommandResolutionContext {
|
|
161
193
|
cwd: string;
|
|
162
194
|
argv: string[];
|
|
163
195
|
execPath: string;
|
|
164
196
|
extraEnv?: Record<string, string>;
|
|
197
|
+
tmuxExitMarkerPath?: string;
|
|
165
198
|
platform?: NodeJS.Platform;
|
|
166
199
|
}
|
|
167
200
|
|
|
@@ -285,7 +318,17 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
|
|
|
285
318
|
const innerArgs = stripRootTmuxFlag(rawArgs).map(powershellQuote).join(" ");
|
|
286
319
|
const invocation = `& ${resolvedCommand} ${innerArgs}`;
|
|
287
320
|
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
288
|
-
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");
|
|
289
332
|
// Encode the script as UTF-16LE base64 for pwsh -EncodedCommand. Do NOT
|
|
290
333
|
// prepend a UTF-16LE BOM (0xFF 0xFE): the BOM survives the decode and is
|
|
291
334
|
// inserted as a literal U+FEFF character in front of the first script
|
|
@@ -365,7 +408,9 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
|
|
|
365
408
|
if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
|
|
366
409
|
const command = resolveCurrentGjcCommand(context);
|
|
367
410
|
const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
|
|
368
|
-
|
|
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 $?`;
|
|
369
414
|
}
|
|
370
415
|
|
|
371
416
|
function visibleWidth(value: string): number {
|
|
@@ -403,6 +448,9 @@ function truncateVisibleTail(value: string, maxWidth: number): string {
|
|
|
403
448
|
const GJC_TMUX_WINDOW_BRANCH_SEPARATOR = "-";
|
|
404
449
|
const GJC_TMUX_WINDOW_TITLE_PREFIX = "GJC-";
|
|
405
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}";
|
|
406
454
|
|
|
407
455
|
function sanitizeTmuxWindowTitleSegment(value: string): string {
|
|
408
456
|
return value.replace(/:+/g, "-");
|
|
@@ -444,17 +492,31 @@ function sanitizeGjcTmuxRootTerminalTitle(title: string): string {
|
|
|
444
492
|
return title.replace(TERMINAL_TITLE_CONTROL_CHARS, "").trim() || "GJC";
|
|
445
493
|
}
|
|
446
494
|
|
|
447
|
-
function
|
|
448
|
-
|
|
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}}`;
|
|
449
498
|
}
|
|
450
499
|
|
|
451
|
-
function buildGjcTmuxRootTerminalTitleCommands(
|
|
452
|
-
|
|
500
|
+
function buildGjcTmuxRootTerminalTitleCommands(
|
|
501
|
+
target: string,
|
|
502
|
+
sessionName: string,
|
|
503
|
+
title: string,
|
|
504
|
+
): GjcTmuxProfileCommand[] {
|
|
505
|
+
const sanitized = sanitizeGjcTmuxRootTerminalTitle(title);
|
|
506
|
+
const format = buildGjcTmuxRootTerminalTitleFormat(sessionName);
|
|
453
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
|
+
},
|
|
454
516
|
{ description: "enable tmux client terminal title", args: ["set-option", "-t", target, "set-titles", "on"] },
|
|
455
517
|
{
|
|
456
|
-
description: "set tmux client terminal title",
|
|
457
|
-
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],
|
|
458
520
|
},
|
|
459
521
|
];
|
|
460
522
|
}
|
|
@@ -467,10 +529,8 @@ function applyGjcTmuxRootTerminalTitleProfile(context: {
|
|
|
467
529
|
options: TmuxSpawnOptions;
|
|
468
530
|
}): void {
|
|
469
531
|
if (!context.title) return;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
context.title,
|
|
473
|
-
)) {
|
|
532
|
+
const optionTarget = buildGjcTmuxExactOptionTarget(context.target, { env: context.options.env });
|
|
533
|
+
for (const command of buildGjcTmuxRootTerminalTitleCommands(optionTarget, context.target, context.title)) {
|
|
474
534
|
context.spawnSync(context.tmuxCommand, command.args, context.options);
|
|
475
535
|
}
|
|
476
536
|
}
|
|
@@ -688,7 +748,13 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
688
748
|
extraEnv: {
|
|
689
749
|
[GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
|
|
690
750
|
[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
751
|
+
// Carry the GJC-managed session name into the child so `gjc team`
|
|
752
|
+
// can target the correct leader session by name. Under psmux on
|
|
753
|
+
// Windows the inherited TMUX_PANE can resolve to the wrong/default
|
|
754
|
+
// session, which would split/send workers into the wrong session.
|
|
755
|
+
[GJC_TMUX_ACTIVE_SESSION_ENV]: sessionName,
|
|
691
756
|
},
|
|
757
|
+
tmuxExitMarkerPath: tmuxExitMarkerPath(sessionStateFile),
|
|
692
758
|
platform,
|
|
693
759
|
},
|
|
694
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
|
}
|
|
@@ -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";
|
|
@@ -22,8 +23,10 @@ import {
|
|
|
22
23
|
import {
|
|
23
24
|
buildGjcTmuxExactOptionTarget,
|
|
24
25
|
buildGjcTmuxUntaggedSessionHint,
|
|
26
|
+
GJC_TMUX_ACTIVE_SESSION_ENV,
|
|
25
27
|
GJC_TMUX_PROFILE_OPTION,
|
|
26
28
|
GJC_TMUX_PROFILE_VALUE,
|
|
29
|
+
resolveGjcTmuxBinary,
|
|
27
30
|
resolveGjcTmuxCommand,
|
|
28
31
|
} from "./tmux-common";
|
|
29
32
|
|
|
@@ -287,6 +290,7 @@ export interface GjcTeamStartOptions {
|
|
|
287
290
|
cwd?: string;
|
|
288
291
|
env?: NodeJS.ProcessEnv;
|
|
289
292
|
dryRun?: boolean;
|
|
293
|
+
platform?: NodeJS.Platform;
|
|
290
294
|
mailboxDeliveryTransport?: GjcTeamMailboxDeliveryTransport;
|
|
291
295
|
}
|
|
292
296
|
|
|
@@ -1843,18 +1847,41 @@ function findWorktreePath(repoRoot: string, worktreePath: string): string | null
|
|
|
1843
1847
|
if (line.startsWith("worktree ") && path.resolve(line.slice("worktree ".length)) === resolved) return resolved;
|
|
1844
1848
|
return null;
|
|
1845
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
|
+
}
|
|
1846
1864
|
async function ensureWorkerWorktree(
|
|
1847
1865
|
cwd: string,
|
|
1848
1866
|
dir: string,
|
|
1849
1867
|
teamName: string,
|
|
1850
1868
|
worker: GjcTeamWorker,
|
|
1851
1869
|
mode: GjcTeamWorktreeMode,
|
|
1870
|
+
platform: NodeJS.Platform,
|
|
1871
|
+
isPsmux: boolean,
|
|
1852
1872
|
): Promise<GjcTeamWorker> {
|
|
1853
1873
|
if (!mode.enabled) return worker;
|
|
1854
1874
|
if (!isGitRepository(cwd)) throw new Error(`team_worktree_requires_git_repo:${cwd}`);
|
|
1855
1875
|
const repoRoot = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1856
1876
|
const baseRef = runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1857
|
-
const worktreePath =
|
|
1877
|
+
const worktreePath = resolveWorkerWorktreePath({
|
|
1878
|
+
repoRoot,
|
|
1879
|
+
stateDir: dir,
|
|
1880
|
+
teamName,
|
|
1881
|
+
workerId: worker.id,
|
|
1882
|
+
platform,
|
|
1883
|
+
isPsmux,
|
|
1884
|
+
});
|
|
1858
1885
|
const existing = findWorktreePath(repoRoot, worktreePath);
|
|
1859
1886
|
let created = false;
|
|
1860
1887
|
const branchName = mode.detached
|
|
@@ -1922,16 +1949,22 @@ function tagTmuxSessionAsGjcLeader(tmuxCommand: string, sessionName: string): bo
|
|
|
1922
1949
|
function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): GjcTmuxLeaderContext {
|
|
1923
1950
|
if (Bun.which(tmuxCommand) === null)
|
|
1924
1951
|
throw new Error(buildTeamTmuxLeaderRequirementMessage(`tmux_not_installed:${tmuxCommand}`));
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1952
|
+
// Prefer the explicit GJC-managed session name propagated by `gjc --tmux`
|
|
1953
|
+
// (GJC_TMUX_ACTIVE_SESSION). Under psmux on Windows the inherited TMUX_PANE
|
|
1954
|
+
// can resolve to the wrong/default session, so querying the tagged session
|
|
1955
|
+
// by name is authoritative for GJC-launched leaders. Fall back to TMUX_PANE,
|
|
1956
|
+
// then to the ambient session, to keep native tmux/WSL flows unchanged.
|
|
1957
|
+
const activeSession = env[GJC_TMUX_ACTIVE_SESSION_ENV]?.trim();
|
|
1958
|
+
const displayTarget = activeSession ? buildGjcTmuxExactOptionTarget(activeSession, { env }) : env.TMUX_PANE?.trim();
|
|
1959
|
+
const args = displayTarget
|
|
1960
|
+
? ["display-message", "-p", "-t", displayTarget, "#S:#I #{pane_id}"]
|
|
1928
1961
|
: ["display-message", "-p", "#S:#I #{pane_id}"];
|
|
1929
1962
|
const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
1930
1963
|
if (result.exitCode !== 0) {
|
|
1931
1964
|
// Distinguish "you are not inside any tmux session" from a genuine tmux
|
|
1932
1965
|
// query failure so the caller gets actionable guidance instead of raw
|
|
1933
1966
|
// tmux stderr. `gjc team` needs a tmux leader; outside tmux there is none.
|
|
1934
|
-
const insideTmux = Boolean(env.TMUX?.trim() || env.TMUX_PANE?.trim());
|
|
1967
|
+
const insideTmux = Boolean(env.TMUX?.trim() || env.TMUX_PANE?.trim() || activeSession);
|
|
1935
1968
|
const stderr = result.stderr.toString().trim();
|
|
1936
1969
|
throw new Error(
|
|
1937
1970
|
buildTeamTmuxLeaderRequirementMessage(
|
|
@@ -2987,7 +3020,9 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2987
3020
|
const dir = teamDir(stateRoot, teamName);
|
|
2988
3021
|
const createdAt = now();
|
|
2989
3022
|
const worktreeMode = resolveDefaultWorktreeMode(options.worktreeMode);
|
|
2990
|
-
const
|
|
3023
|
+
const platform = options.platform ?? process.platform;
|
|
3024
|
+
const tmuxBinary = resolveGjcTmuxBinary({ env, platform });
|
|
3025
|
+
const tmuxCommand = tmuxBinary.command;
|
|
2991
3026
|
const tmuxContext = options.dryRun
|
|
2992
3027
|
? { sessionName: "dry-run", windowIndex: "0", leaderPaneId: "%dry-run-leader", target: "dry-run:0" }
|
|
2993
3028
|
: readCurrentTmuxLeaderContext(tmuxCommand, env);
|
|
@@ -2996,7 +3031,11 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
2996
3031
|
const workers: GjcTeamWorker[] = [];
|
|
2997
3032
|
try {
|
|
2998
3033
|
for (const worker of initialWorkers)
|
|
2999
|
-
workers.push(
|
|
3034
|
+
workers.push(
|
|
3035
|
+
options.dryRun
|
|
3036
|
+
? worker
|
|
3037
|
+
: await ensureWorkerWorktree(cwd, dir, teamName, worker, worktreeMode, platform, tmuxBinary.isPsmux),
|
|
3038
|
+
);
|
|
3000
3039
|
} catch (error) {
|
|
3001
3040
|
await rollbackCreatedWorktrees(workers);
|
|
3002
3041
|
throw error;
|
|
@@ -4,6 +4,7 @@ import { resolveGjcTmuxBinary } from "./psmux-detect";
|
|
|
4
4
|
export const GJC_DEFAULT_TMUX_SESSION = "gajae_code";
|
|
5
5
|
export const GJC_TMUX_SESSION_PREFIX = `${GJC_DEFAULT_TMUX_SESSION}_`;
|
|
6
6
|
export const GJC_TMUX_COMMAND_ENV = "GJC_TMUX_COMMAND";
|
|
7
|
+
export const GJC_TMUX_ACTIVE_SESSION_ENV = "GJC_TMUX_ACTIVE_SESSION";
|
|
7
8
|
export const GJC_TMUX_PROFILE_ENV = "GJC_TMUX_PROFILE";
|
|
8
9
|
export const GJC_TMUX_MOUSE_ENV = "GJC_MOUSE";
|
|
9
10
|
export const GJC_TMUX_PROFILE_OPTION = "@gjc-profile";
|
|
@@ -4544,6 +4544,15 @@ function parseGitNameStatus(output: string): UltragoalChangeSetPath[] {
|
|
|
4544
4544
|
return rows;
|
|
4545
4545
|
}
|
|
4546
4546
|
|
|
4547
|
+
function categorizeCiChangedPath(value: string): UltragoalChangeCategory {
|
|
4548
|
+
// CI_DEV_CHANGED_PATHS intentionally carries path names only. Mixed registries
|
|
4549
|
+
// such as settings-schema.ts require diff-level narrowing; without the diff,
|
|
4550
|
+
// treating the whole registry as computer-control source forces the mandatory
|
|
4551
|
+
// computer red-team suite on unrelated settings changes.
|
|
4552
|
+
if (isSettingsSchemaPath(value)) return "other";
|
|
4553
|
+
return categorizeComputerChangePath(value);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4547
4556
|
function ciDevChangedPathRows(): UltragoalChangeSetPath[] {
|
|
4548
4557
|
const raw = process.env.CI_DEV_CHANGED_PATHS;
|
|
4549
4558
|
if (!raw) return [];
|
|
@@ -4554,7 +4563,7 @@ function ciDevChangedPathRows(): UltragoalChangeSetPath[] {
|
|
|
4554
4563
|
.map(pathValue => ({
|
|
4555
4564
|
path: normalizeRepoPath(pathValue),
|
|
4556
4565
|
status: "unknown" as UltragoalChangeStatus,
|
|
4557
|
-
category:
|
|
4566
|
+
category: categorizeCiChangedPath(pathValue),
|
|
4558
4567
|
}));
|
|
4559
4568
|
}
|
|
4560
4569
|
|