@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/types/commands/gjc-runtime-bridge.d.ts +24 -0
  3. package/dist/types/config/model-registry.d.ts +1 -0
  4. package/dist/types/config/model-resolver.d.ts +4 -1
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +23 -0
  6. package/dist/types/gjc-runtime/team-runtime.d.ts +40 -1
  7. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +15 -10
  8. package/dist/types/hooks/skill-state.d.ts +4 -1
  9. package/dist/types/modes/components/model-selector.d.ts +2 -4
  10. package/dist/types/modes/interactive-mode.d.ts +1 -0
  11. package/dist/types/sdk.d.ts +2 -4
  12. package/dist/types/session/agent-session.d.ts +3 -9
  13. package/dist/types/skill-state/active-state.d.ts +19 -0
  14. package/dist/types/skill-state/workflow-hud.d.ts +62 -0
  15. package/package.json +9 -9
  16. package/src/commands/deep-interview.ts +21 -2
  17. package/src/commands/gjc-runtime-bridge.ts +161 -15
  18. package/src/commands/ralplan.ts +21 -2
  19. package/src/commands/team.ts +54 -3
  20. package/src/commands/ultragoal.ts +21 -1
  21. package/src/config/model-registry.ts +4 -0
  22. package/src/config/model-resolver.ts +5 -1
  23. package/src/defaults/gjc/skills/deep-interview/SKILL.md +6 -6
  24. package/src/defaults/gjc/skills/ralplan/SKILL.md +5 -9
  25. package/src/defaults/gjc/skills/team/SKILL.md +5 -4
  26. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -8
  27. package/src/gjc-runtime/launch-tmux.ts +73 -2
  28. package/src/gjc-runtime/team-runtime.ts +365 -35
  29. package/src/gjc-runtime/ultragoal-guard.ts +43 -1
  30. package/src/gjc-runtime/ultragoal-runtime.ts +307 -187
  31. package/src/hooks/skill-state.ts +4 -1
  32. package/src/main.ts +1 -0
  33. package/src/modes/components/model-selector.ts +108 -8
  34. package/src/modes/components/skill-hud/render.ts +35 -8
  35. package/src/modes/interactive-mode.ts +34 -22
  36. package/src/prompts/system/system-prompt.md +5 -4
  37. package/src/sdk.ts +3 -1
  38. package/src/session/agent-session.ts +15 -3
  39. package/src/skill-state/active-state.ts +104 -4
  40. package/src/skill-state/workflow-hud.ts +160 -0
  41. 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) return true;
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: "auto_checkpoint" | "integration_merge" | "integration_cherry_pick" | "cross_rebase";
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(config: GjcTeamConfig, dir: string, dryRun: boolean): Promise<GjcTeamWorker[]> {
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 autoCommitDirtyWorker(worker: GjcTeamWorker): { committed: boolean; commit: string | null } {
799
- if (!worker.worktree_path) return { committed: false, commit: null };
800
- const status = runGitResult(worker.worktree_path, ["status", "--porcelain"]);
801
- if (!status.ok || !status.stdout.trim()) return { committed: false, commit: null };
802
- if (!runGitResult(worker.worktree_path, ["add", "-A"]).ok) return { committed: false, commit: null };
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 { committed: false, commit: null };
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 -X theirs failed and was aborted: ${(merge.stderr || merge.stdout).slice(0, 200)}`,
1164
+ detail: `merge --no-ff failed and was aborted: ${(merge.stderr || merge.stdout).slice(0, 200)}`,
962
1165
  });
963
- await notifyLeader(
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", "-X", "theirs", commit]);
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 -X theirs failed and was aborted: ${(pick.stderr || pick.stdout).slice(0, 200)}`,
1217
+ detail: `cherry-pick failed and was aborted: ${(pick.stderr || pick.stdout).slice(0, 200)}`,
1015
1218
  });
1016
- await notifyLeader(
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", "-X", "ours", newLeaderHead]);
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 -X ours failed and was aborted: ${(rebase.stderr || rebase.stdout).slice(0, 200)}`,
1383
+ detail: `rebase failed and was aborted: ${(rebase.stderr || rebase.stdout).slice(0, 200)}`,
1181
1384
  });
1182
- await notifyLeader(
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: { state_root: stateRoot, worker_command: config.worker_command, workspace_mode: config.workspace_mode },
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, "complete");
1403
- await appendEvent(dir, { type: "team_shutdown", message: "Shut down native gjc team runtime" });
1404
- await appendTelemetry(dir, { type: "team_shutdown", message: "Native gjc team runtime stopped" });
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