@gajae-code/coding-agent 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -1
- package/dist/types/commands/gjc-runtime-bridge.d.ts +24 -0
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +23 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +40 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +15 -10
- package/dist/types/hooks/skill-state.d.ts +4 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -4
- package/dist/types/session/agent-session.d.ts +3 -9
- package/dist/types/skill-state/active-state.d.ts +19 -0
- package/dist/types/skill-state/workflow-hud.d.ts +62 -0
- package/package.json +9 -9
- package/src/commands/deep-interview.ts +21 -2
- package/src/commands/gjc-runtime-bridge.ts +161 -15
- package/src/commands/ralplan.ts +21 -2
- package/src/commands/team.ts +54 -3
- package/src/commands/ultragoal.ts +21 -1
- package/src/config/model-registry.ts +4 -0
- package/src/config/model-resolver.ts +5 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +6 -6
- package/src/defaults/gjc/skills/ralplan/SKILL.md +5 -9
- package/src/defaults/gjc/skills/team/SKILL.md +5 -4
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -8
- package/src/gjc-runtime/launch-tmux.ts +73 -2
- package/src/gjc-runtime/team-runtime.ts +365 -35
- package/src/gjc-runtime/ultragoal-guard.ts +43 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +307 -187
- package/src/hooks/skill-state.ts +4 -1
- package/src/main.ts +1 -0
- package/src/modes/components/model-selector.ts +108 -8
- package/src/modes/components/skill-hud/render.ts +35 -8
- package/src/modes/interactive-mode.ts +34 -22
- package/src/prompts/system/system-prompt.md +5 -4
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +15 -3
- package/src/skill-state/active-state.ts +104 -4
- package/src/skill-state/workflow-hud.ts +160 -0
- package/src/tools/image-gen.ts +19 -10
|
@@ -5,6 +5,8 @@ export const GJC_DEFAULT_TMUX_SESSION = "gajae_code";
|
|
|
5
5
|
export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
|
|
6
6
|
export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
|
|
7
7
|
export const GJC_TMUX_COMMAND_ENV = "GJC_TMUX_COMMAND";
|
|
8
|
+
export const GJC_TMUX_PROFILE_ENV = "GJC_TMUX_PROFILE";
|
|
9
|
+
export const GJC_TMUX_MOUSE_ENV = "GJC_MOUSE";
|
|
8
10
|
|
|
9
11
|
type LaunchPolicy = "direct" | "tmux";
|
|
10
12
|
|
|
@@ -51,6 +53,25 @@ export interface TmuxLaunchPlan {
|
|
|
51
53
|
attachSessionArgs: string[];
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
export interface GjcTmuxProfileCommand {
|
|
57
|
+
description: string;
|
|
58
|
+
args: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface GjcTmuxProfileResult {
|
|
62
|
+
skipped: boolean;
|
|
63
|
+
commands: GjcTmuxProfileCommand[];
|
|
64
|
+
failures: Array<{ command: GjcTmuxProfileCommand; stderr?: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface GjcTmuxProfileContext {
|
|
68
|
+
tmuxCommand: string;
|
|
69
|
+
target: string;
|
|
70
|
+
cwd?: string;
|
|
71
|
+
env?: NodeJS.ProcessEnv;
|
|
72
|
+
spawnSync?: TmuxSpawnSync;
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
interface CommandResolutionContext {
|
|
55
76
|
cwd: string;
|
|
56
77
|
argv: string[];
|
|
@@ -82,6 +103,47 @@ function shellQuote(value: string): string {
|
|
|
82
103
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
83
104
|
}
|
|
84
105
|
|
|
106
|
+
function envDisabled(value: string | undefined): boolean {
|
|
107
|
+
const normalized = value?.trim().toLowerCase();
|
|
108
|
+
return normalized === "0" || normalized === "false" || normalized === "off" || normalized === "no";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildGjcTmuxProfileCommands(
|
|
112
|
+
target: string,
|
|
113
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
114
|
+
): GjcTmuxProfileCommand[] {
|
|
115
|
+
if (envDisabled(env[GJC_TMUX_PROFILE_ENV])) return [];
|
|
116
|
+
const commands: GjcTmuxProfileCommand[] = [
|
|
117
|
+
{ description: "mark GJC tmux ownership", args: ["set-option", "-t", target, "@gjc-profile", "1"] },
|
|
118
|
+
{ description: "enable tmux clipboard integration", args: ["set-option", "-t", target, "set-clipboard", "on"] },
|
|
119
|
+
{
|
|
120
|
+
description: "make copy-mode selection readable",
|
|
121
|
+
args: ["set-window-option", "-t", target, "mode-style", "fg=colour231,bg=colour60"],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
if (!envDisabled(env[GJC_TMUX_MOUSE_ENV]))
|
|
125
|
+
commands.unshift({
|
|
126
|
+
description: "enable tmux mouse scrolling",
|
|
127
|
+
args: ["set-option", "-t", target, "mouse", "on"],
|
|
128
|
+
});
|
|
129
|
+
return commands;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
|
|
133
|
+
const env = context.env ?? process.env;
|
|
134
|
+
const commands = buildGjcTmuxProfileCommands(context.target, env);
|
|
135
|
+
if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
|
|
136
|
+
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
137
|
+
const cwd = context.cwd ?? process.cwd();
|
|
138
|
+
const options: TmuxSpawnOptions = { cwd, env, stdin: "inherit", stdout: "inherit", stderr: "inherit" };
|
|
139
|
+
const failures: GjcTmuxProfileResult["failures"] = [];
|
|
140
|
+
for (const command of commands) {
|
|
141
|
+
const result = spawnSync(context.tmuxCommand, command.args, options);
|
|
142
|
+
if (result.exitCode !== 0) failures.push({ command, stderr: result.stderr });
|
|
143
|
+
}
|
|
144
|
+
return { skipped: false, commands, failures };
|
|
145
|
+
}
|
|
146
|
+
|
|
85
147
|
function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
|
|
86
148
|
const entrypoint = context.argv[1];
|
|
87
149
|
if (!entrypoint) return ["gjc"];
|
|
@@ -126,7 +188,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
126
188
|
sessionName,
|
|
127
189
|
cwd,
|
|
128
190
|
innerCommand,
|
|
129
|
-
newSessionArgs: ["new-session", "-s", sessionName, "-c", cwd, innerCommand],
|
|
191
|
+
newSessionArgs: ["new-session", "-d", "-s", sessionName, "-c", cwd, innerCommand],
|
|
130
192
|
attachSessionArgs: ["attach-session", "-t", sessionName],
|
|
131
193
|
};
|
|
132
194
|
}
|
|
@@ -156,7 +218,16 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
156
218
|
stderr: "inherit",
|
|
157
219
|
};
|
|
158
220
|
const created = spawnSync(plan.tmuxCommand, plan.newSessionArgs, options);
|
|
159
|
-
if (created.exitCode === 0)
|
|
221
|
+
if (created.exitCode === 0) {
|
|
222
|
+
applyGjcTmuxProfile({
|
|
223
|
+
tmuxCommand: plan.tmuxCommand,
|
|
224
|
+
target: plan.sessionName,
|
|
225
|
+
cwd: plan.cwd,
|
|
226
|
+
env,
|
|
227
|
+
spawnSync,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
160
230
|
const attached = spawnSync(plan.tmuxCommand, plan.attachSessionArgs, options);
|
|
231
|
+
if (created.exitCode === 0) return attached.exitCode === 0;
|
|
161
232
|
return attached.exitCode === 0;
|
|
162
233
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
|
+
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
|
+
import { applyGjcTmuxProfile } from "./launch-tmux";
|
|
4
7
|
|
|
5
8
|
export type GjcTeamPhase = "starting" | "running" | "complete" | "failed" | "cancelled";
|
|
6
9
|
export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
|
|
@@ -8,6 +11,11 @@ export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "fa
|
|
|
8
11
|
|
|
9
12
|
export const GJC_TEAM_DEFAULT_WORKERS = 3;
|
|
10
13
|
export const GJC_TEAM_MAX_WORKERS = 20;
|
|
14
|
+
const GJC_TEAM_WORKER_CLI_ENV = "GJC_TEAM_WORKER_CLI";
|
|
15
|
+
const GJC_TEAM_WORKER_CLI_MAP_ENV = "GJC_TEAM_WORKER_CLI_MAP";
|
|
16
|
+
|
|
17
|
+
export type GjcTeamWorkerCli = "gjc";
|
|
18
|
+
type GjcTeamWorkerCliMode = "auto" | GjcTeamWorkerCli;
|
|
11
19
|
|
|
12
20
|
export interface GjcTeamLeader {
|
|
13
21
|
session_id: string;
|
|
@@ -75,6 +83,7 @@ export interface GjcTeamConfig {
|
|
|
75
83
|
max_workers: number;
|
|
76
84
|
state_root: string;
|
|
77
85
|
worker_command: string;
|
|
86
|
+
worker_cli_plan: GjcTeamWorkerCli[];
|
|
78
87
|
tmux_command: string;
|
|
79
88
|
tmux_session: string;
|
|
80
89
|
tmux_session_name: string;
|
|
@@ -159,13 +168,77 @@ export interface GjcTeamMailboxMessage {
|
|
|
159
168
|
interface FsError {
|
|
160
169
|
code?: string;
|
|
161
170
|
}
|
|
171
|
+
|
|
172
|
+
function normalizeGjcTeamWorkerCliMode(
|
|
173
|
+
raw: string | undefined,
|
|
174
|
+
sourceEnv = GJC_TEAM_WORKER_CLI_ENV,
|
|
175
|
+
): GjcTeamWorkerCliMode {
|
|
176
|
+
const normalized = String(raw ?? "auto")
|
|
177
|
+
.trim()
|
|
178
|
+
.toLowerCase();
|
|
179
|
+
if (normalized === "" || normalized === "auto") return "auto";
|
|
180
|
+
if (normalized === "gjc") return "gjc";
|
|
181
|
+
if (normalized === "codex" || normalized === "claude" || normalized === "gemini") {
|
|
182
|
+
throw new Error(`Unsupported ${sourceEnv} value "${raw}". GJC team launches GJC teammate sessions only.`);
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`Invalid ${sourceEnv} value "${raw}". Expected: auto or gjc`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function resolveGjcTeamWorkerCli(env: NodeJS.ProcessEnv = process.env): GjcTeamWorkerCli {
|
|
188
|
+
const mode = normalizeGjcTeamWorkerCliMode(env[GJC_TEAM_WORKER_CLI_ENV]);
|
|
189
|
+
return mode === "auto" ? "gjc" : mode;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function resolveGjcTeamWorkerCliPlan(
|
|
193
|
+
workerCount: number,
|
|
194
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
195
|
+
): GjcTeamWorkerCli[] {
|
|
196
|
+
if (!Number.isInteger(workerCount) || workerCount < 1) {
|
|
197
|
+
throw new Error(`workerCount must be >= 1 (got ${workerCount})`);
|
|
198
|
+
}
|
|
199
|
+
normalizeGjcTeamWorkerCliMode(env[GJC_TEAM_WORKER_CLI_ENV]);
|
|
200
|
+
const rawMap = String(env[GJC_TEAM_WORKER_CLI_MAP_ENV] ?? "").trim();
|
|
201
|
+
if (rawMap === "") {
|
|
202
|
+
const cli = resolveGjcTeamWorkerCli(env);
|
|
203
|
+
return Array.from({ length: workerCount }, () => cli);
|
|
204
|
+
}
|
|
205
|
+
const entries = rawMap.split(",").map(entry => entry.trim());
|
|
206
|
+
if (entries.length === 0 || entries.every(entry => entry.length === 0)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Invalid ${GJC_TEAM_WORKER_CLI_MAP_ENV} value "${env[GJC_TEAM_WORKER_CLI_MAP_ENV]}". Expected: auto or gjc`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (entries.some(entry => entry.length === 0)) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Invalid ${GJC_TEAM_WORKER_CLI_MAP_ENV} value "${env[GJC_TEAM_WORKER_CLI_MAP_ENV]}". Empty entries are not allowed.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (entries.length !== 1 && entries.length !== workerCount) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Invalid ${GJC_TEAM_WORKER_CLI_MAP_ENV} length ${entries.length}; expected 1 or ${workerCount} comma-separated values.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const expanded = entries.length === 1 ? Array.from({ length: workerCount }, () => entries[0] ?? "") : entries;
|
|
222
|
+
return expanded.map(entry => {
|
|
223
|
+
const mode = normalizeGjcTeamWorkerCliMode(entry, GJC_TEAM_WORKER_CLI_MAP_ENV);
|
|
224
|
+
return mode === "auto" ? "gjc" : mode;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function translateGjcWorkerLaunchArgsForCli(workerCli: GjcTeamWorkerCli, args: string[]): string[] {
|
|
229
|
+
if (workerCli !== "gjc") {
|
|
230
|
+
throw new Error(`Unsupported team worker CLI "${workerCli}". GJC team launches GJC teammate sessions only.`);
|
|
231
|
+
}
|
|
232
|
+
return [...args];
|
|
233
|
+
}
|
|
234
|
+
|
|
162
235
|
interface GjcTmuxLeaderContext {
|
|
163
236
|
sessionName: string;
|
|
164
237
|
windowIndex: string;
|
|
165
238
|
leaderPaneId: string;
|
|
166
239
|
target: string;
|
|
167
240
|
}
|
|
168
|
-
interface GjcTeamEvent {
|
|
241
|
+
export interface GjcTeamEvent {
|
|
169
242
|
event_id: string;
|
|
170
243
|
ts: string;
|
|
171
244
|
type: string;
|
|
@@ -193,7 +266,12 @@ interface GitResult {
|
|
|
193
266
|
}
|
|
194
267
|
interface GjcTeamCommitHygieneEntry {
|
|
195
268
|
recorded_at: string;
|
|
196
|
-
operation:
|
|
269
|
+
operation:
|
|
270
|
+
| "auto_checkpoint"
|
|
271
|
+
| "leader_integration_attempt"
|
|
272
|
+
| "integration_merge"
|
|
273
|
+
| "integration_cherry_pick"
|
|
274
|
+
| "cross_rebase";
|
|
197
275
|
worker_name: string;
|
|
198
276
|
task_id?: string;
|
|
199
277
|
status: "applied" | "skipped" | "conflict" | "failed";
|
|
@@ -207,6 +285,23 @@ interface GjcTeamCommitHygieneEntry {
|
|
|
207
285
|
detail: string;
|
|
208
286
|
}
|
|
209
287
|
|
|
288
|
+
interface GjcWorkerIntegrationDedupeState {
|
|
289
|
+
last_requested_fingerprint?: string;
|
|
290
|
+
last_requested_head?: string | null;
|
|
291
|
+
last_requested_status?: GjcWorkerCheckpointClassification["kind"];
|
|
292
|
+
last_requested_at?: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface GjcWorkerIntegrationAttemptRequestResult {
|
|
296
|
+
requested: boolean;
|
|
297
|
+
reason: "requested" | "not_worker" | "missing_worktree" | "no_changes" | "deduped" | "git_error";
|
|
298
|
+
worker?: string;
|
|
299
|
+
team_name?: string;
|
|
300
|
+
fingerprint?: string;
|
|
301
|
+
head?: string | null;
|
|
302
|
+
status?: GjcWorkerCheckpointClassification["kind"];
|
|
303
|
+
}
|
|
304
|
+
|
|
210
305
|
function isGjcTeamTaskStatus(value: string): value is GjcTeamTaskStatus {
|
|
211
306
|
return ["pending", "blocked", "in_progress", "completed", "failed"].includes(value);
|
|
212
307
|
}
|
|
@@ -294,6 +389,9 @@ function mailboxPath(dir: string, worker: string): string {
|
|
|
294
389
|
function workerDir(dir: string, worker: string): string {
|
|
295
390
|
return path.join(dir, "workers", worker);
|
|
296
391
|
}
|
|
392
|
+
function workerIntegrationDedupePath(dir: string, worker: string): string {
|
|
393
|
+
return path.join(workerDir(dir, worker), "posttooluse-dedupe.json");
|
|
394
|
+
}
|
|
297
395
|
|
|
298
396
|
export function resolveGjcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
|
|
299
397
|
const explicit = env.GJC_TEAM_STATE_ROOT?.trim();
|
|
@@ -341,6 +439,7 @@ async function readConfig(dir: string): Promise<GjcTeamConfig> {
|
|
|
341
439
|
tmux_target: config.tmux_target ?? config.tmux_session ?? tmuxSessionName,
|
|
342
440
|
leader_cwd: config.leader_cwd ?? config.leader.cwd,
|
|
343
441
|
team_state_root: config.team_state_root ?? config.state_root,
|
|
442
|
+
worker_cli_plan: config.worker_cli_plan ?? Array.from({ length: config.worker_count }, () => "gjc"),
|
|
344
443
|
};
|
|
345
444
|
}
|
|
346
445
|
async function readPhase(dir: string): Promise<GjcTeamPhase> {
|
|
@@ -608,7 +707,12 @@ function buildInitialTasks(task: string, workers: GjcTeamWorker[]): GjcTeamTask[
|
|
|
608
707
|
}));
|
|
609
708
|
}
|
|
610
709
|
|
|
611
|
-
async function startTmuxSession(
|
|
710
|
+
async function startTmuxSession(
|
|
711
|
+
config: GjcTeamConfig,
|
|
712
|
+
dir: string,
|
|
713
|
+
dryRun: boolean,
|
|
714
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
715
|
+
): Promise<GjcTeamWorker[]> {
|
|
612
716
|
if (dryRun) return config.workers.map(worker => ({ ...worker, pane_id: `%dry-run-${worker.id}` }));
|
|
613
717
|
const rollbackPaneIds: string[] = [];
|
|
614
718
|
try {
|
|
@@ -669,6 +773,23 @@ async function startTmuxSession(config: GjcTeamConfig, dir: string, dryRun: bool
|
|
|
669
773
|
stderr: "ignore",
|
|
670
774
|
});
|
|
671
775
|
}
|
|
776
|
+
const profileResult = applyGjcTmuxProfile({
|
|
777
|
+
tmuxCommand: config.tmux_command,
|
|
778
|
+
target: config.tmux_target,
|
|
779
|
+
cwd: config.leader.cwd,
|
|
780
|
+
env,
|
|
781
|
+
});
|
|
782
|
+
await appendTelemetry(dir, {
|
|
783
|
+
type: "tmux_profile_applied",
|
|
784
|
+
message: profileResult.skipped
|
|
785
|
+
? "Skipped GJC scoped tmux profile"
|
|
786
|
+
: "Applied GJC scoped tmux profile to team tmux target",
|
|
787
|
+
data: {
|
|
788
|
+
tmux_target: config.tmux_target,
|
|
789
|
+
command_count: profileResult.commands.length,
|
|
790
|
+
failure_count: profileResult.failures.length,
|
|
791
|
+
},
|
|
792
|
+
});
|
|
672
793
|
await appendTelemetry(dir, {
|
|
673
794
|
type: "tmux_started",
|
|
674
795
|
message: "Started gjc team worker panes in current tmux window",
|
|
@@ -772,6 +893,72 @@ function listConflictFiles(cwd: string): string[] {
|
|
|
772
893
|
.map(line => line.trim())
|
|
773
894
|
.filter(Boolean);
|
|
774
895
|
}
|
|
896
|
+
|
|
897
|
+
export type GjcWorkerCheckpointClassification =
|
|
898
|
+
| { kind: "clean"; files: string[] }
|
|
899
|
+
| { kind: "eligible"; files: string[] }
|
|
900
|
+
| { kind: "protected_only"; files: string[] }
|
|
901
|
+
| { kind: "conflicted"; files: string[] }
|
|
902
|
+
| { kind: "git_error"; files: string[]; detail: string };
|
|
903
|
+
|
|
904
|
+
const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
905
|
+
const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [
|
|
906
|
+
".gjc/state/",
|
|
907
|
+
".gjc/logs/",
|
|
908
|
+
".gjc/reports/",
|
|
909
|
+
".gjc/tmp/",
|
|
910
|
+
".gjc/ultragoal/",
|
|
911
|
+
];
|
|
912
|
+
|
|
913
|
+
function parsePorcelainStatusFiles(stdout: string): string[] {
|
|
914
|
+
return stdout
|
|
915
|
+
.split(/\r?\n/)
|
|
916
|
+
.map(line => line.trimEnd())
|
|
917
|
+
.filter(Boolean)
|
|
918
|
+
.map(line => line.slice(3).trim())
|
|
919
|
+
.filter(Boolean);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function normalizeGitStatusPath(filePath: string): string {
|
|
923
|
+
return (filePath.split(" -> ").at(-1) ?? filePath).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function classifyGjcTeamCheckpointFiles(files: string[]): { eligible: string[]; protected: string[] } {
|
|
927
|
+
const eligible: string[] = [];
|
|
928
|
+
const protectedFiles: string[] = [];
|
|
929
|
+
for (const file of files) {
|
|
930
|
+
const normalized = normalizeGitStatusPath(file);
|
|
931
|
+
if (
|
|
932
|
+
PROTECTED_WORKER_CHECKPOINT_PREFIXES.some(
|
|
933
|
+
prefix => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix),
|
|
934
|
+
)
|
|
935
|
+
)
|
|
936
|
+
protectedFiles.push(file);
|
|
937
|
+
else eligible.push(file);
|
|
938
|
+
}
|
|
939
|
+
return { eligible, protected: protectedFiles };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpointClassification {
|
|
943
|
+
const status = runGitResult(cwd, ["status", "--porcelain", "-uall"]);
|
|
944
|
+
if (!status.ok) {
|
|
945
|
+
return { kind: "git_error", files: [], detail: status.stderr || status.stdout || "git status failed" };
|
|
946
|
+
}
|
|
947
|
+
if (!status.stdout.trim()) return { kind: "clean", files: [] };
|
|
948
|
+
const files = parsePorcelainStatusFiles(status.stdout);
|
|
949
|
+
const hasUnmergedStatus = status.stdout
|
|
950
|
+
.split(/\r?\n/)
|
|
951
|
+
.filter(Boolean)
|
|
952
|
+
.some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
|
|
953
|
+
const conflictFiles = listConflictFiles(cwd);
|
|
954
|
+
if (hasUnmergedStatus || conflictFiles.length > 0) {
|
|
955
|
+
return { kind: "conflicted", files: conflictFiles.length > 0 ? conflictFiles : files };
|
|
956
|
+
}
|
|
957
|
+
const classified = classifyGjcTeamCheckpointFiles(files);
|
|
958
|
+
if (classified.eligible.length === 0 && classified.protected.length > 0)
|
|
959
|
+
return { kind: "protected_only", files: classified.protected };
|
|
960
|
+
return { kind: "eligible", files: classified.eligible };
|
|
961
|
+
}
|
|
775
962
|
async function appendIntegrationEvent(
|
|
776
963
|
dir: string,
|
|
777
964
|
type: string,
|
|
@@ -795,15 +982,39 @@ async function notifyLeader(
|
|
|
795
982
|
): Promise<void> {
|
|
796
983
|
await sendGjcTeamMessage(config.team_name, worker.id, "leader-fixed", body, cwd, env).catch(() => undefined);
|
|
797
984
|
}
|
|
798
|
-
function
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
985
|
+
async function notifyWorker(
|
|
986
|
+
config: GjcTeamConfig,
|
|
987
|
+
worker: GjcTeamWorker,
|
|
988
|
+
body: string,
|
|
989
|
+
cwd: string,
|
|
990
|
+
env: NodeJS.ProcessEnv,
|
|
991
|
+
): Promise<void> {
|
|
992
|
+
await sendGjcTeamMessage(config.team_name, "leader-fixed", worker.id, body, cwd, env).catch(() => undefined);
|
|
993
|
+
}
|
|
994
|
+
async function notifyIntegrationConflict(
|
|
995
|
+
config: GjcTeamConfig,
|
|
996
|
+
worker: GjcTeamWorker,
|
|
997
|
+
body: string,
|
|
998
|
+
cwd: string,
|
|
999
|
+
env: NodeJS.ProcessEnv,
|
|
1000
|
+
): Promise<void> {
|
|
1001
|
+
await Promise.all([notifyLeader(config, worker, body, cwd, env), notifyWorker(config, worker, body, cwd, env)]);
|
|
1002
|
+
}
|
|
1003
|
+
function autoCommitDirtyWorker(worker: GjcTeamWorker): {
|
|
1004
|
+
committed: boolean;
|
|
1005
|
+
commit: string | null;
|
|
1006
|
+
classification: GjcWorkerCheckpointClassification | null;
|
|
1007
|
+
} {
|
|
1008
|
+
const empty = { committed: false, commit: null, classification: null };
|
|
1009
|
+
if (!worker.worktree_path) return empty;
|
|
1010
|
+
const classification = classifyWorkerCheckpointStatus(worker.worktree_path);
|
|
1011
|
+
if (classification.kind !== "eligible") return { ...empty, classification };
|
|
1012
|
+
if (!runGitResult(worker.worktree_path, ["add", "--", ...classification.files]).ok)
|
|
1013
|
+
return { ...empty, classification };
|
|
803
1014
|
const message = `gjc(team): auto-checkpoint ${worker.id} [${worker.assigned_tasks[0] ?? "unknown"}]`;
|
|
804
1015
|
if (!runGitResult(worker.worktree_path, ["commit", "--no-verify", "-m", message]).ok)
|
|
805
|
-
return {
|
|
806
|
-
return { committed: true, commit: resolveHead(worker.worktree_path) };
|
|
1016
|
+
return { ...empty, classification };
|
|
1017
|
+
return { committed: true, commit: resolveHead(worker.worktree_path), classification };
|
|
807
1018
|
}
|
|
808
1019
|
function workerMergeRef(worker: GjcTeamWorker, workerHead: string): string {
|
|
809
1020
|
if (!worker.worktree_path) return workerHead;
|
|
@@ -868,15 +1079,7 @@ async function integrateGjcWorkerCommits(
|
|
|
868
1079
|
}
|
|
869
1080
|
if (isAncestor(worker.worktree_path, leaderHead, workerHead)) {
|
|
870
1081
|
const mergeRef = workerMergeRef(worker, workerHead);
|
|
871
|
-
const merge = runGitResult(leaderCwd, [
|
|
872
|
-
"merge",
|
|
873
|
-
"--no-ff",
|
|
874
|
-
"-X",
|
|
875
|
-
"theirs",
|
|
876
|
-
"-m",
|
|
877
|
-
`gjc(team): merge ${worker.id}`,
|
|
878
|
-
mergeRef,
|
|
879
|
-
]);
|
|
1082
|
+
const merge = runGitResult(leaderCwd, ["merge", "--no-ff", "-m", `gjc(team): merge ${worker.id}`, mergeRef]);
|
|
880
1083
|
if (merge.ok) {
|
|
881
1084
|
const newLeaderHead = resolveHead(leaderCwd);
|
|
882
1085
|
if (newLeaderHead && newLeaderHead !== leaderHead && isAncestor(leaderCwd, workerHead, "HEAD")) {
|
|
@@ -958,12 +1161,12 @@ async function integrateGjcWorkerCommits(
|
|
|
958
1161
|
worker: worker.id,
|
|
959
1162
|
operation: "merge",
|
|
960
1163
|
files: conflictFiles,
|
|
961
|
-
detail: `merge --no-ff
|
|
1164
|
+
detail: `merge --no-ff failed and was aborted: ${(merge.stderr || merge.stdout).slice(0, 200)}`,
|
|
962
1165
|
});
|
|
963
|
-
await
|
|
1166
|
+
await notifyIntegrationConflict(
|
|
964
1167
|
config,
|
|
965
1168
|
worker,
|
|
966
|
-
`CONFLICT: merge failed for ${worker.id}; files: ${conflictFiles.join(",") || "unknown"}.`,
|
|
1169
|
+
`CONFLICT: merge failed for ${worker.id}; files: ${conflictFiles.join(",") || "unknown"}. Manual resolution required; runtime aborted the merge and did not auto-resolve.`,
|
|
967
1170
|
cwd,
|
|
968
1171
|
env,
|
|
969
1172
|
);
|
|
@@ -990,7 +1193,7 @@ async function integrateGjcWorkerCommits(
|
|
|
990
1193
|
: leaderHead;
|
|
991
1194
|
const commits = listCommitRange(worker.worktree_path, baseline, workerHead);
|
|
992
1195
|
for (const commit of commits) {
|
|
993
|
-
const pick = runGitResult(leaderCwd, ["cherry-pick", "--allow-empty",
|
|
1196
|
+
const pick = runGitResult(leaderCwd, ["cherry-pick", "--allow-empty", commit]);
|
|
994
1197
|
if (!pick.ok) {
|
|
995
1198
|
const conflictFiles = listConflictFiles(leaderCwd);
|
|
996
1199
|
runGitResult(leaderCwd, ["cherry-pick", "--abort"]);
|
|
@@ -1011,12 +1214,12 @@ async function integrateGjcWorkerCommits(
|
|
|
1011
1214
|
worker: worker.id,
|
|
1012
1215
|
operation: "cherry-pick",
|
|
1013
1216
|
files: conflictFiles,
|
|
1014
|
-
detail: `cherry-pick
|
|
1217
|
+
detail: `cherry-pick failed and was aborted: ${(pick.stderr || pick.stdout).slice(0, 200)}`,
|
|
1015
1218
|
});
|
|
1016
|
-
await
|
|
1219
|
+
await notifyIntegrationConflict(
|
|
1017
1220
|
config,
|
|
1018
1221
|
worker,
|
|
1019
|
-
`CONFLICT: cherry-pick failed for ${worker.id}; files: ${conflictFiles.join(",") || "unknown"}.`,
|
|
1222
|
+
`CONFLICT: cherry-pick failed for ${worker.id}; files: ${conflictFiles.join(",") || "unknown"}. Manual resolution required; runtime aborted the cherry-pick and did not auto-resolve.`,
|
|
1020
1223
|
cwd,
|
|
1021
1224
|
env,
|
|
1022
1225
|
);
|
|
@@ -1128,7 +1331,7 @@ async function integrateGjcWorkerCommits(
|
|
|
1128
1331
|
continue;
|
|
1129
1332
|
}
|
|
1130
1333
|
const before = resolveHead(worker.worktree_path);
|
|
1131
|
-
const rebase = runGitResult(worker.worktree_path, ["rebase",
|
|
1334
|
+
const rebase = runGitResult(worker.worktree_path, ["rebase", newLeaderHead]);
|
|
1132
1335
|
if (rebase.ok) {
|
|
1133
1336
|
const after = resolveHead(worker.worktree_path);
|
|
1134
1337
|
integrationByWorker[worker.id] = {
|
|
@@ -1177,12 +1380,12 @@ async function integrateGjcWorkerCommits(
|
|
|
1177
1380
|
worker: worker.id,
|
|
1178
1381
|
operation: "rebase",
|
|
1179
1382
|
files: conflictFiles,
|
|
1180
|
-
detail: `rebase
|
|
1383
|
+
detail: `rebase failed and was aborted: ${(rebase.stderr || rebase.stdout).slice(0, 200)}`,
|
|
1181
1384
|
});
|
|
1182
|
-
await
|
|
1385
|
+
await notifyIntegrationConflict(
|
|
1183
1386
|
config,
|
|
1184
1387
|
worker,
|
|
1185
|
-
`CONFLICT: cross-rebase failed for ${worker.id}; files: ${conflictFiles.join(",") || "unknown"}.`,
|
|
1388
|
+
`CONFLICT: cross-rebase failed for ${worker.id}; files: ${conflictFiles.join(",") || "unknown"}. Manual resolution required; runtime aborted the rebase and did not auto-resolve.`,
|
|
1186
1389
|
cwd,
|
|
1187
1390
|
env,
|
|
1188
1391
|
);
|
|
@@ -1227,6 +1430,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1227
1430
|
const env = options.env ?? process.env;
|
|
1228
1431
|
if (!Number.isInteger(options.workerCount) || options.workerCount < 1 || options.workerCount > GJC_TEAM_MAX_WORKERS)
|
|
1229
1432
|
throw new Error(`invalid_team_worker_count:${options.workerCount}:expected_1_${GJC_TEAM_MAX_WORKERS}`);
|
|
1433
|
+
const workerCliPlan = resolveGjcTeamWorkerCliPlan(options.workerCount, env);
|
|
1230
1434
|
const stateRoot = resolveGjcTeamStateRoot(cwd, env);
|
|
1231
1435
|
const teamName = sanitizeName(options.teamName ?? makeTeamName(options.task, env));
|
|
1232
1436
|
const displayName = sanitizeName(options.teamName ?? options.task).slice(0, 30) || teamName;
|
|
@@ -1256,6 +1460,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1256
1460
|
max_workers: GJC_TEAM_MAX_WORKERS,
|
|
1257
1461
|
state_root: stateRoot,
|
|
1258
1462
|
worker_command: resolveGjcWorkerCommand(cwd, env),
|
|
1463
|
+
worker_cli_plan: workerCliPlan,
|
|
1259
1464
|
tmux_command: tmuxCommand,
|
|
1260
1465
|
tmux_session: tmuxContext.sessionName,
|
|
1261
1466
|
tmux_session_name: tmuxContext.sessionName,
|
|
@@ -1279,6 +1484,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1279
1484
|
tmux_session_name: config.tmux_session_name,
|
|
1280
1485
|
tmux_target: config.tmux_target,
|
|
1281
1486
|
worker_command: config.worker_command,
|
|
1487
|
+
worker_cli_plan: config.worker_cli_plan,
|
|
1282
1488
|
tmux_command: config.tmux_command,
|
|
1283
1489
|
leader: config.leader,
|
|
1284
1490
|
workers: config.workers,
|
|
@@ -1296,11 +1502,16 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1296
1502
|
await appendTelemetry(dir, {
|
|
1297
1503
|
type: "team_runtime",
|
|
1298
1504
|
message: "Native gjc team runtime initialized",
|
|
1299
|
-
data: {
|
|
1505
|
+
data: {
|
|
1506
|
+
state_root: stateRoot,
|
|
1507
|
+
worker_command: config.worker_command,
|
|
1508
|
+
worker_cli_plan: workerCliPlan,
|
|
1509
|
+
workspace_mode: config.workspace_mode,
|
|
1510
|
+
},
|
|
1300
1511
|
});
|
|
1301
1512
|
let tmuxWorkers: GjcTeamWorker[];
|
|
1302
1513
|
try {
|
|
1303
|
-
tmuxWorkers = await startTmuxSession(config, dir, options.dryRun ?? false);
|
|
1514
|
+
tmuxWorkers = await startTmuxSession(config, dir, options.dryRun ?? false, env);
|
|
1304
1515
|
} catch (error) {
|
|
1305
1516
|
await writePhase(dir, "failed");
|
|
1306
1517
|
await appendEvent(dir, {
|
|
@@ -1354,6 +1565,108 @@ export async function readGjcTeamSnapshot(
|
|
|
1354
1565
|
updated_at: config.updated_at,
|
|
1355
1566
|
};
|
|
1356
1567
|
}
|
|
1568
|
+
function workerIntegrationFingerprint(head: string | null, classification: GjcWorkerCheckpointClassification): string {
|
|
1569
|
+
return `${head ?? "no-head"}:${classification.kind}:${classification.files.join("\0")}`;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
export async function requestGjcWorkerIntegrationAttempt(
|
|
1573
|
+
cwd = process.cwd(),
|
|
1574
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1575
|
+
): Promise<GjcWorkerIntegrationAttemptRequestResult> {
|
|
1576
|
+
const teamName = env.GJC_TEAM_NAME?.trim();
|
|
1577
|
+
const worker = env.GJC_TEAM_WORKER_ID?.trim() || env.GJC_TEAM_INTERNAL_WORKER?.split("/").pop()?.trim();
|
|
1578
|
+
if (!teamName || !worker) return { requested: false, reason: "not_worker" };
|
|
1579
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
1580
|
+
const config = await readConfig(dir);
|
|
1581
|
+
const configuredWorker = config.workers.find(candidate => candidate.id === worker);
|
|
1582
|
+
const worktreePath = env.GJC_TEAM_WORKTREE_PATH?.trim() || configuredWorker?.worktree_path;
|
|
1583
|
+
if (!worktreePath || !(await pathExists(worktreePath)))
|
|
1584
|
+
return { requested: false, reason: "missing_worktree", worker, team_name: teamName };
|
|
1585
|
+
const classification = classifyWorkerCheckpointStatus(worktreePath);
|
|
1586
|
+
const head = resolveHead(worktreePath);
|
|
1587
|
+
if (classification.kind === "git_error") {
|
|
1588
|
+
return { requested: false, reason: "git_error", worker, team_name: teamName, head, status: classification.kind };
|
|
1589
|
+
}
|
|
1590
|
+
if (classification.kind === "protected_only") {
|
|
1591
|
+
return { requested: false, reason: "no_changes", worker, team_name: teamName, head, status: classification.kind };
|
|
1592
|
+
}
|
|
1593
|
+
if (classification.kind === "clean" && configuredWorker?.worktree_base_ref === head) {
|
|
1594
|
+
return { requested: false, reason: "no_changes", worker, team_name: teamName, head, status: classification.kind };
|
|
1595
|
+
}
|
|
1596
|
+
const fingerprint = workerIntegrationFingerprint(head, classification);
|
|
1597
|
+
const dedupePath = workerIntegrationDedupePath(dir, worker);
|
|
1598
|
+
const dedupe = (await readJsonFile<GjcWorkerIntegrationDedupeState>(dedupePath)) ?? {};
|
|
1599
|
+
if (dedupe.last_requested_fingerprint === fingerprint) {
|
|
1600
|
+
return {
|
|
1601
|
+
requested: false,
|
|
1602
|
+
reason: "deduped",
|
|
1603
|
+
worker,
|
|
1604
|
+
team_name: teamName,
|
|
1605
|
+
fingerprint,
|
|
1606
|
+
head,
|
|
1607
|
+
status: classification.kind,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
await writeJsonFile(dedupePath, {
|
|
1611
|
+
last_requested_fingerprint: fingerprint,
|
|
1612
|
+
last_requested_head: head,
|
|
1613
|
+
last_requested_status: classification.kind,
|
|
1614
|
+
last_requested_at: now(),
|
|
1615
|
+
} satisfies GjcWorkerIntegrationDedupeState);
|
|
1616
|
+
await appendEvent(dir, {
|
|
1617
|
+
type: "worker_integration_attempt_requested",
|
|
1618
|
+
worker,
|
|
1619
|
+
message: `Worker ${worker} requested leader integration attempt`,
|
|
1620
|
+
data: { worker_name: worker, worker_head: head, status: classification.kind, files: classification.files },
|
|
1621
|
+
});
|
|
1622
|
+
await sendGjcTeamMessage(
|
|
1623
|
+
teamName,
|
|
1624
|
+
worker,
|
|
1625
|
+
"leader-fixed",
|
|
1626
|
+
`INTEGRATION REQUESTED: ${worker} has ${classification.kind} git changes at ${head?.slice(0, 12) ?? "unknown-head"}.`,
|
|
1627
|
+
cwd,
|
|
1628
|
+
env,
|
|
1629
|
+
).catch(() => undefined);
|
|
1630
|
+
await appendCommitHygieneEntries(config, [
|
|
1631
|
+
{
|
|
1632
|
+
recorded_at: now(),
|
|
1633
|
+
operation: "leader_integration_attempt",
|
|
1634
|
+
worker_name: worker,
|
|
1635
|
+
task_id: configuredWorker?.assigned_tasks[0],
|
|
1636
|
+
status: "applied",
|
|
1637
|
+
source_commit: head ?? undefined,
|
|
1638
|
+
worker_head_after: head,
|
|
1639
|
+
worktree_path: worktreePath,
|
|
1640
|
+
detail: "Worker turn-end requested a leader integration attempt for semantic git changes.",
|
|
1641
|
+
},
|
|
1642
|
+
]);
|
|
1643
|
+
return {
|
|
1644
|
+
requested: true,
|
|
1645
|
+
reason: "requested",
|
|
1646
|
+
worker,
|
|
1647
|
+
team_name: teamName,
|
|
1648
|
+
fingerprint,
|
|
1649
|
+
head,
|
|
1650
|
+
status: classification.kind,
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
export async function buildTeamHudSummary(
|
|
1655
|
+
snapshot: GjcTeamSnapshot,
|
|
1656
|
+
latestEvent?: GjcTeamEvent,
|
|
1657
|
+
latestMessage?: GjcTeamMailboxMessage,
|
|
1658
|
+
): Promise<WorkflowHudSummary> {
|
|
1659
|
+
return buildWorkflowTeamHudSummary({
|
|
1660
|
+
phase: snapshot.phase,
|
|
1661
|
+
task_total: snapshot.task_total,
|
|
1662
|
+
task_counts: snapshot.task_counts,
|
|
1663
|
+
workers: snapshot.workers,
|
|
1664
|
+
updated_at: snapshot.updated_at,
|
|
1665
|
+
latestEvent,
|
|
1666
|
+
latestMessage,
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1357
1670
|
export async function monitorGjcTeam(
|
|
1358
1671
|
teamName: string,
|
|
1359
1672
|
cwd = process.cwd(),
|
|
@@ -1391,6 +1704,13 @@ export async function shutdownGjcTeam(
|
|
|
1391
1704
|
): Promise<GjcTeamSnapshot> {
|
|
1392
1705
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1393
1706
|
const config = await readConfig(dir);
|
|
1707
|
+
const tasks = await readTasks(dir);
|
|
1708
|
+
const shutdownPhase: GjcTeamPhase =
|
|
1709
|
+
tasks.length === 0 || tasks.every(task => task.status === "completed")
|
|
1710
|
+
? "complete"
|
|
1711
|
+
: tasks.some(task => task.status === "failed" || task.status === "blocked")
|
|
1712
|
+
? "failed"
|
|
1713
|
+
: "cancelled";
|
|
1394
1714
|
killWorkerPanes(config);
|
|
1395
1715
|
await removeCleanCreatedWorktrees(config.workers);
|
|
1396
1716
|
const stopped = {
|
|
@@ -1399,9 +1719,19 @@ export async function shutdownGjcTeam(
|
|
|
1399
1719
|
updated_at: now(),
|
|
1400
1720
|
};
|
|
1401
1721
|
await writeJsonFile(path.join(dir, "config.json"), stopped);
|
|
1402
|
-
await writePhase(dir,
|
|
1403
|
-
await appendEvent(dir, {
|
|
1404
|
-
|
|
1722
|
+
await writePhase(dir, shutdownPhase);
|
|
1723
|
+
await appendEvent(dir, {
|
|
1724
|
+
type: "team_shutdown",
|
|
1725
|
+
message:
|
|
1726
|
+
shutdownPhase === "complete"
|
|
1727
|
+
? "Shut down native gjc team runtime after completed tasks"
|
|
1728
|
+
: "Shut down native gjc team runtime with incomplete tasks",
|
|
1729
|
+
data: { phase: shutdownPhase },
|
|
1730
|
+
});
|
|
1731
|
+
await appendTelemetry(dir, {
|
|
1732
|
+
type: "team_shutdown",
|
|
1733
|
+
message: `Native gjc team runtime stopped with phase ${shutdownPhase}`,
|
|
1734
|
+
});
|
|
1405
1735
|
return readGjcTeamSnapshot(config.team_name, cwd, env);
|
|
1406
1736
|
}
|
|
1407
1737
|
|