@gajae-code/coding-agent 0.1.3 → 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 +18 -0
- package/dist/types/commands/gjc-runtime-bridge.d.ts +24 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +23 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +35 -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/skill-state/active-state.d.ts +19 -0
- package/dist/types/skill-state/workflow-hud.d.ts +62 -0
- package/package.json +7 -7
- 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/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 +4 -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 +285 -34
- 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/modes/components/skill-hud/render.ts +35 -8
- package/src/prompts/system/system-prompt.md +5 -4
- package/src/session/agent-session.ts +6 -0
- package/src/skill-state/active-state.ts +104 -4
- package/src/skill-state/workflow-hud.ts +160 -0
|
@@ -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";
|
|
@@ -235,7 +238,7 @@ interface GjcTmuxLeaderContext {
|
|
|
235
238
|
leaderPaneId: string;
|
|
236
239
|
target: string;
|
|
237
240
|
}
|
|
238
|
-
interface GjcTeamEvent {
|
|
241
|
+
export interface GjcTeamEvent {
|
|
239
242
|
event_id: string;
|
|
240
243
|
ts: string;
|
|
241
244
|
type: string;
|
|
@@ -263,7 +266,12 @@ interface GitResult {
|
|
|
263
266
|
}
|
|
264
267
|
interface GjcTeamCommitHygieneEntry {
|
|
265
268
|
recorded_at: string;
|
|
266
|
-
operation:
|
|
269
|
+
operation:
|
|
270
|
+
| "auto_checkpoint"
|
|
271
|
+
| "leader_integration_attempt"
|
|
272
|
+
| "integration_merge"
|
|
273
|
+
| "integration_cherry_pick"
|
|
274
|
+
| "cross_rebase";
|
|
267
275
|
worker_name: string;
|
|
268
276
|
task_id?: string;
|
|
269
277
|
status: "applied" | "skipped" | "conflict" | "failed";
|
|
@@ -277,6 +285,23 @@ interface GjcTeamCommitHygieneEntry {
|
|
|
277
285
|
detail: string;
|
|
278
286
|
}
|
|
279
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
|
+
|
|
280
305
|
function isGjcTeamTaskStatus(value: string): value is GjcTeamTaskStatus {
|
|
281
306
|
return ["pending", "blocked", "in_progress", "completed", "failed"].includes(value);
|
|
282
307
|
}
|
|
@@ -364,6 +389,9 @@ function mailboxPath(dir: string, worker: string): string {
|
|
|
364
389
|
function workerDir(dir: string, worker: string): string {
|
|
365
390
|
return path.join(dir, "workers", worker);
|
|
366
391
|
}
|
|
392
|
+
function workerIntegrationDedupePath(dir: string, worker: string): string {
|
|
393
|
+
return path.join(workerDir(dir, worker), "posttooluse-dedupe.json");
|
|
394
|
+
}
|
|
367
395
|
|
|
368
396
|
export function resolveGjcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
|
|
369
397
|
const explicit = env.GJC_TEAM_STATE_ROOT?.trim();
|
|
@@ -679,7 +707,12 @@ function buildInitialTasks(task: string, workers: GjcTeamWorker[]): GjcTeamTask[
|
|
|
679
707
|
}));
|
|
680
708
|
}
|
|
681
709
|
|
|
682
|
-
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[]> {
|
|
683
716
|
if (dryRun) return config.workers.map(worker => ({ ...worker, pane_id: `%dry-run-${worker.id}` }));
|
|
684
717
|
const rollbackPaneIds: string[] = [];
|
|
685
718
|
try {
|
|
@@ -740,6 +773,23 @@ async function startTmuxSession(config: GjcTeamConfig, dir: string, dryRun: bool
|
|
|
740
773
|
stderr: "ignore",
|
|
741
774
|
});
|
|
742
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
|
+
});
|
|
743
793
|
await appendTelemetry(dir, {
|
|
744
794
|
type: "tmux_started",
|
|
745
795
|
message: "Started gjc team worker panes in current tmux window",
|
|
@@ -843,6 +893,72 @@ function listConflictFiles(cwd: string): string[] {
|
|
|
843
893
|
.map(line => line.trim())
|
|
844
894
|
.filter(Boolean);
|
|
845
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
|
+
}
|
|
846
962
|
async function appendIntegrationEvent(
|
|
847
963
|
dir: string,
|
|
848
964
|
type: string,
|
|
@@ -866,15 +982,39 @@ async function notifyLeader(
|
|
|
866
982
|
): Promise<void> {
|
|
867
983
|
await sendGjcTeamMessage(config.team_name, worker.id, "leader-fixed", body, cwd, env).catch(() => undefined);
|
|
868
984
|
}
|
|
869
|
-
function
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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 };
|
|
874
1014
|
const message = `gjc(team): auto-checkpoint ${worker.id} [${worker.assigned_tasks[0] ?? "unknown"}]`;
|
|
875
1015
|
if (!runGitResult(worker.worktree_path, ["commit", "--no-verify", "-m", message]).ok)
|
|
876
|
-
return {
|
|
877
|
-
return { committed: true, commit: resolveHead(worker.worktree_path) };
|
|
1016
|
+
return { ...empty, classification };
|
|
1017
|
+
return { committed: true, commit: resolveHead(worker.worktree_path), classification };
|
|
878
1018
|
}
|
|
879
1019
|
function workerMergeRef(worker: GjcTeamWorker, workerHead: string): string {
|
|
880
1020
|
if (!worker.worktree_path) return workerHead;
|
|
@@ -939,15 +1079,7 @@ async function integrateGjcWorkerCommits(
|
|
|
939
1079
|
}
|
|
940
1080
|
if (isAncestor(worker.worktree_path, leaderHead, workerHead)) {
|
|
941
1081
|
const mergeRef = workerMergeRef(worker, workerHead);
|
|
942
|
-
const merge = runGitResult(leaderCwd, [
|
|
943
|
-
"merge",
|
|
944
|
-
"--no-ff",
|
|
945
|
-
"-X",
|
|
946
|
-
"theirs",
|
|
947
|
-
"-m",
|
|
948
|
-
`gjc(team): merge ${worker.id}`,
|
|
949
|
-
mergeRef,
|
|
950
|
-
]);
|
|
1082
|
+
const merge = runGitResult(leaderCwd, ["merge", "--no-ff", "-m", `gjc(team): merge ${worker.id}`, mergeRef]);
|
|
951
1083
|
if (merge.ok) {
|
|
952
1084
|
const newLeaderHead = resolveHead(leaderCwd);
|
|
953
1085
|
if (newLeaderHead && newLeaderHead !== leaderHead && isAncestor(leaderCwd, workerHead, "HEAD")) {
|
|
@@ -1029,12 +1161,12 @@ async function integrateGjcWorkerCommits(
|
|
|
1029
1161
|
worker: worker.id,
|
|
1030
1162
|
operation: "merge",
|
|
1031
1163
|
files: conflictFiles,
|
|
1032
|
-
detail: `merge --no-ff
|
|
1164
|
+
detail: `merge --no-ff failed and was aborted: ${(merge.stderr || merge.stdout).slice(0, 200)}`,
|
|
1033
1165
|
});
|
|
1034
|
-
await
|
|
1166
|
+
await notifyIntegrationConflict(
|
|
1035
1167
|
config,
|
|
1036
1168
|
worker,
|
|
1037
|
-
`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.`,
|
|
1038
1170
|
cwd,
|
|
1039
1171
|
env,
|
|
1040
1172
|
);
|
|
@@ -1061,7 +1193,7 @@ async function integrateGjcWorkerCommits(
|
|
|
1061
1193
|
: leaderHead;
|
|
1062
1194
|
const commits = listCommitRange(worker.worktree_path, baseline, workerHead);
|
|
1063
1195
|
for (const commit of commits) {
|
|
1064
|
-
const pick = runGitResult(leaderCwd, ["cherry-pick", "--allow-empty",
|
|
1196
|
+
const pick = runGitResult(leaderCwd, ["cherry-pick", "--allow-empty", commit]);
|
|
1065
1197
|
if (!pick.ok) {
|
|
1066
1198
|
const conflictFiles = listConflictFiles(leaderCwd);
|
|
1067
1199
|
runGitResult(leaderCwd, ["cherry-pick", "--abort"]);
|
|
@@ -1082,12 +1214,12 @@ async function integrateGjcWorkerCommits(
|
|
|
1082
1214
|
worker: worker.id,
|
|
1083
1215
|
operation: "cherry-pick",
|
|
1084
1216
|
files: conflictFiles,
|
|
1085
|
-
detail: `cherry-pick
|
|
1217
|
+
detail: `cherry-pick failed and was aborted: ${(pick.stderr || pick.stdout).slice(0, 200)}`,
|
|
1086
1218
|
});
|
|
1087
|
-
await
|
|
1219
|
+
await notifyIntegrationConflict(
|
|
1088
1220
|
config,
|
|
1089
1221
|
worker,
|
|
1090
|
-
`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.`,
|
|
1091
1223
|
cwd,
|
|
1092
1224
|
env,
|
|
1093
1225
|
);
|
|
@@ -1199,7 +1331,7 @@ async function integrateGjcWorkerCommits(
|
|
|
1199
1331
|
continue;
|
|
1200
1332
|
}
|
|
1201
1333
|
const before = resolveHead(worker.worktree_path);
|
|
1202
|
-
const rebase = runGitResult(worker.worktree_path, ["rebase",
|
|
1334
|
+
const rebase = runGitResult(worker.worktree_path, ["rebase", newLeaderHead]);
|
|
1203
1335
|
if (rebase.ok) {
|
|
1204
1336
|
const after = resolveHead(worker.worktree_path);
|
|
1205
1337
|
integrationByWorker[worker.id] = {
|
|
@@ -1248,12 +1380,12 @@ async function integrateGjcWorkerCommits(
|
|
|
1248
1380
|
worker: worker.id,
|
|
1249
1381
|
operation: "rebase",
|
|
1250
1382
|
files: conflictFiles,
|
|
1251
|
-
detail: `rebase
|
|
1383
|
+
detail: `rebase failed and was aborted: ${(rebase.stderr || rebase.stdout).slice(0, 200)}`,
|
|
1252
1384
|
});
|
|
1253
|
-
await
|
|
1385
|
+
await notifyIntegrationConflict(
|
|
1254
1386
|
config,
|
|
1255
1387
|
worker,
|
|
1256
|
-
`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.`,
|
|
1257
1389
|
cwd,
|
|
1258
1390
|
env,
|
|
1259
1391
|
);
|
|
@@ -1379,7 +1511,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1379
1511
|
});
|
|
1380
1512
|
let tmuxWorkers: GjcTeamWorker[];
|
|
1381
1513
|
try {
|
|
1382
|
-
tmuxWorkers = await startTmuxSession(config, dir, options.dryRun ?? false);
|
|
1514
|
+
tmuxWorkers = await startTmuxSession(config, dir, options.dryRun ?? false, env);
|
|
1383
1515
|
} catch (error) {
|
|
1384
1516
|
await writePhase(dir, "failed");
|
|
1385
1517
|
await appendEvent(dir, {
|
|
@@ -1433,6 +1565,108 @@ export async function readGjcTeamSnapshot(
|
|
|
1433
1565
|
updated_at: config.updated_at,
|
|
1434
1566
|
};
|
|
1435
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
|
+
|
|
1436
1670
|
export async function monitorGjcTeam(
|
|
1437
1671
|
teamName: string,
|
|
1438
1672
|
cwd = process.cwd(),
|
|
@@ -1470,6 +1704,13 @@ export async function shutdownGjcTeam(
|
|
|
1470
1704
|
): Promise<GjcTeamSnapshot> {
|
|
1471
1705
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1472
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";
|
|
1473
1714
|
killWorkerPanes(config);
|
|
1474
1715
|
await removeCleanCreatedWorktrees(config.workers);
|
|
1475
1716
|
const stopped = {
|
|
@@ -1478,9 +1719,19 @@ export async function shutdownGjcTeam(
|
|
|
1478
1719
|
updated_at: now(),
|
|
1479
1720
|
};
|
|
1480
1721
|
await writeJsonFile(path.join(dir, "config.json"), stopped);
|
|
1481
|
-
await writePhase(dir,
|
|
1482
|
-
await appendEvent(dir, {
|
|
1483
|
-
|
|
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
|
+
});
|
|
1484
1735
|
return readGjcTeamSnapshot(config.team_name, cwd, env);
|
|
1485
1736
|
}
|
|
1486
1737
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
2
3
|
import {
|
|
3
4
|
computeUltragoalPlanGeneration,
|
|
5
|
+
getUltragoalPaths,
|
|
4
6
|
hashStructuredValue,
|
|
5
7
|
readUltragoalLedger,
|
|
6
8
|
readUltragoalPlan,
|
|
@@ -42,6 +44,31 @@ function objectiveMatches(currentObjective: string, plan: UltragoalPlan): boolea
|
|
|
42
44
|
return plan.goals.some(goal => goal.objective === normalized);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function isKnownUltragoalObjective(currentObjective: string): boolean {
|
|
48
|
+
const normalized = currentObjective.trim();
|
|
49
|
+
return (
|
|
50
|
+
normalized === DEFAULT_ULTRAGOAL_OBJECTIVE ||
|
|
51
|
+
(normalized.includes(".gjc/ultragoal/goals.json") && normalized.includes(".gjc/ultragoal/ledger.jsonl"))
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function hasDurableUltragoalState(cwd: string): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
await fs.stat(getUltragoalPaths(cwd).dir);
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (
|
|
61
|
+
typeof error === "object" &&
|
|
62
|
+
error !== null &&
|
|
63
|
+
"code" in error &&
|
|
64
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
65
|
+
) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
function requiredGoals(plan: UltragoalPlan): UltragoalGoal[] {
|
|
46
73
|
return plan.goals.filter(goal => goal.status !== "superseded");
|
|
47
74
|
}
|
|
@@ -141,6 +168,13 @@ export function validateCompletionReceipt(input: {
|
|
|
141
168
|
goalId: input.goal.id,
|
|
142
169
|
};
|
|
143
170
|
}
|
|
171
|
+
if (hashStructuredValue(event.gjcGoalJson) !== receipt.gjcGoalSnapshotHash) {
|
|
172
|
+
return {
|
|
173
|
+
state: "active_stale_receipt",
|
|
174
|
+
message: `Ultragoal ${input.goal.id} receipt get_goal snapshot hash does not match ledger.`,
|
|
175
|
+
goalId: input.goal.id,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
144
178
|
if (input.goal.updatedAt !== receipt.verifiedAt) {
|
|
145
179
|
return {
|
|
146
180
|
state: "active_stale_receipt",
|
|
@@ -195,7 +229,15 @@ export async function readUltragoalVerificationState(input: {
|
|
|
195
229
|
}
|
|
196
230
|
return { state: "unrelated_goal", message: "Current goal is not an active Ultragoal objective." };
|
|
197
231
|
}
|
|
198
|
-
if (!plan)
|
|
232
|
+
if (!plan) {
|
|
233
|
+
if (isKnownUltragoalObjective(currentObjective) || (await hasDurableUltragoalState(input.cwd))) {
|
|
234
|
+
return {
|
|
235
|
+
state: "unreadable_fail_closed",
|
|
236
|
+
message: "Active Ultragoal objective is missing durable .gjc/ultragoal/goals.json state.",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return { state: "inactive", message: "No Ultragoal plan exists." };
|
|
240
|
+
}
|
|
199
241
|
if (!objectiveMatches(currentObjective, plan))
|
|
200
242
|
return { state: "unrelated_goal", message: "Current goal is not an active Ultragoal objective." };
|
|
201
243
|
if (plan.goals.some(goal => goal.status === "review_blocked")) {
|