@agimon-ai/doompi-git 0.0.1-alpha.13 → 0.0.1-alpha.15
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/dist/extensions/web.mjs +81 -16
- package/dist/extensions/web.mjs.map +1 -1
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.cjs +5 -2
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.cjs.map +1 -1
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.mjs +5 -2
- package/dist/src/extensions/(backend)/channel/_lib/git-worktrees.server.mjs.map +1 -1
- package/dist/src/schemas/apiContracts.cjs +5 -1
- package/dist/src/schemas/apiContracts.cjs.map +1 -1
- package/dist/src/schemas/apiContracts.d.cts.map +1 -1
- package/dist/src/schemas/apiContracts.d.mts.map +1 -1
- package/dist/src/schemas/apiContracts.mjs +5 -1
- package/dist/src/schemas/apiContracts.mjs.map +1 -1
- package/dist/src/services/errors/index.cjs.map +1 -1
- package/dist/src/services/errors/index.mjs.map +1 -1
- package/dist/src/services/paths/index.cjs +10 -1
- package/dist/src/services/paths/index.cjs.map +1 -1
- package/dist/src/services/paths/index.mjs +10 -1
- package/dist/src/services/paths/index.mjs.map +1 -1
- package/dist/src/services/worktreeOperations/index.cjs +47 -8
- package/dist/src/services/worktreeOperations/index.cjs.map +1 -1
- package/dist/src/services/worktreeOperations/index.d.cts +2 -0
- package/dist/src/services/worktreeOperations/index.d.cts.map +1 -1
- package/dist/src/services/worktreeOperations/index.d.mts +2 -0
- package/dist/src/services/worktreeOperations/index.d.mts.map +1 -1
- package/dist/src/services/worktreeOperations/index.mjs +47 -8
- package/dist/src/services/worktreeOperations/index.mjs.map +1 -1
- package/dist/src/services/worktreeRegistry/index.cjs +41 -13
- package/dist/src/services/worktreeRegistry/index.cjs.map +1 -1
- package/dist/src/services/worktreeRegistry/index.mjs +35 -10
- package/dist/src/services/worktreeRegistry/index.mjs.map +1 -1
- package/dist/src/types/webWorktrees.cjs.map +1 -1
- package/dist/src/types/webWorktrees.mjs.map +1 -1
- package/package.json +10 -10
|
@@ -85,9 +85,18 @@ function createWorktreeOperations(deps) {
|
|
|
85
85
|
const requirePeerLive = (peerSessionId) => {
|
|
86
86
|
if (!requireSessionService().isLive(peerSessionId)) throw new DoomGitExpectedError("worktree_peer_unavailable", "The other worktree session is no longer live.", false, "Start a new worktree session before sending a message.");
|
|
87
87
|
};
|
|
88
|
-
|
|
88
|
+
const operations = {
|
|
89
89
|
async spawn(context, request, options) {
|
|
90
90
|
const { root, store, records } = await resolve(context);
|
|
91
|
+
const reservations = request.reservationId === void 0 ? void 0 : requireSessionService().reservations;
|
|
92
|
+
if (request.reservationId !== void 0 && (!reservations || request.task !== void 0)) throw new DoomGitExpectedError("invalid_request", "Reserved worktree sessions require host setup and cannot start a task.", false, "Create the session from its pending setup card.");
|
|
93
|
+
const reserved = request.reservationId === void 0 ? void 0 : reservations.read(request.reservationId, context.sessionId);
|
|
94
|
+
const existing = reserved === void 0 ? void 0 : records.find((record) => record.sessionId === reserved.sessionId);
|
|
95
|
+
if (existing) {
|
|
96
|
+
if (existing.parentSessionId !== context.sessionId || existing.branch !== request.branch || existing.path !== reserved?.cwd || request.baseRef !== void 0 && existing.baseRef !== request.baseRef) throw new DoomGitExpectedError("invalid_request", "This setup already owns a different worktree.", false, "Recover its existing branch and checkout.");
|
|
97
|
+
await reservations.complete(request.reservationId, context.sessionId);
|
|
98
|
+
return existing;
|
|
99
|
+
}
|
|
91
100
|
const refusal = refuseSpawn({
|
|
92
101
|
branch: request.branch,
|
|
93
102
|
existing: records
|
|
@@ -95,16 +104,20 @@ function createWorktreeOperations(deps) {
|
|
|
95
104
|
if (refusal !== void 0) throw new DoomGitExpectedError("worktree_exists", refusal, false, "Pick another branch name, or close it.");
|
|
96
105
|
const taskDelivery = request.task === void 0 ? void 0 : requireSessionDelivery();
|
|
97
106
|
const baseRef = request.baseRef ?? await git.currentBranch(root) ?? "HEAD";
|
|
98
|
-
const id = shortId(randomUUID());
|
|
99
|
-
|
|
107
|
+
const id = shortId(reserved?.sessionId ?? randomUUID());
|
|
108
|
+
let path = worktreeDirectory({
|
|
100
109
|
worktreesRoot: worktreesRoot(deps.homeDir),
|
|
101
110
|
repositoryLabel: repositoryLabel(root),
|
|
102
111
|
repositoryId: repositoryId(root),
|
|
103
112
|
branch: request.branch,
|
|
104
113
|
shortId: id
|
|
105
114
|
});
|
|
115
|
+
if (request.reservationId !== void 0) path = (await reservations.prepare(request.reservationId, context.sessionId, path)).cwd;
|
|
116
|
+
const recovering = reserved?.cwd !== void 0 && fs.existsSync(path);
|
|
117
|
+
if (recovering && (!(await git.listWorktreePaths(root)).includes(path) || await git.currentBranch(path) !== request.branch)) throw new DoomGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing directory; it will not be replaced.");
|
|
106
118
|
const cancelled = () => options?.signal?.aborted === true;
|
|
107
119
|
const rollback = async () => {
|
|
120
|
+
if (reserved) return ` The reserved checkout at ${path} was retained for recovery.`;
|
|
108
121
|
await git.removeWorktree({
|
|
109
122
|
repositoryRoot: root,
|
|
110
123
|
path,
|
|
@@ -116,7 +129,7 @@ function createWorktreeOperations(deps) {
|
|
|
116
129
|
}).catch(() => false) ? "" : ` The branch ${request.branch} has work on it and was kept.`;
|
|
117
130
|
};
|
|
118
131
|
options?.onProgress?.(`creating branch ${request.branch}\u2026`);
|
|
119
|
-
await git.addWorktree({
|
|
132
|
+
if (!recovering) await git.addWorktree({
|
|
120
133
|
repositoryRoot: root,
|
|
121
134
|
path,
|
|
122
135
|
branch: request.branch,
|
|
@@ -124,10 +137,10 @@ function createWorktreeOperations(deps) {
|
|
|
124
137
|
});
|
|
125
138
|
if (cancelled()) {
|
|
126
139
|
const kept = await rollback();
|
|
127
|
-
throw new DoomGitExpectedError("spawn_cancelled", `Cancelled before the session started.${kept}`, false, "The worktree was removed; run it again when you are ready.");
|
|
140
|
+
throw new DoomGitExpectedError("spawn_cancelled", `Cancelled before the session started.${kept}`, false, reserved ? "Retry setup with the same branch to recover the reserved checkout." : "The worktree was removed; run it again when you are ready.");
|
|
128
141
|
}
|
|
129
142
|
options?.onProgress?.("mirroring build output…");
|
|
130
|
-
mirror(root, path);
|
|
143
|
+
if (!recovering) mirror(root, path);
|
|
131
144
|
options?.onProgress?.("starting session…");
|
|
132
145
|
let sessionId;
|
|
133
146
|
try {
|
|
@@ -136,11 +149,13 @@ function createWorktreeOperations(deps) {
|
|
|
136
149
|
name: request.name ?? request.branch,
|
|
137
150
|
parentSessionId: context.sessionId,
|
|
138
151
|
sessionProvenance: "worktree",
|
|
139
|
-
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
152
|
+
...options?.signal === void 0 ? {} : { signal: options.signal },
|
|
153
|
+
...request.reservationId === void 0 ? {} : { reservationId: request.reservationId }
|
|
140
154
|
})).sessionId;
|
|
155
|
+
if (reserved && sessionId !== reserved.sessionId) throw new Error("The host did not use the reserved session identity.");
|
|
141
156
|
} catch (error) {
|
|
142
157
|
const kept = await rollback();
|
|
143
|
-
if (cancelled()) throw new DoomGitExpectedError("spawn_cancelled", `Cancelled while the session was starting.${kept}`, false, "The worktree was removed; run it again when you are ready.");
|
|
158
|
+
if (cancelled()) throw new DoomGitExpectedError("spawn_cancelled", `Cancelled while the session was starting.${kept}`, false, reserved ? "Retry setup with the same branch to recover the reserved checkout." : "The worktree was removed; run it again when you are ready.");
|
|
144
159
|
if (error instanceof HubUnavailableError) throw new DoomGitExpectedError("hub_unavailable", `${error.message}${kept}`, true, "Start the cockpit and try again.");
|
|
145
160
|
throw error;
|
|
146
161
|
}
|
|
@@ -180,6 +195,7 @@ function createWorktreeOperations(deps) {
|
|
|
180
195
|
} catch (error) {
|
|
181
196
|
throw new DoomGitExpectedError("task_delivery_failed", `Worktree ${record.id} was created, but its task admission was not confirmed (${error.message}).`, true, `Use send with worktree ${record.id} to deliver the task without recreating the worktree.`);
|
|
182
197
|
}
|
|
198
|
+
if (request.reservationId !== void 0) await reservations.complete(request.reservationId, context.sessionId);
|
|
183
199
|
return record;
|
|
184
200
|
},
|
|
185
201
|
async close(context, id, force) {
|
|
@@ -315,6 +331,29 @@ function createWorktreeOperations(deps) {
|
|
|
315
331
|
return plan;
|
|
316
332
|
}
|
|
317
333
|
};
|
|
334
|
+
const locked = async (context, operation, signal) => {
|
|
335
|
+
try {
|
|
336
|
+
const { store } = await resolve(context);
|
|
337
|
+
return await store.transaction(operation, signal);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (signal?.aborted && !(error instanceof DoomGitExpectedError)) throw new DoomGitExpectedError("spawn_cancelled", "The worktree operation was cancelled.", false, "Inspect its recorded state before retrying.");
|
|
340
|
+
if ([
|
|
341
|
+
"ENOTDIR",
|
|
342
|
+
"EACCES",
|
|
343
|
+
"EROFS",
|
|
344
|
+
"ENOSPC"
|
|
345
|
+
].includes(String(error.code))) throw new DoomGitExpectedError("registry_write_failed", "The worktree registry is not writable.", true, "Check permissions and free space before retrying.");
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
...operations,
|
|
351
|
+
spawn: (context, request, options) => locked(context, () => operations.spawn(context, request, options), options?.signal),
|
|
352
|
+
close: (context, id, force) => locked(context, () => operations.close(context, id, force)),
|
|
353
|
+
list: (context) => locked(context, () => operations.list(context)),
|
|
354
|
+
merge: (context, id, message) => locked(context, () => operations.merge(context, id, message)),
|
|
355
|
+
prune: (context, dryRun) => locked(context, () => operations.prune(context, dryRun))
|
|
356
|
+
};
|
|
318
357
|
}
|
|
319
358
|
//#endregion
|
|
320
359
|
export { createWorktreeOperations };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/worktreeOperations/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\n\nimport type { DoomHubSessionService } from '@agimon-ai/doompi-core/hub-channel';\nimport type { DoomSessionDeliveryService } from '@agimon-ai/doompi-session';\n\nimport { WORKTREE_RECORD_VERSION } from '../../types/worktreeRegistry';\nimport type { WorktreeGit, WorktreeRecord } from '../../types/worktreeRegistry';\nimport { mirrorComposition } from '../compositionMirror';\nimport { DoomGitExpectedError, HubUnavailableError } from '../errors';\nimport { registryFile, worktreesRoot } from '../paths';\nimport { repositoryId, repositoryLabel, shortId } from '../repositoryIdentity';\nimport { MAX_WORKTREE_MESSAGE_BYTES, type WorktreeMessageInbox, type WorktreeMessageParty } from '../worktreeEvents';\nimport { planPrune, reconcile, refuseClose, refuseSpawn, worktreeDirectory } from '../worktreeNaming';\nimport { createWorktreeRegistry } from '../worktreeRegistry';\n\nconst GIT_WORKTREE_DELEGATION_KIND = 'git.worktree.delegation';\nconst GIT_WORKTREE_MESSAGE_KIND = 'git.worktree.message';\nconst GIT_WORKTREE_REPORT_KIND = 'git.worktree.report';\nexport interface WorktreeContext {\n /** Where the calling session is working, used to find the repository. */\n cwd: string;\n /** The calling session, recorded as the new session's parent. */\n sessionId: string;\n}\n\nexport interface SpawnWorktreeRequest {\n branch: string;\n baseRef?: string;\n name?: string;\n task?: string;\n}\n\nexport interface SpawnOptions {\n /** Aborted when the caller gives up, so a spawn nobody is waiting for stops. */\n signal?: AbortSignal;\n /** Phase labels, for a caller that can show them while the work runs. */\n onProgress?: (label: string) => void;\n}\n\nexport interface WorktreeOperations {\n spawn(context: WorktreeContext, request: SpawnWorktreeRequest, options?: SpawnOptions): Promise<WorktreeRecord>;\n close(context: WorktreeContext, id: string, force: boolean): Promise<WorktreeRecord>;\n list(context: WorktreeContext): Promise<WorktreeRecord[]>;\n status(context: WorktreeContext, id: string): Promise<{ record: WorktreeRecord; dirtyFiles: string[] }>;\n merge(context: WorktreeContext, id: string, message?: string): Promise<WorktreeRecord>;\n prune(context: WorktreeContext, dryRun: boolean): Promise<ReturnType<typeof planPrune>>;\n send(context: WorktreeContext, id: string, message: string): Promise<void>;\n messages(context: WorktreeContext, id: string): Promise<import('../worktreeEvents').WorktreeMessage[]>;\n}\n\nexport interface WorktreeOperationsDeps {\n git: WorktreeGit;\n /** The canonical hub-owned session lifecycle. */\n sessionService?: DoomHubSessionService;\n /** Legacy in-memory fallback used outside the fixed Session composition. */\n messageInbox?: WorktreeMessageInbox;\n /** Reads the active Session provider when an operation executes. */\n sessionDelivery?: () => DoomSessionDeliveryService | undefined;\n /** Injected so a test never copies a real dependency tree. */\n mirror?: typeof mirrorComposition;\n homeDir?: string;\n /** Injected so a record's createdAt is reproducible in a test. */\n now?: () => Date;\n}\n\nexport function createWorktreeOperations(deps: WorktreeOperationsDeps): WorktreeOperations {\n const { git } = deps;\n const sessionService = deps.sessionService;\n const mirror = deps.mirror ?? mirrorComposition;\n const now = deps.now ?? (() => new Date());\n const requireSessionService = (): DoomHubSessionService => {\n if (sessionService === undefined)\n throw new HubUnavailableError('The cockpit session service is unavailable. Start the cockpit and try again.');\n return sessionService;\n };\n const requireMessageInbox = (): WorktreeMessageInbox => {\n if (deps.messageInbox === undefined)\n throw new HubUnavailableError('The Git message service is unavailable. Start the cockpit and try again.');\n return deps.messageInbox;\n };\n const readSessionDelivery = (): DoomSessionDeliveryService | undefined => deps.sessionDelivery?.();\n const requireSessionDelivery = (): DoomSessionDeliveryService => {\n const delivery = readSessionDelivery();\n if (delivery === undefined)\n throw new HubUnavailableError('The Session delivery service is unavailable. Start the cockpit and try again.');\n return delivery;\n };\n const probe = {\n alive: (record: WorktreeRecord) => requireSessionService().isLive(record.sessionId),\n exists: (path: string) => fs.existsSync(path),\n };\n\n /**\n * Everything an action needs about the repository it is acting on.\n *\n * Resolved per call rather than cached: a session's cwd is stable, but the\n * worktree registry is shared and another process may have written it since\n * the last call.\n */\n const resolve = async (context: WorktreeContext) => {\n const root = await git.repositoryRoot(context.cwd);\n if (root === undefined) {\n throw new DoomGitExpectedError(\n 'not_a_repository',\n `${context.cwd} is not inside a git repository.`,\n false,\n 'Run this from a repository checkout.',\n );\n }\n const store = createWorktreeRegistry(registryFile(root, deps.homeDir));\n return { root, store, records: store.list() };\n };\n\n const require = (records: readonly WorktreeRecord[], id: string): WorktreeRecord => {\n const record = records.find((entry) => entry.id === id);\n if (record === undefined) {\n throw new DoomGitExpectedError(\n 'worktree_not_found',\n `No worktree with id ${id}.`,\n false,\n 'Call list to see the current ids.',\n );\n }\n return record;\n };\n\n /**\n * Refuses to destroy or merge a worktree another live session is using.\n *\n * `parentSessionId` records who asked for the worktree. Two sessions in one\n * checkout share this registry, so without the check either could close the\n * other's work in progress by id.\n *\n * Once that session is gone there is no owner left to protect, and refusing\n * would strand the worktree: nothing else could ever close it.\n */\n const requireOwner = (context: WorktreeContext, record: WorktreeRecord, action: string): void => {\n if (record.parentSessionId === context.sessionId) return;\n if (!requireSessionService().isLive(record.parentSessionId)) return;\n throw new DoomGitExpectedError(\n 'worktree_not_owned',\n `Worktree ${record.id} belongs to another session.`,\n false,\n `Ask the session that created it to ${action} it, or close that session first.`,\n );\n };\n\n const peerFor = (\n context: WorktreeContext,\n record: WorktreeRecord,\n ): { sessionId: string; party: WorktreeMessageParty } => {\n if (context.sessionId === record.parentSessionId) return { sessionId: record.sessionId, party: 'parent' };\n if (context.sessionId === record.sessionId) return { sessionId: record.parentSessionId, party: 'child' };\n throw new DoomGitExpectedError(\n 'worktree_not_owned',\n `Worktree ${record.id} belongs to another session.`,\n false,\n 'Use a worktree owned by this session.',\n );\n };\n\n const requirePeerLive = (peerSessionId: string): void => {\n if (!requireSessionService().isLive(peerSessionId)) {\n throw new DoomGitExpectedError(\n 'worktree_peer_unavailable',\n 'The other worktree session is no longer live.',\n false,\n 'Start a new worktree session before sending a message.',\n );\n }\n };\n\n return {\n async spawn(context, request, options) {\n const { root, store, records } = await resolve(context);\n const refusal = refuseSpawn({ branch: request.branch, existing: records });\n if (refusal !== undefined) {\n throw new DoomGitExpectedError('worktree_exists', refusal, false, 'Pick another branch name, or close it.');\n }\n const taskDelivery = request.task === undefined ? undefined : requireSessionDelivery();\n const baseRef = request.baseRef ?? (await git.currentBranch(root)) ?? 'HEAD';\n const id = shortId(randomUUID());\n const path = worktreeDirectory({\n worktreesRoot: worktreesRoot(deps.homeDir),\n repositoryLabel: repositoryLabel(root),\n repositoryId: repositoryId(root),\n branch: request.branch,\n shortId: id,\n });\n\n // The worktree and the branch are made by one command, so they are undone\n // by one too. `worktree remove` leaves the branch behind, and a branch\n // nobody asked for is what a failed spawn used to leave on the floor.\n const cancelled = (): boolean => options?.signal?.aborted === true;\n const rollback = async (): Promise<string> => {\n await git.removeWorktree({ repositoryRoot: root, path, force: true }).catch(() => undefined);\n const deleted = await git.deleteBranch({ repositoryRoot: root, branch: request.branch }).catch(() => false);\n return deleted ? '' : ` The branch ${request.branch} has work on it and was kept.`;\n };\n\n options?.onProgress?.(`creating branch ${request.branch}\\u2026`);\n await git.addWorktree({ repositoryRoot: root, path, branch: request.branch, baseRef });\n\n // Checked here rather than only inside the session call: this is the\n // point where a checkout exists that nothing has recorded yet, which is\n // exactly the state an interrupted spawn used to leave behind.\n if (cancelled()) {\n const kept = await rollback();\n throw new DoomGitExpectedError(\n 'spawn_cancelled',\n `Cancelled before the session started.${kept}`,\n false,\n 'The worktree was removed; run it again when you are ready.',\n );\n }\n\n // Before the session, because the session composes the extensions this\n // repository's config names and those live in build output git does not\n // track. A worktree without it starts and dies on the first package it\n // cannot resolve. Nothing here can fail the spawn: a repository with no\n // build output to mirror simply has none.\n options?.onProgress?.('mirroring build output\\u2026');\n mirror(root, path);\n\n // The worktree exists from here on, so every later failure has to leave\n // it removed or recorded. An unrecorded directory on disk is the one\n // outcome with no way back: nothing would ever list it again.\n options?.onProgress?.('starting session\\u2026');\n let sessionId: string;\n try {\n const session = await requireSessionService().create({\n cwd: path,\n name: request.name ?? request.branch,\n parentSessionId: context.sessionId,\n sessionProvenance: 'worktree',\n ...(options?.signal === undefined ? {} : { signal: options.signal }),\n });\n sessionId = session.sessionId;\n } catch (error) {\n const kept = await rollback();\n if (cancelled()) {\n throw new DoomGitExpectedError(\n 'spawn_cancelled',\n `Cancelled while the session was starting.${kept}`,\n false,\n 'The worktree was removed; run it again when you are ready.',\n );\n }\n if (error instanceof HubUnavailableError) {\n throw new DoomGitExpectedError(\n 'hub_unavailable',\n `${error.message}${kept}`,\n true,\n 'Start the cockpit and try again.',\n );\n }\n throw error;\n }\n\n const record: WorktreeRecord = {\n version: WORKTREE_RECORD_VERSION,\n id,\n branch: request.branch,\n baseRef,\n path,\n repositoryRoot: root,\n sessionId,\n parentSessionId: context.sessionId,\n status: 'running',\n createdAt: now().toISOString(),\n };\n try {\n store.replace([...records, record]);\n } catch (error) {\n // A live session with no record is the worst outcome available here:\n // it runs, and no id anything can name points at it. The session goes\n // back too, rather than being left for someone to find by hand.\n await requireSessionService()\n .close(sessionId)\n .catch(() => undefined);\n const kept = await rollback();\n throw new DoomGitExpectedError(\n 'registry_write_failed',\n `The worktree was created but the registry could not be written (${(error as Error).message}).${kept}`,\n true,\n 'Check the registry file for permissions or free space, then try again.',\n );\n }\n if (request.task !== undefined && taskDelivery !== undefined) {\n try {\n const receipt = await taskDelivery.deliver({\n recipientKey: sessionId,\n kind: GIT_WORKTREE_DELEGATION_KIND,\n prompt: request.task,\n metadata: {\n worktreeId: record.id,\n fromRole: 'parent',\n text: request.task,\n sentAt: record.createdAt,\n },\n });\n const admission = await taskDelivery.waitForAdmission(receipt.deliveryId);\n if (admission !== 'admitted') {\n throw new Error(\n admission === 'recovery_required'\n ? 'The recipient could not admit the task.'\n : 'The recipient did not confirm task admission before the timeout.',\n );\n }\n } catch (error) {\n throw new DoomGitExpectedError(\n 'task_delivery_failed',\n `Worktree ${record.id} was created, but its task admission was not confirmed (${(error as Error).message}).`,\n true,\n `Use send with worktree ${record.id} to deliver the task without recreating the worktree.`,\n );\n }\n }\n return record;\n },\n\n async close(context, id, force) {\n const { root, store, records } = await resolve(context);\n const record = require(records, id);\n requireOwner(context, record, 'close');\n const dirtyFiles = fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : [];\n const refusal = refuseClose({ record, dirtyFiles, force });\n if (refusal !== undefined) {\n throw new DoomGitExpectedError('worktree_dirty', refusal, false, 'Commit the work, or pass force: true.');\n }\n // The session is stopped before the directory goes. Removing a checkout\n // while a process still holds files open in it is how a half-removed\n // worktree and a stale git administrative entry are made.\n await requireSessionService().close(record.sessionId);\n await git.removeWorktree({ repositoryRoot: root, path: record.path, force });\n store.replace(records.filter((entry) => entry.id !== id));\n return record;\n },\n\n async list(context) {\n const { store, records } = await resolve(context);\n const outcome = reconcile(records, probe);\n if (outcome.changed.length > 0) store.replace(outcome.records);\n return outcome.records;\n },\n\n async status(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n const dirtyFiles = fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : [];\n return { record, dirtyFiles };\n },\n\n async merge(context, id, message) {\n const { root, records } = await resolve(context);\n const record = require(records, id);\n requireOwner(context, record, 'merge');\n // The merge lands in the parent, so the parent is what must be clean. A\n // dirty parent would mix the person's in-flight edits into a merge commit\n // they did not intend to make.\n const parentDirty = await git.dirtyFiles(root);\n if (parentDirty.length > 0) {\n throw new DoomGitExpectedError(\n 'worktree_dirty',\n `The parent checkout has ${String(parentDirty.length)} uncommitted file(s): ${parentDirty.slice(0, 10).join(', ')}.`,\n false,\n 'Commit or stash the parent checkout first.',\n );\n }\n await git.mergeBranch({\n repositoryRoot: root,\n branch: record.branch,\n ...(message === undefined ? {} : { message }),\n });\n return record;\n },\n\n /** Sends only between the parent session and its own worktree session. */\n async send(context, id, message) {\n if (Buffer.byteLength(message, 'utf8') > MAX_WORKTREE_MESSAGE_BYTES) {\n throw new DoomGitExpectedError(\n 'message_too_large',\n `A message may not exceed ${String(MAX_WORKTREE_MESSAGE_BYTES)} bytes.`,\n false,\n 'Send a shorter message.',\n );\n }\n const { records } = await resolve(context);\n const record = require(records, id);\n const peer = peerFor(context, record);\n requirePeerLive(peer.sessionId);\n const sentAt = new Date().toISOString();\n const delivery = readSessionDelivery();\n if (delivery !== undefined) {\n const kind = peer.party === 'parent' ? GIT_WORKTREE_MESSAGE_KIND : GIT_WORKTREE_REPORT_KIND;\n const receipt = await delivery.deliver({\n recipientKey: peer.sessionId,\n kind,\n prompt:\n peer.party === 'parent'\n ? `Parent message for worktree ${record.id}:\\n\\n${message}`\n : `Report from worktree ${record.id}:\\n\\n${message}`,\n metadata: { worktreeId: record.id, fromRole: peer.party, text: message, sentAt },\n });\n const admission = await delivery.waitForAdmission(receipt.deliveryId);\n if (admission !== 'admitted') {\n throw new DoomGitExpectedError(\n 'message_delivery_failed',\n `Worktree ${record.id} persisted the message, but its session did not confirm admission.`,\n true,\n admission === 'recovery_required'\n ? 'Call messages from the recipient session to recover the persisted text.'\n : 'The message remains durable; retry after the recipient is ready.',\n );\n }\n return;\n }\n requireMessageInbox().send(peer.sessionId, {\n version: 1,\n worktreeId: record.id,\n fromSessionId: context.sessionId,\n from: peer.party,\n text: message,\n sentAt,\n });\n },\n\n async messages(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n const peer = peerFor(context, record);\n const delivery = readSessionDelivery();\n if (delivery === undefined) return requireMessageInbox().receive(record.id);\n const entries = delivery\n .inbox({ consumed: false, metadata: { worktreeId: record.id } })\n .filter(\n (entry) =>\n entry.senderKey === peer.sessionId &&\n (entry.state === 'admitted' || entry.state === 'recovery_required') &&\n (peer.party === 'parent'\n ? entry.kind === GIT_WORKTREE_REPORT_KIND\n : entry.kind === GIT_WORKTREE_MESSAGE_KIND || entry.kind === GIT_WORKTREE_DELEGATION_KIND),\n );\n return entries.map((entry) => {\n delivery.consume(entry.deliveryId);\n return {\n version: 1,\n worktreeId: record.id,\n fromSessionId: entry.senderKey,\n from: peer.party === 'parent' ? ('child' as const) : ('parent' as const),\n text:\n entry.state === 'recovery_required'\n ? `[Recovery required: prompt admission was not confirmed] ${entry.metadata.text ?? entry.prompt}`\n : (entry.metadata.text ?? entry.prompt),\n sentAt: entry.metadata.sentAt ?? '',\n };\n });\n },\n\n async prune(context, dryRun) {\n const { root, store, records } = await resolve(context);\n const reconciled = reconcile(records, probe);\n const gitPaths = await git.listWorktreePaths(root);\n const dirtyByPath = new Map<string, readonly string[]>();\n for (const record of reconciled.records) {\n if (record.status === 'orphaned' && fs.existsSync(record.path)) {\n dirtyByPath.set(record.path, await git.dirtyFiles(record.path));\n }\n }\n const plan = planPrune({\n records: reconciled.records,\n gitPaths,\n worktreesRoot: worktreesRoot(deps.homeDir),\n dirtyByPath,\n });\n if (dryRun) {\n store.replace(reconciled.records);\n return plan;\n }\n for (const record of plan.remove) {\n await git.removeWorktree({ repositoryRoot: root, path: record.path, force: false });\n }\n // The checkout goes, so the branch it was made for goes with it. Safe\n // delete only: git keeps anything holding commits, and the plan says\n // which ones it kept rather than leaving the reader to notice later.\n for (const record of [...plan.remove, ...plan.forget]) {\n const deleted = await git.deleteBranch({ repositoryRoot: root, branch: record.branch }).catch(() => false);\n if (!deleted) plan.keptBranches.push(record.branch);\n }\n const dropped = new Set([...plan.remove, ...plan.forget].map((record) => record.id));\n store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));\n return plan;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/worktreeOperations/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\n\nimport type { DoomHubSessionService } from '@agimon-ai/doompi-core/hub-channel';\nimport type { DoomSessionDeliveryService } from '@agimon-ai/doompi-session';\n\nimport { WORKTREE_RECORD_VERSION } from '../../types/worktreeRegistry';\nimport type { WorktreeGit, WorktreeRecord } from '../../types/worktreeRegistry';\nimport { mirrorComposition } from '../compositionMirror';\nimport { DoomGitExpectedError, HubUnavailableError } from '../errors';\nimport { registryFile, worktreesRoot } from '../paths';\nimport { repositoryId, repositoryLabel, shortId } from '../repositoryIdentity';\nimport { MAX_WORKTREE_MESSAGE_BYTES, type WorktreeMessageInbox, type WorktreeMessageParty } from '../worktreeEvents';\nimport { planPrune, reconcile, refuseClose, refuseSpawn, worktreeDirectory } from '../worktreeNaming';\nimport { createWorktreeRegistry } from '../worktreeRegistry';\n\nconst GIT_WORKTREE_DELEGATION_KIND = 'git.worktree.delegation';\nconst GIT_WORKTREE_MESSAGE_KIND = 'git.worktree.message';\nconst GIT_WORKTREE_REPORT_KIND = 'git.worktree.report';\nexport interface WorktreeContext {\n /** Where the calling session is working, used to find the repository. */\n cwd: string;\n /** The calling session, recorded as the new session's parent. */\n sessionId: string;\n}\n\nexport interface SpawnWorktreeRequest {\n branch: string;\n baseRef?: string;\n name?: string;\n task?: string;\n /** Host-owned execution setup, exposed by the UI rather than the agent tool. */\n reservationId?: string;\n}\n\nexport interface SpawnOptions {\n /** Aborted when the caller gives up, so a spawn nobody is waiting for stops. */\n signal?: AbortSignal;\n /** Phase labels, for a caller that can show them while the work runs. */\n onProgress?: (label: string) => void;\n}\n\nexport interface WorktreeOperations {\n spawn(context: WorktreeContext, request: SpawnWorktreeRequest, options?: SpawnOptions): Promise<WorktreeRecord>;\n close(context: WorktreeContext, id: string, force: boolean): Promise<WorktreeRecord>;\n list(context: WorktreeContext): Promise<WorktreeRecord[]>;\n status(context: WorktreeContext, id: string): Promise<{ record: WorktreeRecord; dirtyFiles: string[] }>;\n merge(context: WorktreeContext, id: string, message?: string): Promise<WorktreeRecord>;\n prune(context: WorktreeContext, dryRun: boolean): Promise<ReturnType<typeof planPrune>>;\n send(context: WorktreeContext, id: string, message: string): Promise<void>;\n messages(context: WorktreeContext, id: string): Promise<import('../worktreeEvents').WorktreeMessage[]>;\n}\n\nexport interface WorktreeOperationsDeps {\n git: WorktreeGit;\n /** The canonical hub-owned session lifecycle. */\n sessionService?: DoomHubSessionService;\n /** Legacy in-memory fallback used outside the fixed Session composition. */\n messageInbox?: WorktreeMessageInbox;\n /** Reads the active Session provider when an operation executes. */\n sessionDelivery?: () => DoomSessionDeliveryService | undefined;\n /** Injected so a test never copies a real dependency tree. */\n mirror?: typeof mirrorComposition;\n homeDir?: string;\n /** Injected so a record's createdAt is reproducible in a test. */\n now?: () => Date;\n}\n\nexport function createWorktreeOperations(deps: WorktreeOperationsDeps): WorktreeOperations {\n const { git } = deps;\n const sessionService = deps.sessionService;\n const mirror = deps.mirror ?? mirrorComposition;\n const now = deps.now ?? (() => new Date());\n const requireSessionService = (): DoomHubSessionService => {\n if (sessionService === undefined)\n throw new HubUnavailableError('The cockpit session service is unavailable. Start the cockpit and try again.');\n return sessionService;\n };\n const requireMessageInbox = (): WorktreeMessageInbox => {\n if (deps.messageInbox === undefined)\n throw new HubUnavailableError('The Git message service is unavailable. Start the cockpit and try again.');\n return deps.messageInbox;\n };\n const readSessionDelivery = (): DoomSessionDeliveryService | undefined => deps.sessionDelivery?.();\n const requireSessionDelivery = (): DoomSessionDeliveryService => {\n const delivery = readSessionDelivery();\n if (delivery === undefined)\n throw new HubUnavailableError('The Session delivery service is unavailable. Start the cockpit and try again.');\n return delivery;\n };\n const probe = {\n alive: (record: WorktreeRecord) => requireSessionService().isLive(record.sessionId),\n exists: (path: string) => fs.existsSync(path),\n };\n\n /**\n * Everything an action needs about the repository it is acting on.\n *\n * Resolved per call rather than cached: a session's cwd is stable, but the\n * worktree registry is shared and another process may have written it since\n * the last call.\n */\n const resolve = async (context: WorktreeContext) => {\n const root = await git.repositoryRoot(context.cwd);\n if (root === undefined) {\n throw new DoomGitExpectedError(\n 'not_a_repository',\n `${context.cwd} is not inside a git repository.`,\n false,\n 'Run this from a repository checkout.',\n );\n }\n const store = createWorktreeRegistry(registryFile(root, deps.homeDir));\n return { root, store, records: store.list() };\n };\n\n const require = (records: readonly WorktreeRecord[], id: string): WorktreeRecord => {\n const record = records.find((entry) => entry.id === id);\n if (record === undefined) {\n throw new DoomGitExpectedError(\n 'worktree_not_found',\n `No worktree with id ${id}.`,\n false,\n 'Call list to see the current ids.',\n );\n }\n return record;\n };\n\n /**\n * Refuses to destroy or merge a worktree another live session is using.\n *\n * `parentSessionId` records who asked for the worktree. Two sessions in one\n * checkout share this registry, so without the check either could close the\n * other's work in progress by id.\n *\n * Once that session is gone there is no owner left to protect, and refusing\n * would strand the worktree: nothing else could ever close it.\n */\n const requireOwner = (context: WorktreeContext, record: WorktreeRecord, action: string): void => {\n if (record.parentSessionId === context.sessionId) return;\n if (!requireSessionService().isLive(record.parentSessionId)) return;\n throw new DoomGitExpectedError(\n 'worktree_not_owned',\n `Worktree ${record.id} belongs to another session.`,\n false,\n `Ask the session that created it to ${action} it, or close that session first.`,\n );\n };\n\n const peerFor = (\n context: WorktreeContext,\n record: WorktreeRecord,\n ): { sessionId: string; party: WorktreeMessageParty } => {\n if (context.sessionId === record.parentSessionId) return { sessionId: record.sessionId, party: 'parent' };\n if (context.sessionId === record.sessionId) return { sessionId: record.parentSessionId, party: 'child' };\n throw new DoomGitExpectedError(\n 'worktree_not_owned',\n `Worktree ${record.id} belongs to another session.`,\n false,\n 'Use a worktree owned by this session.',\n );\n };\n\n const requirePeerLive = (peerSessionId: string): void => {\n if (!requireSessionService().isLive(peerSessionId)) {\n throw new DoomGitExpectedError(\n 'worktree_peer_unavailable',\n 'The other worktree session is no longer live.',\n false,\n 'Start a new worktree session before sending a message.',\n );\n }\n };\n\n const operations: WorktreeOperations = {\n async spawn(context, request, options) {\n const { root, store, records } = await resolve(context);\n const reservations = request.reservationId === undefined ? undefined : requireSessionService().reservations;\n if (request.reservationId !== undefined && (!reservations || request.task !== undefined))\n throw new DoomGitExpectedError(\n 'invalid_request',\n 'Reserved worktree sessions require host setup and cannot start a task.',\n false,\n 'Create the session from its pending setup card.',\n );\n const reserved =\n request.reservationId === undefined ? undefined : reservations!.read(request.reservationId, context.sessionId);\n const existing =\n reserved === undefined ? undefined : records.find((record) => record.sessionId === reserved.sessionId);\n if (existing) {\n if (\n existing.parentSessionId !== context.sessionId ||\n existing.branch !== request.branch ||\n existing.path !== reserved?.cwd ||\n (request.baseRef !== undefined && existing.baseRef !== request.baseRef)\n )\n throw new DoomGitExpectedError(\n 'invalid_request',\n 'This setup already owns a different worktree.',\n false,\n 'Recover its existing branch and checkout.',\n );\n await reservations!.complete(request.reservationId!, context.sessionId);\n return existing;\n }\n const refusal = refuseSpawn({ branch: request.branch, existing: records });\n if (refusal !== undefined) {\n throw new DoomGitExpectedError('worktree_exists', refusal, false, 'Pick another branch name, or close it.');\n }\n const taskDelivery = request.task === undefined ? undefined : requireSessionDelivery();\n const baseRef = request.baseRef ?? (await git.currentBranch(root)) ?? 'HEAD';\n const id = shortId(reserved?.sessionId ?? randomUUID());\n let path = worktreeDirectory({\n worktreesRoot: worktreesRoot(deps.homeDir),\n repositoryLabel: repositoryLabel(root),\n repositoryId: repositoryId(root),\n branch: request.branch,\n shortId: id,\n });\n if (request.reservationId !== undefined) {\n path = (await reservations!.prepare(request.reservationId, context.sessionId, path)).cwd;\n }\n const recovering = reserved?.cwd !== undefined && fs.existsSync(path);\n if (\n recovering &&\n (!(await git.listWorktreePaths(root)).includes(path) || (await git.currentBranch(path)) !== request.branch)\n )\n throw new DoomGitExpectedError(\n 'invalid_request',\n 'The reserved checkout no longer matches this setup.',\n false,\n 'Inspect the existing directory; it will not be replaced.',\n );\n // The worktree and the branch are made by one command, so they are undone\n // by one too. `worktree remove` leaves the branch behind, and a branch\n // nobody asked for is what a failed spawn used to leave on the floor.\n const cancelled = (): boolean => options?.signal?.aborted === true;\n const rollback = async (): Promise<string> => {\n if (reserved) return ` The reserved checkout at ${path} was retained for recovery.`;\n await git.removeWorktree({ repositoryRoot: root, path, force: true }).catch(() => undefined);\n const deleted = await git.deleteBranch({ repositoryRoot: root, branch: request.branch }).catch(() => false);\n return deleted ? '' : ` The branch ${request.branch} has work on it and was kept.`;\n };\n\n options?.onProgress?.(`creating branch ${request.branch}\\u2026`);\n if (!recovering) await git.addWorktree({ repositoryRoot: root, path, branch: request.branch, baseRef });\n\n // Checked here rather than only inside the session call: this is the\n // point where a checkout exists that nothing has recorded yet, which is\n // exactly the state an interrupted spawn used to leave behind.\n if (cancelled()) {\n const kept = await rollback();\n throw new DoomGitExpectedError(\n 'spawn_cancelled',\n `Cancelled before the session started.${kept}`,\n false,\n reserved\n ? 'Retry setup with the same branch to recover the reserved checkout.'\n : 'The worktree was removed; run it again when you are ready.',\n );\n }\n\n // Before the session, because the session composes the extensions this\n // repository's config names and those live in build output git does not\n // track. A worktree without it starts and dies on the first package it\n // cannot resolve. Nothing here can fail the spawn: a repository with no\n // build output to mirror simply has none.\n options?.onProgress?.('mirroring build output\\u2026');\n if (!recovering) mirror(root, path);\n\n // The worktree exists from here on, so every later failure has to leave\n // it removed or recorded. An unrecorded directory on disk is the one\n // outcome with no way back: nothing would ever list it again.\n options?.onProgress?.('starting session\\u2026');\n let sessionId: string;\n try {\n const session = await requireSessionService().create({\n cwd: path,\n name: request.name ?? request.branch,\n parentSessionId: context.sessionId,\n sessionProvenance: 'worktree',\n ...(options?.signal === undefined ? {} : { signal: options.signal }),\n ...(request.reservationId === undefined ? {} : { reservationId: request.reservationId }),\n });\n sessionId = session.sessionId;\n if (reserved && sessionId !== reserved.sessionId)\n throw new Error('The host did not use the reserved session identity.');\n } catch (error) {\n const kept = await rollback();\n if (cancelled()) {\n throw new DoomGitExpectedError(\n 'spawn_cancelled',\n `Cancelled while the session was starting.${kept}`,\n false,\n reserved\n ? 'Retry setup with the same branch to recover the reserved checkout.'\n : 'The worktree was removed; run it again when you are ready.',\n );\n }\n if (error instanceof HubUnavailableError) {\n throw new DoomGitExpectedError(\n 'hub_unavailable',\n `${error.message}${kept}`,\n true,\n 'Start the cockpit and try again.',\n );\n }\n throw error;\n }\n\n const record: WorktreeRecord = {\n version: WORKTREE_RECORD_VERSION,\n id,\n branch: request.branch,\n baseRef,\n path,\n repositoryRoot: root,\n sessionId,\n parentSessionId: context.sessionId,\n status: 'running',\n createdAt: now().toISOString(),\n };\n try {\n store.replace([...records, record]);\n } catch (error) {\n // A live session with no record is the worst outcome available here:\n // it runs, and no id anything can name points at it. The session goes\n // back too, rather than being left for someone to find by hand.\n await requireSessionService()\n .close(sessionId)\n .catch(() => undefined);\n const kept = await rollback();\n throw new DoomGitExpectedError(\n 'registry_write_failed',\n `The worktree was created but the registry could not be written (${(error as Error).message}).${kept}`,\n true,\n 'Check the registry file for permissions or free space, then try again.',\n );\n }\n if (request.task !== undefined && taskDelivery !== undefined) {\n try {\n const receipt = await taskDelivery.deliver({\n recipientKey: sessionId,\n kind: GIT_WORKTREE_DELEGATION_KIND,\n prompt: request.task,\n metadata: {\n worktreeId: record.id,\n fromRole: 'parent',\n text: request.task,\n sentAt: record.createdAt,\n },\n });\n const admission = await taskDelivery.waitForAdmission(receipt.deliveryId);\n if (admission !== 'admitted') {\n throw new Error(\n admission === 'recovery_required'\n ? 'The recipient could not admit the task.'\n : 'The recipient did not confirm task admission before the timeout.',\n );\n }\n } catch (error) {\n throw new DoomGitExpectedError(\n 'task_delivery_failed',\n `Worktree ${record.id} was created, but its task admission was not confirmed (${(error as Error).message}).`,\n true,\n `Use send with worktree ${record.id} to deliver the task without recreating the worktree.`,\n );\n }\n }\n if (request.reservationId !== undefined) await reservations!.complete(request.reservationId, context.sessionId);\n return record;\n },\n\n async close(context, id, force) {\n const { root, store, records } = await resolve(context);\n const record = require(records, id);\n requireOwner(context, record, 'close');\n const dirtyFiles = fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : [];\n const refusal = refuseClose({ record, dirtyFiles, force });\n if (refusal !== undefined) {\n throw new DoomGitExpectedError('worktree_dirty', refusal, false, 'Commit the work, or pass force: true.');\n }\n // The session is stopped before the directory goes. Removing a checkout\n // while a process still holds files open in it is how a half-removed\n // worktree and a stale git administrative entry are made.\n await requireSessionService().close(record.sessionId);\n await git.removeWorktree({ repositoryRoot: root, path: record.path, force });\n store.replace(records.filter((entry) => entry.id !== id));\n return record;\n },\n\n async list(context) {\n const { store, records } = await resolve(context);\n const outcome = reconcile(records, probe);\n if (outcome.changed.length > 0) store.replace(outcome.records);\n return outcome.records;\n },\n\n async status(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n const dirtyFiles = fs.existsSync(record.path) ? await git.dirtyFiles(record.path) : [];\n return { record, dirtyFiles };\n },\n\n async merge(context, id, message) {\n const { root, records } = await resolve(context);\n const record = require(records, id);\n requireOwner(context, record, 'merge');\n // The merge lands in the parent, so the parent is what must be clean. A\n // dirty parent would mix the person's in-flight edits into a merge commit\n // they did not intend to make.\n const parentDirty = await git.dirtyFiles(root);\n if (parentDirty.length > 0) {\n throw new DoomGitExpectedError(\n 'worktree_dirty',\n `The parent checkout has ${String(parentDirty.length)} uncommitted file(s): ${parentDirty.slice(0, 10).join(', ')}.`,\n false,\n 'Commit or stash the parent checkout first.',\n );\n }\n await git.mergeBranch({\n repositoryRoot: root,\n branch: record.branch,\n ...(message === undefined ? {} : { message }),\n });\n return record;\n },\n\n /** Sends only between the parent session and its own worktree session. */\n async send(context, id, message) {\n if (Buffer.byteLength(message, 'utf8') > MAX_WORKTREE_MESSAGE_BYTES) {\n throw new DoomGitExpectedError(\n 'message_too_large',\n `A message may not exceed ${String(MAX_WORKTREE_MESSAGE_BYTES)} bytes.`,\n false,\n 'Send a shorter message.',\n );\n }\n const { records } = await resolve(context);\n const record = require(records, id);\n const peer = peerFor(context, record);\n requirePeerLive(peer.sessionId);\n const sentAt = new Date().toISOString();\n const delivery = readSessionDelivery();\n if (delivery !== undefined) {\n const kind = peer.party === 'parent' ? GIT_WORKTREE_MESSAGE_KIND : GIT_WORKTREE_REPORT_KIND;\n const receipt = await delivery.deliver({\n recipientKey: peer.sessionId,\n kind,\n prompt:\n peer.party === 'parent'\n ? `Parent message for worktree ${record.id}:\\n\\n${message}`\n : `Report from worktree ${record.id}:\\n\\n${message}`,\n metadata: { worktreeId: record.id, fromRole: peer.party, text: message, sentAt },\n });\n const admission = await delivery.waitForAdmission(receipt.deliveryId);\n if (admission !== 'admitted') {\n throw new DoomGitExpectedError(\n 'message_delivery_failed',\n `Worktree ${record.id} persisted the message, but its session did not confirm admission.`,\n true,\n admission === 'recovery_required'\n ? 'Call messages from the recipient session to recover the persisted text.'\n : 'The message remains durable; retry after the recipient is ready.',\n );\n }\n return;\n }\n requireMessageInbox().send(peer.sessionId, {\n version: 1,\n worktreeId: record.id,\n fromSessionId: context.sessionId,\n from: peer.party,\n text: message,\n sentAt,\n });\n },\n\n async messages(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n const peer = peerFor(context, record);\n const delivery = readSessionDelivery();\n if (delivery === undefined) return requireMessageInbox().receive(record.id);\n const entries = delivery\n .inbox({ consumed: false, metadata: { worktreeId: record.id } })\n .filter(\n (entry) =>\n entry.senderKey === peer.sessionId &&\n (entry.state === 'admitted' || entry.state === 'recovery_required') &&\n (peer.party === 'parent'\n ? entry.kind === GIT_WORKTREE_REPORT_KIND\n : entry.kind === GIT_WORKTREE_MESSAGE_KIND || entry.kind === GIT_WORKTREE_DELEGATION_KIND),\n );\n return entries.map((entry) => {\n delivery.consume(entry.deliveryId);\n return {\n version: 1,\n worktreeId: record.id,\n fromSessionId: entry.senderKey,\n from: peer.party === 'parent' ? ('child' as const) : ('parent' as const),\n text:\n entry.state === 'recovery_required'\n ? `[Recovery required: prompt admission was not confirmed] ${entry.metadata.text ?? entry.prompt}`\n : (entry.metadata.text ?? entry.prompt),\n sentAt: entry.metadata.sentAt ?? '',\n };\n });\n },\n\n async prune(context, dryRun) {\n const { root, store, records } = await resolve(context);\n const reconciled = reconcile(records, probe);\n const gitPaths = await git.listWorktreePaths(root);\n const dirtyByPath = new Map<string, readonly string[]>();\n for (const record of reconciled.records) {\n if (record.status === 'orphaned' && fs.existsSync(record.path)) {\n dirtyByPath.set(record.path, await git.dirtyFiles(record.path));\n }\n }\n const plan = planPrune({\n records: reconciled.records,\n gitPaths,\n worktreesRoot: worktreesRoot(deps.homeDir),\n dirtyByPath,\n });\n if (dryRun) {\n store.replace(reconciled.records);\n return plan;\n }\n for (const record of plan.remove) {\n await git.removeWorktree({ repositoryRoot: root, path: record.path, force: false });\n }\n // The checkout goes, so the branch it was made for goes with it. Safe\n // delete only: git keeps anything holding commits, and the plan says\n // which ones it kept rather than leaving the reader to notice later.\n for (const record of [...plan.remove, ...plan.forget]) {\n const deleted = await git.deleteBranch({ repositoryRoot: root, branch: record.branch }).catch(() => false);\n if (!deleted) plan.keptBranches.push(record.branch);\n }\n const dropped = new Set([...plan.remove, ...plan.forget].map((record) => record.id));\n store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));\n return plan;\n },\n };\n const locked = async <T>(context: WorktreeContext, operation: () => Promise<T>, signal?: AbortSignal): Promise<T> => {\n try {\n const { store } = await resolve(context);\n return await store.transaction(operation, signal);\n } catch (error) {\n if (signal?.aborted && !(error instanceof DoomGitExpectedError))\n throw new DoomGitExpectedError(\n 'spawn_cancelled',\n 'The worktree operation was cancelled.',\n false,\n 'Inspect its recorded state before retrying.',\n );\n if (['ENOTDIR', 'EACCES', 'EROFS', 'ENOSPC'].includes(String((error as NodeJS.ErrnoException).code)))\n throw new DoomGitExpectedError(\n 'registry_write_failed',\n 'The worktree registry is not writable.',\n true,\n 'Check permissions and free space before retrying.',\n );\n throw error;\n }\n };\n // ponytail: serialize Git lifecycle mutations, not session execution. Keeping\n // the lock across startup avoids a second lease system for partially created trees.\n return {\n ...operations,\n spawn: (context, request, options) =>\n locked(context, () => operations.spawn(context, request, options), options?.signal),\n close: (context, id, force) => locked(context, () => operations.close(context, id, force)),\n list: (context) => locked(context, () => operations.list(context)),\n merge: (context, id, message) => locked(context, () => operations.merge(context, id, message)),\n prune: (context, dryRun) => locked(context, () => operations.prune(context, dryRun)),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,34 +1,62 @@
|
|
|
1
|
-
const
|
|
1
|
+
const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_index = require("../errors/index.cjs");
|
|
3
|
+
const require_index$1 = require("../atomicJson/index.cjs");
|
|
4
|
+
let node_fs = require("node:fs");
|
|
5
|
+
node_fs = require_runtime.__toESM(node_fs, 1);
|
|
6
|
+
let node_path = require("node:path");
|
|
7
|
+
node_path = require_runtime.__toESM(node_path, 1);
|
|
8
|
+
let node_timers_promises = require("node:timers/promises");
|
|
2
9
|
//#region src/services/worktreeRegistry/index.ts
|
|
10
|
+
const LOCK_WAIT_MS = 5e3;
|
|
11
|
+
const LOCK_RETRY_MS = 25;
|
|
3
12
|
function isRecord(value) {
|
|
4
13
|
if (typeof value !== "object" || value === null) return false;
|
|
5
14
|
const record = value;
|
|
6
15
|
return record.version === 1 && typeof record.id === "string" && record.id !== "" && typeof record.path === "string" && typeof record.branch === "string" && typeof record.sessionId === "string";
|
|
7
16
|
}
|
|
8
17
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* merging: every write comes from a tool call in one session's turn, so there
|
|
13
|
-
* is no concurrent writer to reconcile with. Choosing the simpler shape now
|
|
14
|
-
* means a reader can see the whole registry in one place; if concurrent writers
|
|
15
|
-
* ever appear, that is the point to revisit, not before.
|
|
16
|
-
*
|
|
17
|
-
* A file that does not parse is treated as empty rather than fatal. Losing
|
|
18
|
-
* track of worktrees is recoverable through prune; refusing to run is not.
|
|
18
|
+
* Repository mutations hold an exclusive directory lock from the read through
|
|
19
|
+
* the final commit. This works across independently loaded packages and hosts.
|
|
20
|
+
* A crashed owner leaves a visible lock, never a lease another writer may steal.
|
|
19
21
|
*/
|
|
20
22
|
function createWorktreeRegistry(file) {
|
|
21
23
|
return {
|
|
22
24
|
list() {
|
|
23
|
-
const parsed = require_index.readJson(file);
|
|
25
|
+
const parsed = require_index$1.readJson(file);
|
|
24
26
|
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) return [];
|
|
25
27
|
return parsed.entries.filter(isRecord);
|
|
26
28
|
},
|
|
27
29
|
replace(entries) {
|
|
28
|
-
require_index.writeJsonAtomic(file, {
|
|
30
|
+
require_index$1.writeJsonAtomic(file, {
|
|
29
31
|
version: 1,
|
|
30
32
|
entries
|
|
31
33
|
});
|
|
34
|
+
},
|
|
35
|
+
async transaction(operation, signal) {
|
|
36
|
+
const lock = `${file}.lock`;
|
|
37
|
+
node_fs.default.mkdirSync(node_path.default.dirname(file), { recursive: true });
|
|
38
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
39
|
+
for (;;) {
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
try {
|
|
42
|
+
node_fs.default.mkdirSync(lock, { mode: 448 });
|
|
43
|
+
break;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code !== "EEXIST") throw error;
|
|
46
|
+
if (Date.now() >= deadline) throw new require_index.DoomGitExpectedError("registry_busy", "Another worktree operation holds the repository registry.", true, "Retry after it finishes. After a host crash, inspect and remove the abandoned worktrees.json.lock directory.");
|
|
47
|
+
await (0, node_timers_promises.setTimeout)(LOCK_RETRY_MS, void 0, { signal });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
node_fs.default.writeFileSync(node_path.default.join(lock, "owner.json"), JSON.stringify({
|
|
52
|
+
pid: process.pid,
|
|
53
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
54
|
+
}), { mode: 384 });
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
return await operation();
|
|
57
|
+
} finally {
|
|
58
|
+
node_fs.default.rmSync(lock, { recursive: true });
|
|
59
|
+
}
|
|
32
60
|
}
|
|
33
61
|
};
|
|
34
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["readJson"],"sources":["../../../../src/services/worktreeRegistry/index.ts"],"sourcesContent":["import {\n WORKTREE_RECORD_VERSION,\n type WorktreeRecord,\n type WorktreeRegistryFile,\n type WorktreeRegistryStore,\n} from '../../types/worktreeRegistry';\nimport { readJson, writeJsonAtomic } from '../atomicJson';\n\nfunction isRecord(value: unknown): value is WorktreeRecord {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<WorktreeRecord>;\n return (\n record.version === WORKTREE_RECORD_VERSION &&\n typeof record.id === 'string' &&\n record.id !== '' &&\n typeof record.path === 'string' &&\n typeof record.branch === 'string' &&\n typeof record.sessionId === 'string'\n );\n}\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["readJson","path","DoomGitExpectedError","setTimeout"],"sources":["../../../../src/services/worktreeRegistry/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { setTimeout } from 'node:timers/promises';\n\nimport {\n WORKTREE_RECORD_VERSION,\n type WorktreeRecord,\n type WorktreeRegistryFile,\n type WorktreeRegistryStore,\n} from '../../types/worktreeRegistry';\nimport { readJson, writeJsonAtomic } from '../atomicJson';\nimport { DoomGitExpectedError } from '../errors';\n\nconst LOCK_WAIT_MS = 5000;\nconst LOCK_RETRY_MS = 25;\n\nfunction isRecord(value: unknown): value is WorktreeRecord {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<WorktreeRecord>;\n return (\n record.version === WORKTREE_RECORD_VERSION &&\n typeof record.id === 'string' &&\n record.id !== '' &&\n typeof record.path === 'string' &&\n typeof record.branch === 'string' &&\n typeof record.sessionId === 'string'\n );\n}\n\n/**\n * Repository mutations hold an exclusive directory lock from the read through\n * the final commit. This works across independently loaded packages and hosts.\n * A crashed owner leaves a visible lock, never a lease another writer may steal.\n */\nexport function createWorktreeRegistry(file: string): WorktreeRegistryStore {\n return {\n list() {\n const parsed = readJson<WorktreeRegistryFile>(file);\n if (!parsed || parsed.version !== WORKTREE_RECORD_VERSION || !Array.isArray(parsed.entries)) return [];\n return parsed.entries.filter(isRecord);\n },\n replace(entries) {\n writeJsonAtomic(file, { version: WORKTREE_RECORD_VERSION, entries } satisfies WorktreeRegistryFile);\n },\n async transaction(operation, signal) {\n const lock = `${file}.lock`;\n fs.mkdirSync(path.dirname(file), { recursive: true });\n const deadline = Date.now() + LOCK_WAIT_MS;\n for (;;) {\n signal?.throwIfAborted();\n try {\n fs.mkdirSync(lock, { mode: 0o700 });\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n if (Date.now() >= deadline)\n throw new DoomGitExpectedError(\n 'registry_busy',\n 'Another worktree operation holds the repository registry.',\n true,\n 'Retry after it finishes. After a host crash, inspect and remove the abandoned worktrees.json.lock directory.',\n );\n await setTimeout(LOCK_RETRY_MS, undefined, { signal });\n }\n }\n try {\n fs.writeFileSync(\n path.join(lock, 'owner.json'),\n JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }),\n { mode: 0o600 },\n );\n signal?.throwIfAborted();\n return await operation();\n } finally {\n fs.rmSync(lock, { recursive: true });\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;AAaA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAEtB,SAAS,SAAS,OAAyC;CACzD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,SAAS;CACf,OACE,OAAO,YAAA,KACP,OAAO,OAAO,OAAO,YACrB,OAAO,OAAO,MACd,OAAO,OAAO,SAAS,YACvB,OAAO,OAAO,WAAW,YACzB,OAAO,OAAO,cAAc;AAEhC;;;;;;AAOA,SAAgB,uBAAuB,MAAqC;CAC1E,OAAO;EACL,OAAO;GACL,MAAM,SAASA,gBAAAA,SAA+B,IAAI;GAClD,IAAI,CAAC,UAAU,OAAO,YAAA,KAAuC,CAAC,MAAM,QAAQ,OAAO,OAAO,GAAG,OAAO,CAAC;GACrG,OAAO,OAAO,QAAQ,OAAO,QAAQ;EACvC;EACA,QAAQ,SAAS;GACf,gBAAA,gBAAgB,MAAM;IAAE,SAAA;IAAkC;GAAQ,CAAgC;EACpG;EACA,MAAM,YAAY,WAAW,QAAQ;GACnC,MAAM,OAAO,GAAG,KAAK;GACrB,QAAA,QAAG,UAAUC,UAAAA,QAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;GACpD,MAAM,WAAW,KAAK,IAAI,IAAI;GAC9B,SAAS;IACP,QAAQ,eAAe;IACvB,IAAI;KACF,QAAA,QAAG,UAAU,MAAM,EAAE,MAAM,IAAM,CAAC;KAClC;IACF,SAAS,OAAO;KACd,IAAK,MAAgC,SAAS,UAAU,MAAM;KAC9D,IAAI,KAAK,IAAI,KAAK,UAChB,MAAM,IAAIC,cAAAA,qBACR,iBACA,6DACA,MACA,8GACF;KACF,OAAA,GAAMC,qBAAAA,WAAAA,CAAW,eAAe,KAAA,GAAW,EAAE,OAAO,CAAC;IACvD;GACF;GACA,IAAI;IACF,QAAA,QAAG,cACDF,UAAAA,QAAK,KAAK,MAAM,YAAY,GAC5B,KAAK,UAAU;KAAE,KAAK,QAAQ;KAAK,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;IAAE,CAAC,GACxE,EAAE,MAAM,IAAM,CAChB;IACA,QAAQ,eAAe;IACvB,OAAO,MAAM,UAAU;GACzB,UAAU;IACR,QAAA,QAAG,OAAO,MAAM,EAAE,WAAW,KAAK,CAAC;GACrC;EACF;CACF;AACF"}
|
|
@@ -1,21 +1,20 @@
|
|
|
1
|
+
import { DoomGitExpectedError } from "../errors/index.mjs";
|
|
1
2
|
import { readJson, writeJsonAtomic } from "../atomicJson/index.mjs";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
2
6
|
//#region src/services/worktreeRegistry/index.ts
|
|
7
|
+
const LOCK_WAIT_MS = 5e3;
|
|
8
|
+
const LOCK_RETRY_MS = 25;
|
|
3
9
|
function isRecord(value) {
|
|
4
10
|
if (typeof value !== "object" || value === null) return false;
|
|
5
11
|
const record = value;
|
|
6
12
|
return record.version === 1 && typeof record.id === "string" && record.id !== "" && typeof record.path === "string" && typeof record.branch === "string" && typeof record.sessionId === "string";
|
|
7
13
|
}
|
|
8
14
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* merging: every write comes from a tool call in one session's turn, so there
|
|
13
|
-
* is no concurrent writer to reconcile with. Choosing the simpler shape now
|
|
14
|
-
* means a reader can see the whole registry in one place; if concurrent writers
|
|
15
|
-
* ever appear, that is the point to revisit, not before.
|
|
16
|
-
*
|
|
17
|
-
* A file that does not parse is treated as empty rather than fatal. Losing
|
|
18
|
-
* track of worktrees is recoverable through prune; refusing to run is not.
|
|
15
|
+
* Repository mutations hold an exclusive directory lock from the read through
|
|
16
|
+
* the final commit. This works across independently loaded packages and hosts.
|
|
17
|
+
* A crashed owner leaves a visible lock, never a lease another writer may steal.
|
|
19
18
|
*/
|
|
20
19
|
function createWorktreeRegistry(file) {
|
|
21
20
|
return {
|
|
@@ -29,6 +28,32 @@ function createWorktreeRegistry(file) {
|
|
|
29
28
|
version: 1,
|
|
30
29
|
entries
|
|
31
30
|
});
|
|
31
|
+
},
|
|
32
|
+
async transaction(operation, signal) {
|
|
33
|
+
const lock = `${file}.lock`;
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
36
|
+
for (;;) {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(lock, { mode: 448 });
|
|
40
|
+
break;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code !== "EEXIST") throw error;
|
|
43
|
+
if (Date.now() >= deadline) throw new DoomGitExpectedError("registry_busy", "Another worktree operation holds the repository registry.", true, "Retry after it finishes. After a host crash, inspect and remove the abandoned worktrees.json.lock directory.");
|
|
44
|
+
await setTimeout(LOCK_RETRY_MS, void 0, { signal });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
fs.writeFileSync(path.join(lock, "owner.json"), JSON.stringify({
|
|
49
|
+
pid: process.pid,
|
|
50
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
51
|
+
}), { mode: 384 });
|
|
52
|
+
signal?.throwIfAborted();
|
|
53
|
+
return await operation();
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(lock, { recursive: true });
|
|
56
|
+
}
|
|
32
57
|
}
|
|
33
58
|
};
|
|
34
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/worktreeRegistry/index.ts"],"sourcesContent":["import {\n WORKTREE_RECORD_VERSION,\n type WorktreeRecord,\n type WorktreeRegistryFile,\n type WorktreeRegistryStore,\n} from '../../types/worktreeRegistry';\nimport { readJson, writeJsonAtomic } from '../atomicJson';\n\nfunction isRecord(value: unknown): value is WorktreeRecord {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<WorktreeRecord>;\n return (\n record.version === WORKTREE_RECORD_VERSION &&\n typeof record.id === 'string' &&\n record.id !== '' &&\n typeof record.path === 'string' &&\n typeof record.branch === 'string' &&\n typeof record.sessionId === 'string'\n );\n}\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/worktreeRegistry/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { setTimeout } from 'node:timers/promises';\n\nimport {\n WORKTREE_RECORD_VERSION,\n type WorktreeRecord,\n type WorktreeRegistryFile,\n type WorktreeRegistryStore,\n} from '../../types/worktreeRegistry';\nimport { readJson, writeJsonAtomic } from '../atomicJson';\nimport { DoomGitExpectedError } from '../errors';\n\nconst LOCK_WAIT_MS = 5000;\nconst LOCK_RETRY_MS = 25;\n\nfunction isRecord(value: unknown): value is WorktreeRecord {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<WorktreeRecord>;\n return (\n record.version === WORKTREE_RECORD_VERSION &&\n typeof record.id === 'string' &&\n record.id !== '' &&\n typeof record.path === 'string' &&\n typeof record.branch === 'string' &&\n typeof record.sessionId === 'string'\n );\n}\n\n/**\n * Repository mutations hold an exclusive directory lock from the read through\n * the final commit. This works across independently loaded packages and hosts.\n * A crashed owner leaves a visible lock, never a lease another writer may steal.\n */\nexport function createWorktreeRegistry(file: string): WorktreeRegistryStore {\n return {\n list() {\n const parsed = readJson<WorktreeRegistryFile>(file);\n if (!parsed || parsed.version !== WORKTREE_RECORD_VERSION || !Array.isArray(parsed.entries)) return [];\n return parsed.entries.filter(isRecord);\n },\n replace(entries) {\n writeJsonAtomic(file, { version: WORKTREE_RECORD_VERSION, entries } satisfies WorktreeRegistryFile);\n },\n async transaction(operation, signal) {\n const lock = `${file}.lock`;\n fs.mkdirSync(path.dirname(file), { recursive: true });\n const deadline = Date.now() + LOCK_WAIT_MS;\n for (;;) {\n signal?.throwIfAborted();\n try {\n fs.mkdirSync(lock, { mode: 0o700 });\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n if (Date.now() >= deadline)\n throw new DoomGitExpectedError(\n 'registry_busy',\n 'Another worktree operation holds the repository registry.',\n true,\n 'Retry after it finishes. After a host crash, inspect and remove the abandoned worktrees.json.lock directory.',\n );\n await setTimeout(LOCK_RETRY_MS, undefined, { signal });\n }\n }\n try {\n fs.writeFileSync(\n path.join(lock, 'owner.json'),\n JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }),\n { mode: 0o600 },\n );\n signal?.throwIfAborted();\n return await operation();\n } finally {\n fs.rmSync(lock, { recursive: true });\n }\n },\n };\n}\n"],"mappings":";;;;;;AAaA,MAAM,eAAe;AACrB,MAAM,gBAAgB;AAEtB,SAAS,SAAS,OAAyC;CACzD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,SAAS;CACf,OACE,OAAO,YAAA,KACP,OAAO,OAAO,OAAO,YACrB,OAAO,OAAO,MACd,OAAO,OAAO,SAAS,YACvB,OAAO,OAAO,WAAW,YACzB,OAAO,OAAO,cAAc;AAEhC;;;;;;AAOA,SAAgB,uBAAuB,MAAqC;CAC1E,OAAO;EACL,OAAO;GACL,MAAM,SAAS,SAA+B,IAAI;GAClD,IAAI,CAAC,UAAU,OAAO,YAAA,KAAuC,CAAC,MAAM,QAAQ,OAAO,OAAO,GAAG,OAAO,CAAC;GACrG,OAAO,OAAO,QAAQ,OAAO,QAAQ;EACvC;EACA,QAAQ,SAAS;GACf,gBAAgB,MAAM;IAAE,SAAA;IAAkC;GAAQ,CAAgC;EACpG;EACA,MAAM,YAAY,WAAW,QAAQ;GACnC,MAAM,OAAO,GAAG,KAAK;GACrB,GAAG,UAAU,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;GACpD,MAAM,WAAW,KAAK,IAAI,IAAI;GAC9B,SAAS;IACP,QAAQ,eAAe;IACvB,IAAI;KACF,GAAG,UAAU,MAAM,EAAE,MAAM,IAAM,CAAC;KAClC;IACF,SAAS,OAAO;KACd,IAAK,MAAgC,SAAS,UAAU,MAAM;KAC9D,IAAI,KAAK,IAAI,KAAK,UAChB,MAAM,IAAI,qBACR,iBACA,6DACA,MACA,8GACF;KACF,MAAM,WAAW,eAAe,KAAA,GAAW,EAAE,OAAO,CAAC;IACvD;GACF;GACA,IAAI;IACF,GAAG,cACD,KAAK,KAAK,MAAM,YAAY,GAC5B,KAAK,UAAU;KAAE,KAAK,QAAQ;KAAK,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;IAAE,CAAC,GACxE,EAAE,MAAM,IAAM,CAChB;IACA,QAAQ,eAAe;IACvB,OAAO,MAAM,UAAU;GACzB,UAAU;IACR,GAAG,OAAO,MAAM,EAAE,WAAW,KAAK,CAAC;GACrC;EACF;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webWorktrees.cjs","names":[],"sources":["../../../src/types/webWorktrees.ts"],"sourcesContent":["/**\n * The cockpit's view of this session's worktrees.\n *\n * DESIGN PATTERNS:\n * - Declarations only. The channel frame type is a constant so the hub and the\n * page cannot drift apart on a string.\n * - A view is the subset of a record the dock needs, not the record. The\n * registry may hold paths and provenance the page has no business rendering.\n *\n * AVOID:\n * - Adding behaviour here. This file is imported by browser code, so it must\n * stay free of node builtins.\n */\n\n/** The data channel carrying one session's worktrees to the page. */\nexport const GIT_WORKTREES_TYPE = 'git_worktrees';\n\n/** One worktree, as the activity dock and its panel render it. */\nexport interface WorktreeView {\n id: string;\n branch: string;\n path: string;\n /** The session the hub started for this worktree, when it is still live. */\n sessionId: string | null;\n /** True once the directory the record names has gone missing. */\n orphaned: boolean;\n /**\n * True when the session that asked for this worktree is gone.\n *\n * An owned worktree is only ever shown to its own session. This flag marks\n * the exception: once the parent is dead nobody would see the worktree at\n * all, so every session in the repository does, and may close it.\n */\n unowned: boolean;\n}\n\n/**\n * What the panel asks the hub channel to do.\n *\n * The dock acts through its own channel rather than through the agent: a\n * worktree made from the panel and one made by the tool take the same\n * `worktreeOperations` path, but the panel no longer has to spend a turn of\n * the conversation to get there.\n */\nexport type GitWorktreesCommand =\n | { action: 'create'; branch: string; baseRef?: string }\n | { action: 'close'; id: string; force?: boolean };\n\nexport type WorktreeErrorTarget = { action: 'create' } | { action: 'close'; id: string };\n\nexport interface GitWorktreesPayload {\n worktrees: WorktreeView[];\n /** Label of the operation in flight, absent when the session is idle. */\n pending?: string;\n /** The last failure, cleared when the next command starts. */\n error?: string;\n /** The operation that produced the last failure, when known. */\n errorTarget?: WorktreeErrorTarget;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,qBAAqB"}
|
|
1
|
+
{"version":3,"file":"webWorktrees.cjs","names":[],"sources":["../../../src/types/webWorktrees.ts"],"sourcesContent":["/**\n * The cockpit's view of this session's worktrees.\n *\n * DESIGN PATTERNS:\n * - Declarations only. The channel frame type is a constant so the hub and the\n * page cannot drift apart on a string.\n * - A view is the subset of a record the dock needs, not the record. The\n * registry may hold paths and provenance the page has no business rendering.\n *\n * AVOID:\n * - Adding behaviour here. This file is imported by browser code, so it must\n * stay free of node builtins.\n */\n\n/** The data channel carrying one session's worktrees to the page. */\nexport const GIT_WORKTREES_TYPE = 'git_worktrees';\n\n/** One worktree, as the activity dock and its panel render it. */\nexport interface WorktreeView {\n id: string;\n branch: string;\n path: string;\n /** The session the hub started for this worktree, when it is still live. */\n sessionId: string | null;\n /** True once the directory the record names has gone missing. */\n orphaned: boolean;\n /**\n * True when the session that asked for this worktree is gone.\n *\n * An owned worktree is only ever shown to its own session. This flag marks\n * the exception: once the parent is dead nobody would see the worktree at\n * all, so every session in the repository does, and may close it.\n */\n unowned: boolean;\n}\n\n/**\n * What the panel asks the hub channel to do.\n *\n * The dock acts through its own channel rather than through the agent: a\n * worktree made from the panel and one made by the tool take the same\n * `worktreeOperations` path, but the panel no longer has to spend a turn of\n * the conversation to get there.\n */\nexport type GitWorktreesCommand =\n | { action: 'create'; branch: string; baseRef?: string; reservationId?: string }\n | { action: 'close'; id: string; force?: boolean };\n\nexport type WorktreeErrorTarget = { action: 'create' } | { action: 'close'; id: string };\n\nexport interface GitWorktreesPayload {\n worktrees: WorktreeView[];\n /** Label of the operation in flight, absent when the session is idle. */\n pending?: string;\n /** The last failure, cleared when the next command starts. */\n error?: string;\n /** The operation that produced the last failure, when known. */\n errorTarget?: WorktreeErrorTarget;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,qBAAqB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webWorktrees.mjs","names":[],"sources":["../../../src/types/webWorktrees.ts"],"sourcesContent":["/**\n * The cockpit's view of this session's worktrees.\n *\n * DESIGN PATTERNS:\n * - Declarations only. The channel frame type is a constant so the hub and the\n * page cannot drift apart on a string.\n * - A view is the subset of a record the dock needs, not the record. The\n * registry may hold paths and provenance the page has no business rendering.\n *\n * AVOID:\n * - Adding behaviour here. This file is imported by browser code, so it must\n * stay free of node builtins.\n */\n\n/** The data channel carrying one session's worktrees to the page. */\nexport const GIT_WORKTREES_TYPE = 'git_worktrees';\n\n/** One worktree, as the activity dock and its panel render it. */\nexport interface WorktreeView {\n id: string;\n branch: string;\n path: string;\n /** The session the hub started for this worktree, when it is still live. */\n sessionId: string | null;\n /** True once the directory the record names has gone missing. */\n orphaned: boolean;\n /**\n * True when the session that asked for this worktree is gone.\n *\n * An owned worktree is only ever shown to its own session. This flag marks\n * the exception: once the parent is dead nobody would see the worktree at\n * all, so every session in the repository does, and may close it.\n */\n unowned: boolean;\n}\n\n/**\n * What the panel asks the hub channel to do.\n *\n * The dock acts through its own channel rather than through the agent: a\n * worktree made from the panel and one made by the tool take the same\n * `worktreeOperations` path, but the panel no longer has to spend a turn of\n * the conversation to get there.\n */\nexport type GitWorktreesCommand =\n | { action: 'create'; branch: string; baseRef?: string }\n | { action: 'close'; id: string; force?: boolean };\n\nexport type WorktreeErrorTarget = { action: 'create' } | { action: 'close'; id: string };\n\nexport interface GitWorktreesPayload {\n worktrees: WorktreeView[];\n /** Label of the operation in flight, absent when the session is idle. */\n pending?: string;\n /** The last failure, cleared when the next command starts. */\n error?: string;\n /** The operation that produced the last failure, when known. */\n errorTarget?: WorktreeErrorTarget;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,qBAAqB"}
|
|
1
|
+
{"version":3,"file":"webWorktrees.mjs","names":[],"sources":["../../../src/types/webWorktrees.ts"],"sourcesContent":["/**\n * The cockpit's view of this session's worktrees.\n *\n * DESIGN PATTERNS:\n * - Declarations only. The channel frame type is a constant so the hub and the\n * page cannot drift apart on a string.\n * - A view is the subset of a record the dock needs, not the record. The\n * registry may hold paths and provenance the page has no business rendering.\n *\n * AVOID:\n * - Adding behaviour here. This file is imported by browser code, so it must\n * stay free of node builtins.\n */\n\n/** The data channel carrying one session's worktrees to the page. */\nexport const GIT_WORKTREES_TYPE = 'git_worktrees';\n\n/** One worktree, as the activity dock and its panel render it. */\nexport interface WorktreeView {\n id: string;\n branch: string;\n path: string;\n /** The session the hub started for this worktree, when it is still live. */\n sessionId: string | null;\n /** True once the directory the record names has gone missing. */\n orphaned: boolean;\n /**\n * True when the session that asked for this worktree is gone.\n *\n * An owned worktree is only ever shown to its own session. This flag marks\n * the exception: once the parent is dead nobody would see the worktree at\n * all, so every session in the repository does, and may close it.\n */\n unowned: boolean;\n}\n\n/**\n * What the panel asks the hub channel to do.\n *\n * The dock acts through its own channel rather than through the agent: a\n * worktree made from the panel and one made by the tool take the same\n * `worktreeOperations` path, but the panel no longer has to spend a turn of\n * the conversation to get there.\n */\nexport type GitWorktreesCommand =\n | { action: 'create'; branch: string; baseRef?: string; reservationId?: string }\n | { action: 'close'; id: string; force?: boolean };\n\nexport type WorktreeErrorTarget = { action: 'create' } | { action: 'close'; id: string };\n\nexport interface GitWorktreesPayload {\n worktrees: WorktreeView[];\n /** Label of the operation in flight, absent when the session is idle. */\n pending?: string;\n /** The last failure, cleared when the next command starts. */\n error?: string;\n /** The operation that produced the last failure, when known. */\n errorTarget?: WorktreeErrorTarget;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,qBAAqB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-git",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.15",
|
|
4
4
|
"description": "Git worktree sessions for DoomPi: spawn an isolated worktree with its own session and manage it from the parent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -67,19 +67,19 @@
|
|
|
67
67
|
"access": "public"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@agimon-ai/doompi-core": "0.0.1-alpha.
|
|
71
|
-
"@agimon-ai/doompi-session": "0.0.1-alpha.
|
|
72
|
-
"@agimon-ai/doompi-web-components": "0.0.1-alpha.
|
|
70
|
+
"@agimon-ai/doompi-core": "0.0.1-alpha.79",
|
|
71
|
+
"@agimon-ai/doompi-session": "0.0.1-alpha.3",
|
|
72
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.37",
|
|
73
73
|
"@deepseek-ai/cordis": "4.0.2",
|
|
74
74
|
"@tanstack/react-store": "0.11.1",
|
|
75
75
|
"@tanstack/store": "0.11.1",
|
|
76
76
|
"typebox": "1.3.30"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@agimon-ai/doompi-build": "0.0.1-alpha.
|
|
80
|
-
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.
|
|
81
|
-
"@earendil-works/pi-agent-core": "0.
|
|
82
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
79
|
+
"@agimon-ai/doompi-build": "0.0.1-alpha.8",
|
|
80
|
+
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.74",
|
|
81
|
+
"@earendil-works/pi-agent-core": "0.86.0",
|
|
82
|
+
"@earendil-works/pi-coding-agent": "0.86.0",
|
|
83
83
|
"@types/node": "26.5.1",
|
|
84
84
|
"@types/react": "19.3.0",
|
|
85
85
|
"@vitest/coverage-v8": "5.0.0",
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
"vitest": "5.0.0"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"@earendil-works/pi-agent-core": "0.
|
|
94
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
93
|
+
"@earendil-works/pi-agent-core": "0.86.0",
|
|
94
|
+
"@earendil-works/pi-coding-agent": "0.86.0"
|
|
95
95
|
},
|
|
96
96
|
"peerDependenciesMeta": {
|
|
97
97
|
"@earendil-works/pi-agent-core": {
|