@gajae-code/coding-agent 0.12.4 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
package/src/task/render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { Component } from "@gajae-code/tui";
|
|
9
9
|
import { Text } from "@gajae-code/tui";
|
|
10
|
-
import { formatNumber } from "@gajae-code/utils";
|
|
10
|
+
import { formatNumber, sanitizeText } from "@gajae-code/utils";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
13
13
|
import {
|
|
@@ -916,6 +916,47 @@ function renderAgentResult(result: TaskResultReceipt, isLast: boolean, expanded:
|
|
|
916
916
|
if (result.roi?.lowRoi) {
|
|
917
917
|
lines.push(`${continuePrefix}${theme.fg("warning", "low ROI: produced no material contribution")}`);
|
|
918
918
|
}
|
|
919
|
+
if (result.reviewSource?.disposition === "stale_review_delivery") {
|
|
920
|
+
lines.push(
|
|
921
|
+
`${continuePrefix}${theme.fg(
|
|
922
|
+
"warning",
|
|
923
|
+
truncateToWidth(
|
|
924
|
+
replaceTabs(
|
|
925
|
+
sanitizeText(
|
|
926
|
+
`STALE REVIEW: source advanced after dispatch; excluded from review gates. Rerun: ${result.reviewSource.rerunCommand}`,
|
|
927
|
+
),
|
|
928
|
+
),
|
|
929
|
+
|
|
930
|
+
100,
|
|
931
|
+
),
|
|
932
|
+
)}`,
|
|
933
|
+
);
|
|
934
|
+
} else if (result.reviewSource?.disposition === "invalid_provenance") {
|
|
935
|
+
lines.push(
|
|
936
|
+
`${continuePrefix}${theme.fg(
|
|
937
|
+
"error",
|
|
938
|
+
truncateToWidth(
|
|
939
|
+
replaceTabs(
|
|
940
|
+
sanitizeText(
|
|
941
|
+
`INVALID REVIEW PROVENANCE: receipt cannot satisfy review gates. Rerun: ${result.reviewSource.rerunCommand}`,
|
|
942
|
+
),
|
|
943
|
+
),
|
|
944
|
+
100,
|
|
945
|
+
),
|
|
946
|
+
)}`,
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (result.persistence?.recoveryRef) {
|
|
951
|
+
const label = result.persistence.ownerWorktreeApplied ? "Recovery patch" : "Unapplied recovery patch";
|
|
952
|
+
const recoveryUri = truncateToWidth(replaceTabs(sanitizeText(result.persistence.recoveryRef.uri)), 80);
|
|
953
|
+
lines.push(
|
|
954
|
+
`${continuePrefix}${theme.fg(
|
|
955
|
+
result.persistence.ownerWorktreeApplied ? "dim" : "warning",
|
|
956
|
+
`${label}: ${recoveryUri} (${formatBytes(result.persistence.recoveryRef.sizeBytes)})`,
|
|
957
|
+
)}`,
|
|
958
|
+
);
|
|
959
|
+
}
|
|
919
960
|
|
|
920
961
|
if (result.outputRef) {
|
|
921
962
|
lines.push(
|
package/src/task/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
|
2
2
|
import type { Usage } from "@gajae-code/ai";
|
|
3
3
|
import { $env } from "@gajae-code/utils";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
|
+
import type { ReviewDeliveryDisposition, ReviewSourceLane } from "../gjc-runtime/ultragoal-review-source";
|
|
5
6
|
import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
|
|
6
7
|
import type { TaskResultReceipt } from "./receipt";
|
|
7
8
|
import type { SpawnRoiReconciliation } from "./roi-reconciliation";
|
|
@@ -114,8 +115,26 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
|
|
|
114
115
|
.describe(
|
|
115
116
|
"authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
|
|
116
117
|
),
|
|
118
|
+
reviewSource: reviewSourceSchema.optional(),
|
|
117
119
|
});
|
|
118
120
|
|
|
121
|
+
const reviewSourceSchema = z
|
|
122
|
+
.object({
|
|
123
|
+
schema: z.literal("gjc.review_source_dispatch.v1"),
|
|
124
|
+
cohortId: z.string().min(1),
|
|
125
|
+
generation: z.number().int().min(1),
|
|
126
|
+
lane: z.enum(["cleaner", "architect", "qa", "critic"]),
|
|
127
|
+
snapshotId: z.string().startsWith("sha256:"),
|
|
128
|
+
repositoryBindingDigest: z.string().startsWith("sha256:"),
|
|
129
|
+
stateRevision: z.number().int().min(0),
|
|
130
|
+
dispatchId: z.string().min(1),
|
|
131
|
+
rerunCommand: z.string().min(1),
|
|
132
|
+
taskId: z.string().min(1),
|
|
133
|
+
createdAt: z.string().min(1),
|
|
134
|
+
})
|
|
135
|
+
.strict()
|
|
136
|
+
.describe("leader-issued immutable review source identity; review lanes only");
|
|
137
|
+
|
|
119
138
|
/** Single task item for parallel execution (default shape with context enabled). */
|
|
120
139
|
export const taskItemSchema = createTaskItemSchema(true);
|
|
121
140
|
export type TaskItem = z.infer<typeof taskItemSchema>;
|
|
@@ -425,6 +444,20 @@ export function createSetupFailureSummary(error: unknown): SetupFailureSummary {
|
|
|
425
444
|
};
|
|
426
445
|
}
|
|
427
446
|
|
|
447
|
+
export interface TaskRecoveryArtifactRef {
|
|
448
|
+
uri: string;
|
|
449
|
+
sizeBytes: number;
|
|
450
|
+
sha256: string;
|
|
451
|
+
/** Recovery artifacts remain readable for the parent session lifetime. */
|
|
452
|
+
durability: "session";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export interface TaskPersistenceResult {
|
|
456
|
+
outcome: "applied" | "no_changes" | "recovery_available";
|
|
457
|
+
ownerWorktreeApplied: boolean;
|
|
458
|
+
recoveryRef?: TaskRecoveryArtifactRef;
|
|
459
|
+
}
|
|
460
|
+
|
|
428
461
|
/** Result from a single agent execution */
|
|
429
462
|
export interface SingleResult {
|
|
430
463
|
index: number;
|
|
@@ -470,6 +503,10 @@ export interface SingleResult {
|
|
|
470
503
|
nestedPatches?: NestedRepoPatch[];
|
|
471
504
|
/** Whether isolated execution produced a non-empty root or nested patch. */
|
|
472
505
|
producedChanges?: boolean;
|
|
506
|
+
/** Receipt-safe owner-worktree persistence result for isolated execution. */
|
|
507
|
+
persistence?: TaskPersistenceResult;
|
|
508
|
+
/** Identity-bound patch artifact captured before isolation cleanup. */
|
|
509
|
+
recoveryRef?: TaskRecoveryArtifactRef;
|
|
473
510
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
474
511
|
extractedToolData?: Record<string, unknown[]>;
|
|
475
512
|
/** Full wrapper-owned review evidence, kept separate from caller completion data. */
|
|
@@ -506,6 +543,21 @@ export interface SingleResult {
|
|
|
506
543
|
head?: string;
|
|
507
544
|
branch?: string;
|
|
508
545
|
};
|
|
546
|
+
/** Leader-issued immutable source identity for source-aware review lanes (#3469). */
|
|
547
|
+
reviewSource?: {
|
|
548
|
+
schema: "gjc.review_source_dispatch.v1";
|
|
549
|
+
cohortId: string;
|
|
550
|
+
generation: number;
|
|
551
|
+
lane: ReviewSourceLane;
|
|
552
|
+
snapshotId: string;
|
|
553
|
+
repositoryBindingDigest: string;
|
|
554
|
+
stateRevision: number;
|
|
555
|
+
dispatchId: string;
|
|
556
|
+
rerunCommand: string;
|
|
557
|
+
disposition: ReviewDeliveryDisposition;
|
|
558
|
+
currentSnapshotId?: string;
|
|
559
|
+
deliveryId?: string;
|
|
560
|
+
};
|
|
509
561
|
}
|
|
510
562
|
|
|
511
563
|
/** True only for complete, factual five-bucket cost accounting. */
|
package/src/task/worktree.ts
CHANGED
|
@@ -83,13 +83,19 @@ async function captureUntrackedPatch(repoRoot: string, untracked: readonly strin
|
|
|
83
83
|
if (untracked.length === 0) return "";
|
|
84
84
|
const nullPath = getGitNoIndexNullPath();
|
|
85
85
|
const untrackedDiffs = await Promise.all(
|
|
86
|
-
untracked.map(entry =>
|
|
87
|
-
|
|
86
|
+
untracked.map(async entry => {
|
|
87
|
+
const absolutePath = path.join(repoRoot, entry);
|
|
88
|
+
const stat = await fs.lstat(absolutePath);
|
|
89
|
+
if (stat.isFile()) {
|
|
90
|
+
const handle = await fs.open(absolutePath, "r");
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
return await git.diff(repoRoot, {
|
|
88
94
|
allowFailure: true,
|
|
89
95
|
binary: true,
|
|
90
96
|
noIndex: { left: nullPath, right: entry },
|
|
91
|
-
})
|
|
92
|
-
),
|
|
97
|
+
});
|
|
98
|
+
}),
|
|
93
99
|
);
|
|
94
100
|
return untrackedDiffs.filter(diff => diff.trim()).join("\n");
|
|
95
101
|
}
|
|
@@ -135,26 +141,40 @@ export async function captureBaseline(repoRoot: string): Promise<WorktreeBaselin
|
|
|
135
141
|
return { root, nested };
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
async function
|
|
144
|
+
async function captureRepoCurrentTree(repoDir: string): Promise<string> {
|
|
139
145
|
const currentHead = (await git.head.sha(repoDir)) ?? "";
|
|
140
146
|
const currentStaged = await git.diff(repoDir, { binary: true, cached: true });
|
|
141
147
|
const currentUnstaged = await git.diff(repoDir, { binary: true });
|
|
142
148
|
const currentUntracked = await git.ls.untracked(repoDir);
|
|
143
149
|
const currentUntrackedPatch = await captureUntrackedPatch(repoDir, currentUntracked);
|
|
150
|
+
return await writeSyntheticTree(repoDir, currentHead, [currentStaged, currentUnstaged, currentUntrackedPatch]);
|
|
151
|
+
}
|
|
144
152
|
|
|
153
|
+
async function captureRepoDeltaPatch(repoDir: string, rb: RepoBaseline): Promise<string> {
|
|
145
154
|
const baselineTree = await writeSyntheticTree(repoDir, rb.headCommit, [rb.staged, rb.unstaged, rb.untrackedPatch]);
|
|
146
|
-
const currentTree = await
|
|
147
|
-
currentStaged,
|
|
148
|
-
currentUnstaged,
|
|
149
|
-
currentUntrackedPatch,
|
|
150
|
-
]);
|
|
155
|
+
const currentTree = await captureRepoCurrentTree(repoDir);
|
|
151
156
|
|
|
152
157
|
return git.diff.tree(repoDir, baselineTree, currentTree, {
|
|
153
|
-
allowFailure: true,
|
|
154
158
|
binary: true,
|
|
155
159
|
});
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
/** Confirm that applying patches to the captured owner baseline exactly matches the current owner worktree. */
|
|
163
|
+
export async function verifyRootPatchesApplied(
|
|
164
|
+
repoRoot: string,
|
|
165
|
+
baseline: WorktreeBaseline,
|
|
166
|
+
patches: readonly string[],
|
|
167
|
+
): Promise<boolean> {
|
|
168
|
+
const baselineTree = await writeSyntheticTree(repoRoot, baseline.root.headCommit, [
|
|
169
|
+
baseline.root.staged,
|
|
170
|
+
baseline.root.unstaged,
|
|
171
|
+
baseline.root.untrackedPatch,
|
|
172
|
+
]);
|
|
173
|
+
const expectedTree = await writeSyntheticTree(repoRoot, baselineTree, patches);
|
|
174
|
+
const currentTree = await captureRepoCurrentTree(repoRoot);
|
|
175
|
+
return expectedTree === currentTree;
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
export interface NestedRepoPatch {
|
|
159
179
|
relativePath: string;
|
|
160
180
|
patch: string;
|
|
@@ -163,64 +183,123 @@ export interface NestedRepoPatch {
|
|
|
163
183
|
export interface DeltaPatchResult {
|
|
164
184
|
rootPatch: string;
|
|
165
185
|
nestedPatches: NestedRepoPatch[];
|
|
186
|
+
captureErrors?: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface RecoveryPatchBundle {
|
|
190
|
+
version: 1;
|
|
191
|
+
rootPatch: string;
|
|
192
|
+
nestedPatches: NestedRepoPatch[];
|
|
193
|
+
captureErrors?: string[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function captureErrorCode(error: unknown): string {
|
|
197
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
198
|
+
return typeof code === "string" && code.length > 0 ? code : "unknown_error";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function serializeRecoveryPatchBundle(delta: DeltaPatchResult): string {
|
|
202
|
+
if (delta.nestedPatches.length === 0 && !delta.captureErrors?.length) return delta.rootPatch;
|
|
203
|
+
return `${JSON.stringify({
|
|
204
|
+
version: 1,
|
|
205
|
+
rootPatch: delta.rootPatch,
|
|
206
|
+
nestedPatches: delta.nestedPatches,
|
|
207
|
+
...(delta.captureErrors?.length ? { captureErrors: delta.captureErrors } : {}),
|
|
208
|
+
} satisfies RecoveryPatchBundle)}\n`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function verifyNestedPatchesApplied(
|
|
212
|
+
repoRoot: string,
|
|
213
|
+
baseline: WorktreeBaseline,
|
|
214
|
+
patches: readonly NestedRepoPatch[],
|
|
215
|
+
): Promise<boolean> {
|
|
216
|
+
const grouped = new Map<string, string[]>();
|
|
217
|
+
for (const patch of patches) {
|
|
218
|
+
const current = grouped.get(patch.relativePath) ?? [];
|
|
219
|
+
current.push(patch.patch);
|
|
220
|
+
grouped.set(patch.relativePath, current);
|
|
221
|
+
}
|
|
222
|
+
for (const [relativePath, repoPatches] of grouped) {
|
|
223
|
+
const nestedBaseline = baseline.nested.find(entry => entry.relativePath === relativePath)?.baseline;
|
|
224
|
+
if (!nestedBaseline) return false;
|
|
225
|
+
const nestedDir = path.join(repoRoot, relativePath);
|
|
226
|
+
const baselineTree = await writeSyntheticTree(nestedDir, nestedBaseline.headCommit, [
|
|
227
|
+
nestedBaseline.staged,
|
|
228
|
+
nestedBaseline.unstaged,
|
|
229
|
+
nestedBaseline.untrackedPatch,
|
|
230
|
+
]);
|
|
231
|
+
const expectedTree = await writeSyntheticTree(nestedDir, baselineTree, repoPatches);
|
|
232
|
+
if ((await captureRepoCurrentTree(nestedDir)) !== expectedTree) return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
166
235
|
}
|
|
167
236
|
|
|
168
237
|
export async function captureDeltaPatch(isolationDir: string, baseline: WorktreeBaseline): Promise<DeltaPatchResult> {
|
|
169
238
|
const rootPatch = await captureRepoDeltaPatch(isolationDir, baseline.root);
|
|
170
239
|
const nestedPatches: NestedRepoPatch[] = [];
|
|
240
|
+
const captureErrors: string[] = [];
|
|
171
241
|
|
|
172
242
|
for (const { relativePath, baseline: nb } of baseline.nested) {
|
|
173
243
|
const nestedDir = path.join(isolationDir, relativePath);
|
|
174
244
|
try {
|
|
175
245
|
await fs.access(path.join(nestedDir, ".git"));
|
|
176
|
-
|
|
177
|
-
|
|
246
|
+
const patch = await captureRepoDeltaPatch(nestedDir, nb);
|
|
247
|
+
if (patch.trim()) nestedPatches.push({ relativePath, patch });
|
|
248
|
+
} catch (error) {
|
|
249
|
+
captureErrors.push(`Nested repository capture failed (${relativePath}): ${captureErrorCode(error)}`);
|
|
178
250
|
}
|
|
179
|
-
const patch = await captureRepoDeltaPatch(nestedDir, nb);
|
|
180
|
-
if (patch.trim()) nestedPatches.push({ relativePath, patch });
|
|
181
251
|
}
|
|
182
252
|
|
|
183
|
-
return { rootPatch, nestedPatches };
|
|
253
|
+
return { rootPatch, nestedPatches, ...(captureErrors.length ? { captureErrors } : {}) };
|
|
184
254
|
}
|
|
185
255
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
* @param commitMessage Optional async function to generate a commit message from the combined diff.
|
|
189
|
-
* If omitted or returns null, falls back to a generic message.
|
|
190
|
-
*/
|
|
191
|
-
export async function applyNestedPatches(
|
|
192
|
-
repoRoot: string,
|
|
193
|
-
patches: NestedRepoPatch[],
|
|
194
|
-
commitMessage?: (diff: string) => Promise<string | null>,
|
|
195
|
-
): Promise<void> {
|
|
196
|
-
// Group patches by target repo to apply all at once and commit
|
|
256
|
+
/** Preflight every nested delta, then roll back earlier repositories if a later apply fails. */
|
|
257
|
+
export async function applyNestedPatches(repoRoot: string, patches: NestedRepoPatch[]): Promise<void> {
|
|
197
258
|
const byRepo = new Map<string, NestedRepoPatch[]>();
|
|
198
|
-
for (const
|
|
199
|
-
if (!
|
|
200
|
-
const group = byRepo.get(
|
|
201
|
-
group.push(
|
|
202
|
-
byRepo.set(
|
|
259
|
+
for (const patch of patches) {
|
|
260
|
+
if (!patch.patch.trim()) continue;
|
|
261
|
+
const group = byRepo.get(patch.relativePath) ?? [];
|
|
262
|
+
group.push(patch);
|
|
263
|
+
byRepo.set(patch.relativePath, group);
|
|
203
264
|
}
|
|
204
265
|
|
|
205
|
-
|
|
266
|
+
const prepared: Array<{ relativePath: string; nestedDir: string; combinedPatch: string }> = [];
|
|
267
|
+
for (const [relativePath, repoPatches] of [...byRepo.entries()].sort(([left], [right]) =>
|
|
268
|
+
left.localeCompare(right),
|
|
269
|
+
)) {
|
|
206
270
|
const nestedDir = path.join(repoRoot, relativePath);
|
|
207
271
|
try {
|
|
208
272
|
await fs.access(path.join(nestedDir, ".git"));
|
|
209
273
|
} catch {
|
|
210
|
-
|
|
274
|
+
throw new Error(`Nested repository is unavailable: ${relativePath}`);
|
|
211
275
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await git.patch.applyText(nestedDir, patch);
|
|
276
|
+
const combinedPatch = git.patch.join(repoPatches.map(entry => entry.patch));
|
|
277
|
+
if (!(await git.patch.canApplyText(nestedDir, combinedPatch))) {
|
|
278
|
+
throw new Error(`Nested repository patch does not apply cleanly: ${relativePath}`);
|
|
216
279
|
}
|
|
280
|
+
prepared.push({ relativePath, nestedDir, combinedPatch });
|
|
281
|
+
}
|
|
217
282
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
await git.
|
|
222
|
-
|
|
283
|
+
const applied: typeof prepared = [];
|
|
284
|
+
try {
|
|
285
|
+
for (const entry of prepared) {
|
|
286
|
+
await git.patch.applyText(entry.nestedDir, entry.combinedPatch);
|
|
287
|
+
applied.push(entry);
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
const rollbackFailures: string[] = [];
|
|
291
|
+
for (const entry of applied.reverse()) {
|
|
292
|
+
try {
|
|
293
|
+
await git.patch.applyText(entry.nestedDir, entry.combinedPatch, { reverse: true });
|
|
294
|
+
} catch {
|
|
295
|
+
rollbackFailures.push(entry.relativePath);
|
|
296
|
+
}
|
|
223
297
|
}
|
|
298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
+
const rollback = rollbackFailures.length
|
|
300
|
+
? `; rollback failed for: ${rollbackFailures.join(", ")}`
|
|
301
|
+
: "; earlier nested patches were rolled back";
|
|
302
|
+
throw new Error(`Nested repository patch application failed: ${message}${rollback}`);
|
|
224
303
|
}
|
|
225
304
|
}
|
|
226
305
|
|