dsh-git-worktree 0.1.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -0
  3. package/README.zh.md +37 -0
  4. package/lib/adapters/files.d.ts +10 -0
  5. package/lib/adapters/files.js +101 -0
  6. package/lib/adapters/files.js.map +1 -0
  7. package/lib/adapters/git.d.ts +18 -0
  8. package/lib/adapters/git.js +172 -0
  9. package/lib/adapters/git.js.map +1 -0
  10. package/lib/adapters/lookup.d.ts +28 -0
  11. package/lib/adapters/lookup.js +41 -0
  12. package/lib/adapters/lookup.js.map +1 -0
  13. package/lib/adapters/registry.d.ts +15 -0
  14. package/lib/adapters/registry.js +242 -0
  15. package/lib/adapters/registry.js.map +1 -0
  16. package/lib/adapters/safe-file.d.ts +19 -0
  17. package/lib/adapters/safe-file.js +94 -0
  18. package/lib/adapters/safe-file.js.map +1 -0
  19. package/lib/commands.d.ts +12 -0
  20. package/lib/commands.js +121 -0
  21. package/lib/commands.js.map +1 -0
  22. package/lib/index.d.ts +76 -0
  23. package/lib/index.js +93 -0
  24. package/lib/index.js.map +1 -0
  25. package/lib/managed-worktree-path.d.ts +27 -0
  26. package/lib/managed-worktree-path.js +53 -0
  27. package/lib/managed-worktree-path.js.map +1 -0
  28. package/lib/ports.d.ts +204 -0
  29. package/lib/ports.js +10 -0
  30. package/lib/ports.js.map +1 -0
  31. package/lib/session-checkout-apply.d.ts +110 -0
  32. package/lib/session-checkout-apply.js +753 -0
  33. package/lib/session-checkout-apply.js.map +1 -0
  34. package/lib/session-checkout-module.d.ts +3 -0
  35. package/lib/session-checkout-module.js +2122 -0
  36. package/lib/session-checkout-module.js.map +1 -0
  37. package/lib/tools.d.ts +12 -0
  38. package/lib/tools.js +384 -0
  39. package/lib/tools.js.map +1 -0
  40. package/lib/types.d.ts +275 -0
  41. package/lib/types.js +40 -0
  42. package/lib/types.js.map +1 -0
  43. package/package.json +60 -0
package/lib/index.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * dsh-git-worktree plugin entry. The session-checkout domain (state machine,
3
+ * apply engine, ports) is Domi-ported and host-agnostic; this file is the DSH
4
+ * plugin face: exports the domain error type and the module contract, builds
5
+ * the DSH adapters, mounts the state machine, and registers the model tools,
6
+ * `/worktree` command, and session context note.
7
+ * @module dsh-git-worktree
8
+ */
9
+ /** Stable domain failure raised by the session-checkout state machine and its adapters. */
10
+ export class SessionCheckoutError extends Error {
11
+ code;
12
+ /**
13
+ * @param code - stable error code from `SESSION_CHECKOUT_ERROR_CODES`.
14
+ * @param message - user-facing message, surfaced verbatim by tools.
15
+ */
16
+ constructor(code, message) {
17
+ super(message);
18
+ this.name = 'SessionCheckoutError';
19
+ this.code = code;
20
+ }
21
+ }
22
+ import { randomUUID } from 'node:crypto';
23
+ import { mkdirSync } from 'node:fs';
24
+ import { homedir } from 'node:os';
25
+ import { join, resolve } from 'node:path';
26
+ import z from '@deepseek-ai/schemastery';
27
+ import { createDshGitPort } from './adapters/git.js';
28
+ import { createNodeFilesPort } from './adapters/files.js';
29
+ import { AtomicJsonCheckoutRegistry } from './adapters/registry.js';
30
+ import { createDshLookupPort } from './adapters/lookup.js';
31
+ import { createSessionCheckoutApplyEngine } from './session-checkout-apply.js';
32
+ import { createSessionCheckoutModule } from './session-checkout-module.js';
33
+ import { registerTools } from './tools.js';
34
+ import { registerWorktreeCommand } from './commands.js';
35
+ const name = 'git-worktree';
36
+ const inject = ['tools', 'commands', 'subprocess'];
37
+ const Config = z.object({
38
+ /**
39
+ * Plugin state directory (registry, disabled-hooks root). Defaults to
40
+ * `$DSH_HOME/plugins/dsh-git-worktree` (`~/.dsh` when `DSH_HOME` is unset).
41
+ * The property is optional by construction (schemastery object properties
42
+ * are optional inputs).
43
+ */
44
+ stateDir: z.string(),
45
+ });
46
+ function resolveStateDir(config) {
47
+ if (config.stateDir)
48
+ return resolve(config.stateDir);
49
+ const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
50
+ return join(home, 'plugins', 'dsh-git-worktree');
51
+ }
52
+ const RETENTION_MAINTENANCE_INTERVAL_MS = 15 * 60 * 1000;
53
+ /**
54
+ * Mount the plugin: DSH adapters, the session-checkout module, the seven
55
+ * worktree tools, the `/worktree` command, the once-per-session worktree
56
+ * context note, startup reconciliation, and the retention-expiry timer.
57
+ */
58
+ export function apply(ctx, config = {}) {
59
+ const stateDir = resolveStateDir(config);
60
+ mkdirSync(stateDir, { recursive: true });
61
+ const hooksPath = join(stateDir, 'disabled-git-hooks');
62
+ mkdirSync(hooksPath, { recursive: true });
63
+ const module = createSessionCheckoutModule({
64
+ lookup: createDshLookupPort(ctx),
65
+ git: createDshGitPort(ctx, { hooksPath }),
66
+ files: createNodeFilesPort(),
67
+ registry: new AtomicJsonCheckoutRegistry(join(stateDir, 'managed-checkouts.json')),
68
+ applyEngine: createSessionCheckoutApplyEngine(),
69
+ managedCheckoutsRoot: stateDir,
70
+ createCheckoutId: randomUUID,
71
+ });
72
+ registerTools(ctx, module);
73
+ registerWorktreeCommand(ctx, module);
74
+ // Startup recovery plus periodic expiry of retained worktrees. cordis's
75
+ // typed Events map omits the runtime 'ready' event; the events service's
76
+ // loose runtime signature covers it.
77
+ const reconcileOnReady = ctx.events.on('ready', () => {
78
+ void module.reconcile().catch((error) => {
79
+ console.warn('[dsh-git-worktree] reconcile failed:', error);
80
+ });
81
+ });
82
+ const retentionTimer = setInterval(() => {
83
+ void module.cleanupExpiredRetained().catch((error) => {
84
+ console.warn('[dsh-git-worktree] retained worktree maintenance failed:', error);
85
+ });
86
+ }, RETENTION_MAINTENANCE_INTERVAL_MS);
87
+ retentionTimer.unref?.();
88
+ ctx.effect(() => () => {
89
+ reconcileOnReady();
90
+ clearInterval(retentionTimer);
91
+ });
92
+ }
93
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /** 将会话标题转换为跨平台安全、可读的单个目录名片段。 */
2
+ export declare function sanitizeManagedWorktreeLabel(rawTitle: string | undefined): string;
3
+ export declare function createManagedWorktreeDirectoryName(input: {
4
+ sessionId: string;
5
+ sessionTitle?: string;
6
+ checkoutId?: string;
7
+ iteration?: number;
8
+ }): string;
9
+ export interface ManagedWorktreePathCandidates {
10
+ siblingContainer: string;
11
+ siblingRoot: string;
12
+ fallbackRoot: string;
13
+ }
14
+ /**
15
+ * 只生成候选路径,不访问文件系统:
16
+ * - siblingRoot:Git 根目录同级的可读容器;
17
+ * - fallbackRoot:原 Domi 数据目录下、按 repository key 分组的安全回退。
18
+ */
19
+ export declare function createManagedWorktreePathCandidates(input: {
20
+ localGitRoot: string;
21
+ managedCheckoutsRoot: string;
22
+ repositoryKey: string;
23
+ sessionId: string;
24
+ sessionTitle?: string;
25
+ checkoutId?: string;
26
+ iteration?: number;
27
+ }): ManagedWorktreePathCandidates;
@@ -0,0 +1,53 @@
1
+ import { basename, dirname, join } from 'node:path';
2
+ const DEFAULT_SESSION_TITLES = new Set(['新 Agent 会话', '新会话']);
3
+ const WINDOWS_INVALID_CHARACTERS = /[<>:"/\\|?*\u0000-\u001f]/g;
4
+ const WINDOWS_RESERVED_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
5
+ const DISPLAY_SUFFIX = /\s*\((?:worktree|fork)\)\s*$/i;
6
+ const REPEATED_DASHES = /-{2,}/g;
7
+ const MAX_LABEL_LENGTH = 40;
8
+ function truncateCodePoints(value, maxLength) {
9
+ return Array.from(value).slice(0, maxLength).join('');
10
+ }
11
+ /** 将会话标题转换为跨平台安全、可读的单个目录名片段。 */
12
+ export function sanitizeManagedWorktreeLabel(rawTitle) {
13
+ const withoutDisplaySuffix = (rawTitle ?? '').replace(DISPLAY_SUFFIX, '').trim();
14
+ const source = DEFAULT_SESSION_TITLES.has(withoutDisplaySuffix) ? '' : withoutDisplaySuffix;
15
+ let cleaned = source
16
+ .normalize('NFC')
17
+ .replace(WINDOWS_INVALID_CHARACTERS, '-')
18
+ .replace(/\s+/g, '-')
19
+ .replace(REPEATED_DASHES, '-')
20
+ .replace(/^[.\s-]+|[.\s-]+$/g, '');
21
+ cleaned = truncateCodePoints(cleaned, MAX_LABEL_LENGTH)
22
+ .replace(/[.\s-]+$/g, '');
23
+ if (!cleaned)
24
+ return 'worktree';
25
+ return WINDOWS_RESERVED_NAME.test(cleaned) ? `_${cleaned}` : cleaned;
26
+ }
27
+ function shortIdentity(value, fallback) {
28
+ return value.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8) || fallback;
29
+ }
30
+ export function createManagedWorktreeDirectoryName(input) {
31
+ const sessionIdentity = shortIdentity(input.sessionId, 'session');
32
+ if (!input.checkoutId || !input.iteration) {
33
+ return `${sanitizeManagedWorktreeLabel(input.sessionTitle)}--${sessionIdentity}`;
34
+ }
35
+ const checkoutIdentity = shortIdentity(input.checkoutId, 'checkout');
36
+ return `${sanitizeManagedWorktreeLabel(input.sessionTitle)}--${sessionIdentity}--i${input.iteration}--${checkoutIdentity}`;
37
+ }
38
+ /**
39
+ * 只生成候选路径,不访问文件系统:
40
+ * - siblingRoot:Git 根目录同级的可读容器;
41
+ * - fallbackRoot:原 Domi 数据目录下、按 repository key 分组的安全回退。
42
+ */
43
+ export function createManagedWorktreePathCandidates(input) {
44
+ const repositoryLabel = sanitizeManagedWorktreeLabel(basename(input.localGitRoot));
45
+ const directoryName = createManagedWorktreeDirectoryName(input);
46
+ const siblingContainer = join(dirname(input.localGitRoot), `${repositoryLabel}-worktrees`);
47
+ return {
48
+ siblingContainer,
49
+ siblingRoot: join(siblingContainer, directoryName),
50
+ fallbackRoot: join(input.managedCheckoutsRoot, input.repositoryKey, directoryName),
51
+ };
52
+ }
53
+ //# sourceMappingURL=managed-worktree-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-worktree-path.js","sourceRoot":"","sources":["../src/managed-worktree-path.ts"],"names":[],"mappings":"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"}
package/lib/ports.d.ts ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Port interfaces for the session-checkout domain, ported from Domi's
3
+ * `session-checkout/ports.ts`. The domain logic depends only on these ports;
4
+ * the DSH adapters (subprocess-backed git, plugin-owned registry storage,
5
+ * session/workspace lookup) live behind them so the state machine and apply
6
+ * engine stay host-agnostic and testable with fakes.
7
+ * @module dsh-git-worktree/ports
8
+ */
9
+ import type { ApplyBaseStrategy, SessionCheckoutPhase, SessionTargetRef, WorktreeValidationItem, WorktreeRetentionMode, WorktreeValidationStatus } from './types.js';
10
+ import type { SessionCheckoutApplyEngine } from './session-checkout-apply.js';
11
+ export interface SessionCheckoutSessionRecord {
12
+ id: string;
13
+ projectId?: string;
14
+ /** Only used for readable managed worktree directory names; never identity. */
15
+ title?: string;
16
+ }
17
+ export interface SessionCheckoutProjectRecord {
18
+ id: string;
19
+ name: string;
20
+ root: string;
21
+ }
22
+ export interface SessionCheckoutLookupPort {
23
+ getSession(sessionId: string): SessionCheckoutSessionRecord | undefined;
24
+ getProject(projectId: string): SessionCheckoutProjectRecord | undefined;
25
+ }
26
+ export interface GitCheckoutSnapshot {
27
+ root: string;
28
+ commonDir: string;
29
+ gitDir: string;
30
+ branch: string | null;
31
+ headOid: string;
32
+ headRef: string;
33
+ }
34
+ export interface SessionCheckoutGitPort {
35
+ inspect(root: string): Promise<GitCheckoutSnapshot | null>;
36
+ /** Top-level of the Git checkout containing the directory; does not require a HEAD. */
37
+ findContainingWorktreeRoot(root: string): Promise<string | null>;
38
+ status(root: string): Promise<{
39
+ dirty: boolean;
40
+ }>;
41
+ createDetachedWorktree(localRoot: string, managedRoot: string, baseOid: string): Promise<void>;
42
+ /** Deletion after the host verified checkout identity and the full fingerprint including untracked. */
43
+ removeWorktree(localRoot: string, managedRoot: string): Promise<void>;
44
+ retainApplyBase(localRoot: string, checkoutId: string, oid: string): Promise<void>;
45
+ releaseApplyBase(localRoot: string, checkoutId: string): Promise<void>;
46
+ retainInternalArtifact(localRoot: string, checkoutId: string, artifactName: string, oid: string): Promise<void>;
47
+ releaseInternalArtifacts(localRoot: string, checkoutId: string, artifactPrefix?: string): Promise<void>;
48
+ /** Read-only ancestry proof; only accepts host-verified checkout roots and OIDs. */
49
+ isAncestor(root: string, ancestorOid: string, descendantOid: string): Promise<boolean>;
50
+ }
51
+ export interface DirectoryIdentity {
52
+ device: string;
53
+ inode: string;
54
+ birthtimeNs: string;
55
+ }
56
+ export interface SessionCheckoutFilesPort {
57
+ exists(path: string): boolean;
58
+ canonicalize(path: string): Promise<string>;
59
+ inspectDirectoryIdentity(path: string): Promise<DirectoryIdentity | null>;
60
+ ensureDirectory(path: string): void;
61
+ /** Deletes only when the whole tree contains no files or symlinks; used to collect empty `git worktree add` residue. */
62
+ removeEmptyDirectoryTree(path: string): boolean;
63
+ /** Atomically moves residue whose file identity matches into the plugin quarantine. */
64
+ quarantineDirectoryTree(path: string, expectedIdentity: DirectoryIdentity, quarantinePath: string): Promise<void>;
65
+ /** Deletes a fully owned non-empty directory tree; the root must not be a file or symlink. */
66
+ removeDirectoryTree(path: string): Promise<void>;
67
+ /** Estimates real file usage; does not follow symlinks. */
68
+ measureDirectoryBytes(path: string): Promise<number>;
69
+ }
70
+ export interface SessionBindingRecord {
71
+ sessionId: string;
72
+ projectId: string;
73
+ projectName: string;
74
+ target: SessionTargetRef;
75
+ ownerSessionId: string;
76
+ inheritedFromSessionId?: string;
77
+ sourceRef: string;
78
+ sourceOid: string;
79
+ revision: number;
80
+ }
81
+ interface ManagedCheckoutJournalBase {
82
+ operationId: string;
83
+ startedAt: number;
84
+ }
85
+ export interface ManagedCheckoutCreateJournal extends ManagedCheckoutJournalBase {
86
+ operation: 'create';
87
+ step: 'creating_worktree';
88
+ }
89
+ export interface ManagedCheckoutMutationJournal extends ManagedCheckoutJournalBase {
90
+ operation: 'apply' | 'finish' | 'cleanup';
91
+ step: 'planning' | 'artifacts_retained' | 'writing_local' | 'updating_ref' | 'replacing_index' | 'removing_worktree';
92
+ baseOid?: string;
93
+ planRevision?: string;
94
+ reviewId?: string;
95
+ localFingerprint?: string;
96
+ isolatedFingerprint?: string;
97
+ effectiveBaseOid?: string;
98
+ baseStrategy?: ApplyBaseStrategy;
99
+ localHeadOid?: string;
100
+ isolatedHeadOid?: string;
101
+ commitOid?: string;
102
+ retention?: WorktreeRetentionMode;
103
+ changedFiles?: string[];
104
+ managedDirectoryIdentity?: DirectoryIdentity;
105
+ cleanupQuarantinePath?: string;
106
+ }
107
+ export type ManagedCheckoutJournal = ManagedCheckoutCreateJournal | ManagedCheckoutMutationJournal;
108
+ export interface ManagedWorktreeReviewRecord {
109
+ reviewId: string;
110
+ iteration: number;
111
+ preparedAt: number;
112
+ detailsMarkdown?: string;
113
+ summary: string;
114
+ validationStatus: WorktreeValidationStatus;
115
+ validationSummary?: string;
116
+ tests: WorktreeValidationItem[];
117
+ changedFiles: string[];
118
+ suggestedCommitMessage: string;
119
+ isolatedFingerprint: string;
120
+ isolatedHeadOid: string;
121
+ }
122
+ export interface ManagedDeliveryProof {
123
+ localBranch: string | null;
124
+ localHeadBefore: string;
125
+ localHeadAfter: string;
126
+ changedFiles: string[];
127
+ }
128
+ export type ManagedCheckoutDelivery = {
129
+ state: 'working';
130
+ iteration: number;
131
+ } | {
132
+ state: 'ready_for_review';
133
+ review: ManagedWorktreeReviewRecord;
134
+ } | {
135
+ state: 'finalized';
136
+ review: ManagedWorktreeReviewRecord;
137
+ commitOid: string | null;
138
+ /** Newer delivery proof; historical registries may omit it. */
139
+ proof?: ManagedDeliveryProof;
140
+ /** The delivered isolated snapshot; cleanup retry must do fingerprint CAS. */
141
+ isolatedFingerprint: string;
142
+ finalizedAt: number;
143
+ cleanup: 'pending' | 'blocked';
144
+ cleanupMessage?: string;
145
+ } | {
146
+ state: 'retained';
147
+ review: ManagedWorktreeReviewRecord;
148
+ commitOid: string | null;
149
+ proof?: ManagedDeliveryProof;
150
+ isolatedFingerprint: string;
151
+ retention: Exclude<WorktreeRetentionMode, 'cleanup'>;
152
+ retainedAt: number;
153
+ expiresAt: number | null;
154
+ cleanup: 'scheduled' | 'blocked';
155
+ cleanupMessage?: string;
156
+ } | {
157
+ state: 'delivered';
158
+ iteration: number;
159
+ commitOid: string | null;
160
+ proof?: ManagedDeliveryProof;
161
+ deliveredAt: number;
162
+ };
163
+ export interface ManagedCheckoutRecord {
164
+ checkoutId: string;
165
+ projectId: string;
166
+ projectName: string;
167
+ ownerSessionId: string;
168
+ /** Canonical root of the project in the user's Local Checkout. */
169
+ localRoot: string;
170
+ /** Canonical root of the project in the managed worktree; also the lease cwd. */
171
+ managedRoot: string;
172
+ /** Repo top level of the managed Git worktree; the project root may be a subdirectory. */
173
+ managedGitRoot: string;
174
+ gitCommonDir: string;
175
+ gitDir: string;
176
+ baseOid: string;
177
+ /** Last successfully applied isolated snapshot; does not change the user-visible Session Base. */
178
+ applyBaseOid?: string;
179
+ sourceRef: string;
180
+ phase: SessionCheckoutPhase;
181
+ delivery: ManagedCheckoutDelivery;
182
+ journal: ManagedCheckoutJournal | null;
183
+ revision: number;
184
+ }
185
+ export interface ManagedCheckoutsRegistry {
186
+ version: 2;
187
+ revision: number;
188
+ sessionBindings: Record<string, SessionBindingRecord>;
189
+ managedCheckouts: Record<string, ManagedCheckoutRecord>;
190
+ }
191
+ export interface SessionCheckoutRegistryPort {
192
+ read(): ManagedCheckoutsRegistry;
193
+ write(registry: ManagedCheckoutsRegistry): void;
194
+ }
195
+ export interface SessionCheckoutDependencies {
196
+ lookup: SessionCheckoutLookupPort;
197
+ git: SessionCheckoutGitPort;
198
+ files: SessionCheckoutFilesPort;
199
+ registry: SessionCheckoutRegistryPort;
200
+ applyEngine: SessionCheckoutApplyEngine;
201
+ managedCheckoutsRoot: string;
202
+ createCheckoutId(): string;
203
+ }
204
+ export {};
package/lib/ports.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Port interfaces for the session-checkout domain, ported from Domi's
3
+ * `session-checkout/ports.ts`. The domain logic depends only on these ports;
4
+ * the DSH adapters (subprocess-backed git, plugin-owned registry storage,
5
+ * session/workspace lookup) live behind them so the state machine and apply
6
+ * engine stay host-agnostic and testable with fakes.
7
+ * @module dsh-git-worktree/ports
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,110 @@
1
+ import type { ApplyBaseStrategy } from './types.js';
2
+ export interface ApplyPlanInput {
3
+ baseOid: string;
4
+ isolatedPath: string;
5
+ localPath: string;
6
+ }
7
+ export interface ApplyPlan {
8
+ revision: string;
9
+ localFingerprint: string;
10
+ isolatedFingerprint: string;
11
+ effectiveBaseOid: string;
12
+ baseStrategy: ApplyBaseStrategy;
13
+ localHeadOid: string;
14
+ /** null 表示 Local 当前为 detached HEAD;Apply 允许,Finish 会拒绝自动提交。 */
15
+ localHeadRef: string | null;
16
+ isolatedHeadOid: string;
17
+ changedFiles: string[];
18
+ }
19
+ export interface ReviewSnapshotResult {
20
+ status: 'ready';
21
+ isolatedFingerprint: string;
22
+ isolatedHeadOid: string;
23
+ changedFiles: string[];
24
+ }
25
+ export type InspectReviewResult = ReviewSnapshotResult | ApplyPlanErrorResult;
26
+ export interface ApplyReadyPlanResult {
27
+ status: 'ready';
28
+ plan: ApplyPlan;
29
+ }
30
+ export interface ApplyConflictPlanResult {
31
+ status: 'conflict';
32
+ revision: string;
33
+ localFingerprint: string;
34
+ isolatedFingerprint: string;
35
+ effectiveBaseOid: string;
36
+ baseStrategy: ApplyBaseStrategy;
37
+ /** 冲突计算时的真实 Local HEAD,供 Isolated Agent 在当前 Worktree 内同步并解决冲突。 */
38
+ localHeadOid: string;
39
+ isolatedHeadOid: string;
40
+ conflictingFiles: string[];
41
+ }
42
+ export interface ApplyPlanErrorResult {
43
+ status: 'error';
44
+ error: ApplyError;
45
+ }
46
+ export type ApplyPlanResult = ApplyReadyPlanResult | ApplyConflictPlanResult | ApplyPlanErrorResult;
47
+ export interface ApplySuccessResult {
48
+ status: 'applied';
49
+ changedFiles: string[];
50
+ /** 本次已整合的 Isolated 快照 commit;不创建 ref,仅供后续 Apply 作为内部基线。 */
51
+ nextBaseOid: string;
52
+ }
53
+ export interface ApplyErrorResult {
54
+ status: 'error';
55
+ error: ApplyError;
56
+ }
57
+ export type ApplyResult = ApplySuccessResult | ApplyErrorResult;
58
+ export interface FinishSuccessResult {
59
+ status: 'finished';
60
+ changedFiles: string[];
61
+ /** null 表示 Worktree 与 Local 已一致,没有创建空提交。 */
62
+ commitOid: string | null;
63
+ /** 本次已整合的 Isolated 快照,供清理未完成时后续 Apply 去重。 */
64
+ nextBaseOid: string;
65
+ }
66
+ export type FinishResult = FinishSuccessResult | ApplyErrorResult;
67
+ export interface InvalidInputApplyError {
68
+ code: 'invalid_input';
69
+ message: string;
70
+ }
71
+ export interface InvalidPlanApplyError {
72
+ code: 'invalid_plan';
73
+ message: string;
74
+ }
75
+ export interface StaleLocalApplyError {
76
+ code: 'stale_local';
77
+ message: string;
78
+ }
79
+ export interface StaleIsolatedApplyError {
80
+ code: 'stale_isolated';
81
+ message: string;
82
+ }
83
+ export interface GitApplyError {
84
+ code: 'git_error';
85
+ message: string;
86
+ }
87
+ export interface CommitIsolationApplyError {
88
+ code: 'commit_isolation_conflict';
89
+ message: string;
90
+ }
91
+ export interface OperationNotAllowedApplyError {
92
+ code: 'operation_not_allowed';
93
+ message: string;
94
+ }
95
+ export type ApplyError = InvalidInputApplyError | InvalidPlanApplyError | StaleLocalApplyError | StaleIsolatedApplyError | GitApplyError | CommitIsolationApplyError | OperationNotAllowedApplyError;
96
+ export interface SessionCheckoutApplyEngine {
97
+ inspectReview(input: ApplyPlanInput): Promise<InspectReviewResult>;
98
+ /** 复用真实 Apply merge 计算,但不持久化可执行 plan,也不修改任一 checkout。 */
99
+ preflight(input: ApplyPlanInput): Promise<ApplyPlanResult>;
100
+ plan(input: ApplyPlanInput): Promise<ApplyPlanResult>;
101
+ apply(plan: ApplyPlan): Promise<ApplyResult>;
102
+ finish(plan: ApplyPlan, options: {
103
+ commitMessage: string;
104
+ }): Promise<FinishResult>;
105
+ }
106
+ export interface SessionCheckoutApplyEngineOptions {
107
+ /** 测试/宿主 seam:在最后一次 Local fingerprint 校验前执行。 */
108
+ beforeFinalLocalValidation?(): Promise<void> | void;
109
+ }
110
+ export declare function createSessionCheckoutApplyEngine(options?: SessionCheckoutApplyEngineOptions): SessionCheckoutApplyEngine;