@agimon-ai/doompi-git 0.0.1-alpha.12 → 0.0.1-alpha.13

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/README.md CHANGED
@@ -70,4 +70,4 @@ Maintained by [Agimon](https://agimon.ai/about).
70
70
 
71
71
  MIT
72
72
 
73
- Named Pi and server factories live in `src/extensions`. Controllers declare commands and APIs; services own worktree operations and filesystem persistence; tools return typed Pi declarations. Public services and types are exposed through flat `src/exports`. The server factory owns the worktree message inbox and closes it through `onDispose`.
73
+ Named Pi and server factories live in `src/extensions`. Controllers declare commands and APIs; services own worktree operations and filesystem persistence; tools return typed Pi declarations. Public services and types are exposed through flat `src/exports`. In composed DoomPi sessions, the fixed Session foundation durably admits `spawn_worktree.task`, parent messages, and child reports while waking the recipient. The prior in-memory inbox remains only as a standalone compatibility fallback.
@@ -5,6 +5,7 @@ const require_runWorktreeTool = require("../../../../../schemas/runWorktreeTool.
5
5
  const require_index$3 = require("../../../../../services/runWorktree/index.cjs");
6
6
  let _agimon_ai_doompi_core_extension_file = require("@agimon-ai/doompi-core/extension-file");
7
7
  let _agimon_ai_doompi_core_server_facet = require("@agimon-ai/doompi-core/server-facet");
8
+ let _agimon_ai_doompi_session = require("@agimon-ai/doompi-session");
8
9
  //#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
9
10
  function progressResult(action, label) {
10
11
  return {
@@ -15,7 +16,7 @@ function progressResult(action, label) {
15
16
  details: { action }
16
17
  };
17
18
  }
18
- function createGitSession({ host: serverHost }) {
19
+ function createGitSession({ context, host: serverHost }) {
19
20
  if (serverHost.context.directEvents === void 0) throw new Error("Git headless facet requires the session direct event bus.");
20
21
  if (serverHost.context.sessionId === void 0) throw new Error("Git headless facet requires a session identity.");
21
22
  const sessionId = serverHost.context.sessionId;
@@ -24,7 +25,8 @@ function createGitSession({ host: serverHost }) {
24
25
  const operations = require_index$2.createWorktreeOperations({
25
26
  git: require_index.createWorktreeGit(),
26
27
  sessionService: serverHost.context.sessionService,
27
- messageInbox
28
+ messageInbox,
29
+ sessionDelivery: () => (0, _agimon_ai_doompi_session.readDoomSessionDelivery)(context)
28
30
  });
29
31
  return {
30
32
  value: {
@@ -1 +1 @@
1
- {"version":3,"file":"root.server.cjs","names":["createWorktreeMessageInbox","createWorktreeOperations","createWorktreeGit","readPackageResource","RUN_WORKTREE_TOOL_NAME","RUN_WORKTREE_DESCRIPTION","RunWorktreeToolSchema","validateParams","executeRunWorktreeTool","defineRoot"],"sources":["../../../../../../../src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts"],"sourcesContent":["import { defineRoot } from '@agimon-ai/doompi-core/extension-file';\nimport {\n type DoomHeadlessResource,\n type DoomHeadlessTool,\n type DoomHeadlessToolResult,\n} from '@agimon-ai/doompi-core/headless';\nimport { readPackageResource, type DoomServerPluginContext } from '@agimon-ai/doompi-core/server-facet';\n\nimport { RunWorktreeToolSchema, type RunWorktreeToolParams } from '../../../../../schemas/runWorktreeTool';\nimport { createWorktreeGit } from '../../../../../services/gitCli';\nimport {\n executeRunWorktreeTool,\n RUN_WORKTREE_DESCRIPTION,\n RUN_WORKTREE_TOOL_NAME,\n validateParams,\n} from '../../../../../services/runWorktree';\nimport { createWorktreeMessageInbox } from '../../../../../services/worktreeEvents';\nimport { createWorktreeOperations } from '../../../../../services/worktreeOperations';\n\nfunction progressResult(action: RunWorktreeToolParams['action'], label: string): DoomHeadlessToolResult {\n return { content: [{ type: 'text', text: label }], details: { action } };\n}\n\nexport function createGitSession({ host: serverHost }: DoomServerPluginContext) {\n if (serverHost.context.directEvents === undefined)\n throw new Error('Git headless facet requires the session direct event bus.');\n if (serverHost.context.sessionId === undefined) throw new Error('Git headless facet requires a session identity.');\n const sessionId = serverHost.context.sessionId;\n const directEvents = serverHost.context.directEvents;\n const messageInbox = createWorktreeMessageInbox(directEvents, sessionId);\n const operations = createWorktreeOperations({\n git: createWorktreeGit(),\n sessionService: serverHost.context.sessionService,\n messageInbox,\n });\n const resources: DoomHeadlessResource[] = [\n {\n // The package index is Help-catalog material, not standing instruction, so it\n // only enters the prompt while Help mode is active. Matches the Pi facet,\n // which has always routed this through the Help service.\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n name: 'doompi-git',\n kind: 'context',\n read: () => readPackageResource(import.meta.url, 'llms.txt'),\n },\n {\n name: 'doompi-use-git',\n kind: 'skill',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-git/SKILL.md'),\n },\n ];\n\n const tool: DoomHeadlessTool<typeof RunWorktreeToolSchema> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: RUN_WORKTREE_DESCRIPTION,\n parameters: RunWorktreeToolSchema,\n promptSnippet: 'Create and manage Git worktree sessions',\n executionMode: 'serial',\n async execute(_toolCallId, parameters, signal, onUpdate, execution) {\n const params = validateParams(parameters) as RunWorktreeToolParams;\n return executeRunWorktreeTool(params, operations, execution, {\n ...(signal === undefined ? {} : { signal }),\n onProgress: (label) => onUpdate?.(progressResult(params.action, label)),\n });\n },\n };\n return { value: { resources, tool }, onDispose: () => messageInbox.close() };\n}\n\nconst root = defineRoot(createGitSession);\nexport type GitServerScope = Awaited<ReturnType<typeof root>>['value'];\nexport default root;\n"],"mappings":";;;;;;;;AAmBA,SAAS,eAAe,QAAyC,OAAuC;CACtG,OAAO;EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM;EAAM,CAAC;EAAG,SAAS,EAAE,OAAO;CAAE;AACzE;AAEA,SAAgB,iBAAiB,EAAE,MAAM,cAAuC;CAC9E,IAAI,WAAW,QAAQ,iBAAiB,KAAA,GACtC,MAAM,IAAI,MAAM,2DAA2D;CAC7E,IAAI,WAAW,QAAQ,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,iDAAiD;CACjH,MAAM,YAAY,WAAW,QAAQ;CACrC,MAAM,eAAe,WAAW,QAAQ;CACxC,MAAM,eAAeA,gBAAAA,2BAA2B,cAAc,SAAS;CACvE,MAAM,aAAaC,gBAAAA,yBAAyB;EAC1C,KAAKC,cAAAA,kBAAkB;EACvB,gBAAgB,WAAW,QAAQ;EACnC;CACF,CAAC;CAiCD,OAAO;EAAE,OAAO;GAAE,WAAA,CA/BhB;IAIE,MAAM;KAAE,OAAO,EAAE,cAAc,OAAO;KAAG,aAAa;MAAE,MAAM;MAAS,MAAM;KAAO;IAAE;IACtF,MAAM;IACN,MAAM;IACN,aAAA,GAAYC,oCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,UAAU;GAC7D,GACA;IACE,MAAM;IACN,MAAM;IACN,aAAA,GAAYA,oCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,qCAAqC;GACxF,CAkBwB;GAAG,MAAA;IAd3B,MAAMC,gBAAAA;IACN,OAAO;IACP,aAAaC,gBAAAA;IACb,YAAYC,wBAAAA;IACZ,eAAe;IACf,eAAe;IACf,MAAM,QAAQ,aAAa,YAAY,QAAQ,UAAU,WAAW;KAClE,MAAM,SAASC,gBAAAA,eAAe,UAAU;KACxC,OAAOC,gBAAAA,uBAAuB,QAAQ,YAAY,WAAW;MAC3D,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;MACzC,aAAa,UAAU,WAAW,eAAe,OAAO,QAAQ,KAAK,CAAC;KACxE,CAAC;IACH;GAE8B;EAAE;EAAG,iBAAiB,aAAa,MAAM;CAAE;AAC7E;AAEA,MAAM,QAAA,GAAOC,sCAAAA,WAAAA,CAAW,gBAAgB"}
1
+ {"version":3,"file":"root.server.cjs","names":["createWorktreeMessageInbox","createWorktreeOperations","createWorktreeGit","readDoomSessionDelivery","readPackageResource","RUN_WORKTREE_TOOL_NAME","RUN_WORKTREE_DESCRIPTION","RunWorktreeToolSchema","validateParams","executeRunWorktreeTool","defineRoot"],"sources":["../../../../../../../src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts"],"sourcesContent":["import { defineRoot } from '@agimon-ai/doompi-core/extension-file';\nimport {\n type DoomHeadlessResource,\n type DoomHeadlessTool,\n type DoomHeadlessToolResult,\n} from '@agimon-ai/doompi-core/headless';\nimport { readPackageResource, type DoomServerPluginContext } from '@agimon-ai/doompi-core/server-facet';\nimport { readDoomSessionDelivery } from '@agimon-ai/doompi-session';\n\nimport { RunWorktreeToolSchema, type RunWorktreeToolParams } from '../../../../../schemas/runWorktreeTool';\nimport { createWorktreeGit } from '../../../../../services/gitCli';\nimport {\n executeRunWorktreeTool,\n RUN_WORKTREE_DESCRIPTION,\n RUN_WORKTREE_TOOL_NAME,\n validateParams,\n} from '../../../../../services/runWorktree';\nimport { createWorktreeMessageInbox } from '../../../../../services/worktreeEvents';\nimport { createWorktreeOperations } from '../../../../../services/worktreeOperations';\n\nfunction progressResult(action: RunWorktreeToolParams['action'], label: string): DoomHeadlessToolResult {\n return { content: [{ type: 'text', text: label }], details: { action } };\n}\n\nexport function createGitSession({ context, host: serverHost }: DoomServerPluginContext) {\n if (serverHost.context.directEvents === undefined)\n throw new Error('Git headless facet requires the session direct event bus.');\n if (serverHost.context.sessionId === undefined) throw new Error('Git headless facet requires a session identity.');\n const sessionId = serverHost.context.sessionId;\n const directEvents = serverHost.context.directEvents;\n const messageInbox = createWorktreeMessageInbox(directEvents, sessionId);\n const operations = createWorktreeOperations({\n git: createWorktreeGit(),\n sessionService: serverHost.context.sessionService,\n messageInbox,\n sessionDelivery: () => readDoomSessionDelivery(context),\n });\n const resources: DoomHeadlessResource[] = [\n {\n // The package index is Help-catalog material, not standing instruction, so it\n // only enters the prompt while Help mode is active. Matches the Pi facet,\n // which has always routed this through the Help service.\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n name: 'doompi-git',\n kind: 'context',\n read: () => readPackageResource(import.meta.url, 'llms.txt'),\n },\n {\n name: 'doompi-use-git',\n kind: 'skill',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-git/SKILL.md'),\n },\n ];\n\n const tool: DoomHeadlessTool<typeof RunWorktreeToolSchema> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: RUN_WORKTREE_DESCRIPTION,\n parameters: RunWorktreeToolSchema,\n promptSnippet: 'Create and manage Git worktree sessions',\n executionMode: 'serial',\n async execute(_toolCallId, parameters, signal, onUpdate, execution) {\n const params = validateParams(parameters) as RunWorktreeToolParams;\n return executeRunWorktreeTool(params, operations, execution, {\n ...(signal === undefined ? {} : { signal }),\n onProgress: (label) => onUpdate?.(progressResult(params.action, label)),\n });\n },\n };\n return { value: { resources, tool }, onDispose: () => messageInbox.close() };\n}\n\nconst root = defineRoot(createGitSession);\nexport type GitServerScope = Awaited<ReturnType<typeof root>>['value'];\nexport default root;\n"],"mappings":";;;;;;;;;AAoBA,SAAS,eAAe,QAAyC,OAAuC;CACtG,OAAO;EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM;EAAM,CAAC;EAAG,SAAS,EAAE,OAAO;CAAE;AACzE;AAEA,SAAgB,iBAAiB,EAAE,SAAS,MAAM,cAAuC;CACvF,IAAI,WAAW,QAAQ,iBAAiB,KAAA,GACtC,MAAM,IAAI,MAAM,2DAA2D;CAC7E,IAAI,WAAW,QAAQ,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,iDAAiD;CACjH,MAAM,YAAY,WAAW,QAAQ;CACrC,MAAM,eAAe,WAAW,QAAQ;CACxC,MAAM,eAAeA,gBAAAA,2BAA2B,cAAc,SAAS;CACvE,MAAM,aAAaC,gBAAAA,yBAAyB;EAC1C,KAAKC,cAAAA,kBAAkB;EACvB,gBAAgB,WAAW,QAAQ;EACnC;EACA,wBAAA,GAAuBC,0BAAAA,wBAAAA,CAAwB,OAAO;CACxD,CAAC;CAiCD,OAAO;EAAE,OAAO;GAAE,WAAA,CA/BhB;IAIE,MAAM;KAAE,OAAO,EAAE,cAAc,OAAO;KAAG,aAAa;MAAE,MAAM;MAAS,MAAM;KAAO;IAAE;IACtF,MAAM;IACN,MAAM;IACN,aAAA,GAAYC,oCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,UAAU;GAC7D,GACA;IACE,MAAM;IACN,MAAM;IACN,aAAA,GAAYA,oCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,qCAAqC;GACxF,CAkBwB;GAAG,MAAA;IAd3B,MAAMC,gBAAAA;IACN,OAAO;IACP,aAAaC,gBAAAA;IACb,YAAYC,wBAAAA;IACZ,eAAe;IACf,eAAe;IACf,MAAM,QAAQ,aAAa,YAAY,QAAQ,UAAU,WAAW;KAClE,MAAM,SAASC,gBAAAA,eAAe,UAAU;KACxC,OAAOC,gBAAAA,uBAAuB,QAAQ,YAAY,WAAW;MAC3D,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;MACzC,aAAa,UAAU,WAAW,eAAe,OAAO,QAAQ,KAAK,CAAC;KACxE,CAAC;IACH;GAE8B;EAAE;EAAG,iBAAiB,aAAa,MAAM;CAAE;AAC7E;AAEA,MAAM,QAAA,GAAOC,sCAAAA,WAAAA,CAAW,gBAAgB"}
@@ -5,6 +5,7 @@ import { RunWorktreeToolSchema } from "../../../../../schemas/runWorktreeTool.mj
5
5
  import { RUN_WORKTREE_DESCRIPTION, RUN_WORKTREE_TOOL_NAME, executeRunWorktreeTool, validateParams } from "../../../../../services/runWorktree/index.mjs";
6
6
  import { defineRoot } from "@agimon-ai/doompi-core/extension-file";
7
7
  import { readPackageResource } from "@agimon-ai/doompi-core/server-facet";
8
+ import { readDoomSessionDelivery } from "@agimon-ai/doompi-session";
8
9
  //#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
9
10
  function progressResult(action, label) {
10
11
  return {
@@ -15,7 +16,7 @@ function progressResult(action, label) {
15
16
  details: { action }
16
17
  };
17
18
  }
18
- function createGitSession({ host: serverHost }) {
19
+ function createGitSession({ context, host: serverHost }) {
19
20
  if (serverHost.context.directEvents === void 0) throw new Error("Git headless facet requires the session direct event bus.");
20
21
  if (serverHost.context.sessionId === void 0) throw new Error("Git headless facet requires a session identity.");
21
22
  const sessionId = serverHost.context.sessionId;
@@ -24,7 +25,8 @@ function createGitSession({ host: serverHost }) {
24
25
  const operations = createWorktreeOperations({
25
26
  git: createWorktreeGit(),
26
27
  sessionService: serverHost.context.sessionService,
27
- messageInbox
28
+ messageInbox,
29
+ sessionDelivery: () => readDoomSessionDelivery(context)
28
30
  });
29
31
  return {
30
32
  value: {
@@ -1 +1 @@
1
- {"version":3,"file":"root.server.mjs","names":[],"sources":["../../../../../../../src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts"],"sourcesContent":["import { defineRoot } from '@agimon-ai/doompi-core/extension-file';\nimport {\n type DoomHeadlessResource,\n type DoomHeadlessTool,\n type DoomHeadlessToolResult,\n} from '@agimon-ai/doompi-core/headless';\nimport { readPackageResource, type DoomServerPluginContext } from '@agimon-ai/doompi-core/server-facet';\n\nimport { RunWorktreeToolSchema, type RunWorktreeToolParams } from '../../../../../schemas/runWorktreeTool';\nimport { createWorktreeGit } from '../../../../../services/gitCli';\nimport {\n executeRunWorktreeTool,\n RUN_WORKTREE_DESCRIPTION,\n RUN_WORKTREE_TOOL_NAME,\n validateParams,\n} from '../../../../../services/runWorktree';\nimport { createWorktreeMessageInbox } from '../../../../../services/worktreeEvents';\nimport { createWorktreeOperations } from '../../../../../services/worktreeOperations';\n\nfunction progressResult(action: RunWorktreeToolParams['action'], label: string): DoomHeadlessToolResult {\n return { content: [{ type: 'text', text: label }], details: { action } };\n}\n\nexport function createGitSession({ host: serverHost }: DoomServerPluginContext) {\n if (serverHost.context.directEvents === undefined)\n throw new Error('Git headless facet requires the session direct event bus.');\n if (serverHost.context.sessionId === undefined) throw new Error('Git headless facet requires a session identity.');\n const sessionId = serverHost.context.sessionId;\n const directEvents = serverHost.context.directEvents;\n const messageInbox = createWorktreeMessageInbox(directEvents, sessionId);\n const operations = createWorktreeOperations({\n git: createWorktreeGit(),\n sessionService: serverHost.context.sessionService,\n messageInbox,\n });\n const resources: DoomHeadlessResource[] = [\n {\n // The package index is Help-catalog material, not standing instruction, so it\n // only enters the prompt while Help mode is active. Matches the Pi facet,\n // which has always routed this through the Help service.\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n name: 'doompi-git',\n kind: 'context',\n read: () => readPackageResource(import.meta.url, 'llms.txt'),\n },\n {\n name: 'doompi-use-git',\n kind: 'skill',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-git/SKILL.md'),\n },\n ];\n\n const tool: DoomHeadlessTool<typeof RunWorktreeToolSchema> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: RUN_WORKTREE_DESCRIPTION,\n parameters: RunWorktreeToolSchema,\n promptSnippet: 'Create and manage Git worktree sessions',\n executionMode: 'serial',\n async execute(_toolCallId, parameters, signal, onUpdate, execution) {\n const params = validateParams(parameters) as RunWorktreeToolParams;\n return executeRunWorktreeTool(params, operations, execution, {\n ...(signal === undefined ? {} : { signal }),\n onProgress: (label) => onUpdate?.(progressResult(params.action, label)),\n });\n },\n };\n return { value: { resources, tool }, onDispose: () => messageInbox.close() };\n}\n\nconst root = defineRoot(createGitSession);\nexport type GitServerScope = Awaited<ReturnType<typeof root>>['value'];\nexport default root;\n"],"mappings":";;;;;;;;AAmBA,SAAS,eAAe,QAAyC,OAAuC;CACtG,OAAO;EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM;EAAM,CAAC;EAAG,SAAS,EAAE,OAAO;CAAE;AACzE;AAEA,SAAgB,iBAAiB,EAAE,MAAM,cAAuC;CAC9E,IAAI,WAAW,QAAQ,iBAAiB,KAAA,GACtC,MAAM,IAAI,MAAM,2DAA2D;CAC7E,IAAI,WAAW,QAAQ,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,iDAAiD;CACjH,MAAM,YAAY,WAAW,QAAQ;CACrC,MAAM,eAAe,WAAW,QAAQ;CACxC,MAAM,eAAe,2BAA2B,cAAc,SAAS;CACvE,MAAM,aAAa,yBAAyB;EAC1C,KAAK,kBAAkB;EACvB,gBAAgB,WAAW,QAAQ;EACnC;CACF,CAAC;CAiCD,OAAO;EAAE,OAAO;GAAE,WAAA,CA/BhB;IAIE,MAAM;KAAE,OAAO,EAAE,cAAc,OAAO;KAAG,aAAa;MAAE,MAAM;MAAS,MAAM;KAAO;IAAE;IACtF,MAAM;IACN,MAAM;IACN,YAAY,oBAAoB,YAAY,KAAK,UAAU;GAC7D,GACA;IACE,MAAM;IACN,MAAM;IACN,YAAY,oBAAoB,YAAY,KAAK,qCAAqC;GACxF,CAkBwB;GAAG,MAAA;IAd3B,MAAM;IACN,OAAO;IACP,aAAa;IACb,YAAY;IACZ,eAAe;IACf,eAAe;IACf,MAAM,QAAQ,aAAa,YAAY,QAAQ,UAAU,WAAW;KAClE,MAAM,SAAS,eAAe,UAAU;KACxC,OAAO,uBAAuB,QAAQ,YAAY,WAAW;MAC3D,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;MACzC,aAAa,UAAU,WAAW,eAAe,OAAO,QAAQ,KAAK,CAAC;KACxE,CAAC;IACH;GAE8B;EAAE;EAAG,iBAAiB,aAAa,MAAM;CAAE;AAC7E;AAEA,MAAM,OAAO,WAAW,gBAAgB"}
1
+ {"version":3,"file":"root.server.mjs","names":[],"sources":["../../../../../../../src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts"],"sourcesContent":["import { defineRoot } from '@agimon-ai/doompi-core/extension-file';\nimport {\n type DoomHeadlessResource,\n type DoomHeadlessTool,\n type DoomHeadlessToolResult,\n} from '@agimon-ai/doompi-core/headless';\nimport { readPackageResource, type DoomServerPluginContext } from '@agimon-ai/doompi-core/server-facet';\nimport { readDoomSessionDelivery } from '@agimon-ai/doompi-session';\n\nimport { RunWorktreeToolSchema, type RunWorktreeToolParams } from '../../../../../schemas/runWorktreeTool';\nimport { createWorktreeGit } from '../../../../../services/gitCli';\nimport {\n executeRunWorktreeTool,\n RUN_WORKTREE_DESCRIPTION,\n RUN_WORKTREE_TOOL_NAME,\n validateParams,\n} from '../../../../../services/runWorktree';\nimport { createWorktreeMessageInbox } from '../../../../../services/worktreeEvents';\nimport { createWorktreeOperations } from '../../../../../services/worktreeOperations';\n\nfunction progressResult(action: RunWorktreeToolParams['action'], label: string): DoomHeadlessToolResult {\n return { content: [{ type: 'text', text: label }], details: { action } };\n}\n\nexport function createGitSession({ context, host: serverHost }: DoomServerPluginContext) {\n if (serverHost.context.directEvents === undefined)\n throw new Error('Git headless facet requires the session direct event bus.');\n if (serverHost.context.sessionId === undefined) throw new Error('Git headless facet requires a session identity.');\n const sessionId = serverHost.context.sessionId;\n const directEvents = serverHost.context.directEvents;\n const messageInbox = createWorktreeMessageInbox(directEvents, sessionId);\n const operations = createWorktreeOperations({\n git: createWorktreeGit(),\n sessionService: serverHost.context.sessionService,\n messageInbox,\n sessionDelivery: () => readDoomSessionDelivery(context),\n });\n const resources: DoomHeadlessResource[] = [\n {\n // The package index is Help-catalog material, not standing instruction, so it\n // only enters the prompt while Help mode is active. Matches the Pi facet,\n // which has always routed this through the Help service.\n when: { state: { 'minor-mode': 'help' }, attribution: { kind: 'minor', mode: 'help' } },\n name: 'doompi-git',\n kind: 'context',\n read: () => readPackageResource(import.meta.url, 'llms.txt'),\n },\n {\n name: 'doompi-use-git',\n kind: 'skill',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-git/SKILL.md'),\n },\n ];\n\n const tool: DoomHeadlessTool<typeof RunWorktreeToolSchema> = {\n name: RUN_WORKTREE_TOOL_NAME,\n label: 'Worktree',\n description: RUN_WORKTREE_DESCRIPTION,\n parameters: RunWorktreeToolSchema,\n promptSnippet: 'Create and manage Git worktree sessions',\n executionMode: 'serial',\n async execute(_toolCallId, parameters, signal, onUpdate, execution) {\n const params = validateParams(parameters) as RunWorktreeToolParams;\n return executeRunWorktreeTool(params, operations, execution, {\n ...(signal === undefined ? {} : { signal }),\n onProgress: (label) => onUpdate?.(progressResult(params.action, label)),\n });\n },\n };\n return { value: { resources, tool }, onDispose: () => messageInbox.close() };\n}\n\nconst root = defineRoot(createGitSession);\nexport type GitServerScope = Awaited<ReturnType<typeof root>>['value'];\nexport default root;\n"],"mappings":";;;;;;;;;AAoBA,SAAS,eAAe,QAAyC,OAAuC;CACtG,OAAO;EAAE,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM;EAAM,CAAC;EAAG,SAAS,EAAE,OAAO;CAAE;AACzE;AAEA,SAAgB,iBAAiB,EAAE,SAAS,MAAM,cAAuC;CACvF,IAAI,WAAW,QAAQ,iBAAiB,KAAA,GACtC,MAAM,IAAI,MAAM,2DAA2D;CAC7E,IAAI,WAAW,QAAQ,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,iDAAiD;CACjH,MAAM,YAAY,WAAW,QAAQ;CACrC,MAAM,eAAe,WAAW,QAAQ;CACxC,MAAM,eAAe,2BAA2B,cAAc,SAAS;CACvE,MAAM,aAAa,yBAAyB;EAC1C,KAAK,kBAAkB;EACvB,gBAAgB,WAAW,QAAQ;EACnC;EACA,uBAAuB,wBAAwB,OAAO;CACxD,CAAC;CAiCD,OAAO;EAAE,OAAO;GAAE,WAAA,CA/BhB;IAIE,MAAM;KAAE,OAAO,EAAE,cAAc,OAAO;KAAG,aAAa;MAAE,MAAM;MAAS,MAAM;KAAO;IAAE;IACtF,MAAM;IACN,MAAM;IACN,YAAY,oBAAoB,YAAY,KAAK,UAAU;GAC7D,GACA;IACE,MAAM;IACN,MAAM;IACN,YAAY,oBAAoB,YAAY,KAAK,qCAAqC;GACxF,CAkBwB;GAAG,MAAA;IAd3B,MAAM;IACN,OAAO;IACP,aAAa;IACb,YAAY;IACZ,eAAe;IACf,eAAe;IACf,MAAM,QAAQ,aAAa,YAAY,QAAQ,UAAU,WAAW;KAClE,MAAM,SAAS,eAAe,UAAU;KACxC,OAAO,uBAAuB,QAAQ,YAAY,WAAW;MAC3D,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;MACzC,aAAa,UAAU,WAAW,eAAe,OAAO,QAAQ,KAAK,CAAC;KACxE,CAAC;IACH;GAE8B;EAAE;EAAG,iBAAiB,aAAa,MAAM;CAAE;AAC7E;AAEA,MAAM,OAAO,WAAW,gBAAgB"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../../src/services/errors/index.ts"],"sourcesContent":["/**\n * Expected failures, kept private to this package.\n *\n * `@agimon-ai/doompi-core` does not export `invalidRequest` or\n * an expected-error base, so this mirrors doompi-team's shape rather than\n * reaching across a layer boundary for it. The duplication is deliberate and\n * small; a shared helper would be the better answer only once a third package\n * needs one.\n */\n\nexport type DoomGitErrorCode =\n | 'invalid_request'\n | 'not_a_repository'\n | 'worktree_exists'\n | 'worktree_not_found'\n | 'worktree_not_owned'\n | 'worktree_peer_unavailable'\n | 'worktree_dirty'\n | 'hub_unavailable'\n | 'spawn_cancelled'\n | 'registry_write_failed'\n | 'message_too_large'\n | 'git_failed';\n\nexport class DoomGitExpectedError extends Error {\n constructor(\n readonly code: DoomGitErrorCode,\n message: string,\n readonly retryable: boolean,\n readonly recovery: string,\n ) {\n // The recovery line rides in the message because a tool result is read as\n // text: an error the model cannot act on costs a turn to rediscover.\n super(`[${code}] ${message}\\nRecovery: ${recovery}`);\n this.name = 'DoomGitExpectedError';\n }\n}\n\n/** Raised when a worktree action needs the canonical hub but it is not injected. */\nexport class HubUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'HubUnavailableError';\n }\n}\n\nexport function invalidRequest(message: string, recovery: string): DoomGitExpectedError {\n return new DoomGitExpectedError('invalid_request', message, false, recovery);\n}\n"],"mappings":";AAwBA,IAAa,uBAAb,cAA0C,MAAM;CAEnC;CAEA;CACA;CAJX,YACE,MACA,SACA,WACA,UACA;EAGA,MAAM,IAAI,KAAK,IAAI,QAAQ,cAAc,UAAU;EAP1C,KAAA,OAAA;EAEA,KAAA,YAAA;EACA,KAAA,WAAA;EAKT,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,eAAe,SAAiB,UAAwC;CACtF,OAAO,IAAI,qBAAqB,mBAAmB,SAAS,OAAO,QAAQ;AAC7E"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../../src/services/errors/index.ts"],"sourcesContent":["/**\n * Expected failures, kept private to this package.\n *\n * `@agimon-ai/doompi-core` does not export `invalidRequest` or\n * an expected-error base, so this mirrors doompi-team's shape rather than\n * reaching across a layer boundary for it. The duplication is deliberate and\n * small; a shared helper would be the better answer only once a third package\n * needs one.\n */\n\nexport type DoomGitErrorCode =\n | 'invalid_request'\n | 'not_a_repository'\n | 'worktree_exists'\n | 'worktree_not_found'\n | 'worktree_not_owned'\n | 'worktree_peer_unavailable'\n | 'worktree_dirty'\n | 'hub_unavailable'\n | 'spawn_cancelled'\n | 'registry_write_failed'\n | 'task_delivery_failed'\n | 'message_delivery_failed'\n | 'message_too_large'\n | 'git_failed';\n\nexport class DoomGitExpectedError extends Error {\n constructor(\n readonly code: DoomGitErrorCode,\n message: string,\n readonly retryable: boolean,\n readonly recovery: string,\n ) {\n // The recovery line rides in the message because a tool result is read as\n // text: an error the model cannot act on costs a turn to rediscover.\n super(`[${code}] ${message}\\nRecovery: ${recovery}`);\n this.name = 'DoomGitExpectedError';\n }\n}\n\n/** Raised when a worktree action needs the canonical hub but it is not injected. */\nexport class HubUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'HubUnavailableError';\n }\n}\n\nexport function invalidRequest(message: string, recovery: string): DoomGitExpectedError {\n return new DoomGitExpectedError('invalid_request', message, false, recovery);\n}\n"],"mappings":";AA0BA,IAAa,uBAAb,cAA0C,MAAM;CAEnC;CAEA;CACA;CAJX,YACE,MACA,SACA,WACA,UACA;EAGA,MAAM,IAAI,KAAK,IAAI,QAAQ,cAAc,UAAU;EAP1C,KAAA,OAAA;EAEA,KAAA,YAAA;EACA,KAAA,WAAA;EAKT,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,eAAe,SAAiB,UAAwC;CACtF,OAAO,IAAI,qBAAqB,mBAAmB,SAAS,OAAO,QAAQ;AAC7E"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/errors/index.ts"],"sourcesContent":["/**\n * Expected failures, kept private to this package.\n *\n * `@agimon-ai/doompi-core` does not export `invalidRequest` or\n * an expected-error base, so this mirrors doompi-team's shape rather than\n * reaching across a layer boundary for it. The duplication is deliberate and\n * small; a shared helper would be the better answer only once a third package\n * needs one.\n */\n\nexport type DoomGitErrorCode =\n | 'invalid_request'\n | 'not_a_repository'\n | 'worktree_exists'\n | 'worktree_not_found'\n | 'worktree_not_owned'\n | 'worktree_peer_unavailable'\n | 'worktree_dirty'\n | 'hub_unavailable'\n | 'spawn_cancelled'\n | 'registry_write_failed'\n | 'message_too_large'\n | 'git_failed';\n\nexport class DoomGitExpectedError extends Error {\n constructor(\n readonly code: DoomGitErrorCode,\n message: string,\n readonly retryable: boolean,\n readonly recovery: string,\n ) {\n // The recovery line rides in the message because a tool result is read as\n // text: an error the model cannot act on costs a turn to rediscover.\n super(`[${code}] ${message}\\nRecovery: ${recovery}`);\n this.name = 'DoomGitExpectedError';\n }\n}\n\n/** Raised when a worktree action needs the canonical hub but it is not injected. */\nexport class HubUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'HubUnavailableError';\n }\n}\n\nexport function invalidRequest(message: string, recovery: string): DoomGitExpectedError {\n return new DoomGitExpectedError('invalid_request', message, false, recovery);\n}\n"],"mappings":";AAwBA,IAAa,uBAAb,cAA0C,MAAM;CAEnC;CAEA;CACA;CAJX,YACE,MACA,SACA,WACA,UACA;EAGA,MAAM,IAAI,KAAK,IAAI,QAAQ,cAAc,UAAU;EAP1C,KAAA,OAAA;EAEA,KAAA,YAAA;EACA,KAAA,WAAA;EAKT,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,eAAe,SAAiB,UAAwC;CACtF,OAAO,IAAI,qBAAqB,mBAAmB,SAAS,OAAO,QAAQ;AAC7E"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/errors/index.ts"],"sourcesContent":["/**\n * Expected failures, kept private to this package.\n *\n * `@agimon-ai/doompi-core` does not export `invalidRequest` or\n * an expected-error base, so this mirrors doompi-team's shape rather than\n * reaching across a layer boundary for it. The duplication is deliberate and\n * small; a shared helper would be the better answer only once a third package\n * needs one.\n */\n\nexport type DoomGitErrorCode =\n | 'invalid_request'\n | 'not_a_repository'\n | 'worktree_exists'\n | 'worktree_not_found'\n | 'worktree_not_owned'\n | 'worktree_peer_unavailable'\n | 'worktree_dirty'\n | 'hub_unavailable'\n | 'spawn_cancelled'\n | 'registry_write_failed'\n | 'task_delivery_failed'\n | 'message_delivery_failed'\n | 'message_too_large'\n | 'git_failed';\n\nexport class DoomGitExpectedError extends Error {\n constructor(\n readonly code: DoomGitErrorCode,\n message: string,\n readonly retryable: boolean,\n readonly recovery: string,\n ) {\n // The recovery line rides in the message because a tool result is read as\n // text: an error the model cannot act on costs a turn to rediscover.\n super(`[${code}] ${message}\\nRecovery: ${recovery}`);\n this.name = 'DoomGitExpectedError';\n }\n}\n\n/** Raised when a worktree action needs the canonical hub but it is not injected. */\nexport class HubUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'HubUnavailableError';\n }\n}\n\nexport function invalidRequest(message: string, recovery: string): DoomGitExpectedError {\n return new DoomGitExpectedError('invalid_request', message, false, recovery);\n}\n"],"mappings":";AA0BA,IAAa,uBAAb,cAA0C,MAAM;CAEnC;CAEA;CACA;CAJX,YACE,MACA,SACA,WACA,UACA;EAGA,MAAM,IAAI,KAAK,IAAI,QAAQ,cAAc,UAAU;EAP1C,KAAA,OAAA;EAEA,KAAA,YAAA;EACA,KAAA,WAAA;EAKT,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AAEA,SAAgB,eAAe,SAAiB,UAAwC;CACtF,OAAO,IAAI,qBAAqB,mBAAmB,SAAS,OAAO,QAAQ;AAC7E"}
@@ -1,9 +1,13 @@
1
1
  const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
2
- const require_index = require("../repositoryIdentity/index.cjs");
2
+ const require_index = require("../atomicJson/index.cjs");
3
+ const require_index$1 = require("../repositoryIdentity/index.cjs");
4
+ let node_fs = require("node:fs");
5
+ node_fs = require_runtime.__toESM(node_fs, 1);
3
6
  let node_path = require("node:path");
4
7
  node_path = require_runtime.__toESM(node_path, 1);
5
8
  let node_os = require("node:os");
6
9
  node_os = require_runtime.__toESM(node_os, 1);
10
+ let node_util = require("node:util");
7
11
  require("@agimon-ai/doompi-core/web");
8
12
  //#region src/services/paths/index.ts
9
13
  /**
@@ -24,8 +28,35 @@ function worktreesRoot(homeDir = node_os.default.homedir()) {
24
28
  }
25
29
  /** One registry file per repository, keyed by the shared git directory. */
26
30
  function registryFile(repositoryRoot, homeDir = node_os.default.homedir()) {
27
- const key = `${require_index.repositoryLabel(repositoryRoot)}--${require_index.repositoryId(repositoryRoot)}`;
28
- return node_path.default.join(doomGitRoot(homeDir), "registry", key, "worktrees.json");
31
+ const id = require_index$1.repositoryId(repositoryRoot);
32
+ const root = node_path.default.join(doomGitRoot(homeDir), "registry");
33
+ const canonical = node_path.default.join(root, id, "worktrees.json");
34
+ if (node_fs.default.existsSync(canonical)) return canonical;
35
+ let names;
36
+ try {
37
+ names = node_fs.default.readdirSync(root);
38
+ } catch {
39
+ return canonical;
40
+ }
41
+ const legacyFiles = names.filter((entry) => entry.endsWith(`--${id}`)).sort().map((entry) => node_path.default.join(root, entry, "worktrees.json")).filter((file) => node_fs.default.existsSync(file));
42
+ if (legacyFiles.length === 0) return canonical;
43
+ const entries = /* @__PURE__ */ new Map();
44
+ for (const file of legacyFiles) {
45
+ const legacy = require_index.readJson(file);
46
+ if (legacy?.version !== 1 || !Array.isArray(legacy.entries)) continue;
47
+ for (const value of legacy.entries) {
48
+ if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || value.id === "") continue;
49
+ const entry = value;
50
+ const existing = entries.get(entry.id);
51
+ if (existing !== void 0 && !(0, node_util.isDeepStrictEqual)(existing, entry)) throw new Error(`Conflicting legacy worktree registry entries for '${entry.id}'.`);
52
+ entries.set(entry.id, entry);
53
+ }
54
+ }
55
+ require_index.writeJsonAtomic(canonical, {
56
+ version: 1,
57
+ entries: [...entries.values()]
58
+ });
59
+ return canonical;
29
60
  }
30
61
  //#endregion
31
62
  exports.doomGitRoot = doomGitRoot;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["os","path","repositoryLabel","repositoryId"],"sources":["../../../../src/services/paths/index.ts"],"sourcesContent":["import os from 'node:os';\nimport path from 'node:path';\n\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '@agimon-ai/doompi-core/web';\n\nimport { repositoryId, repositoryLabel } from '../repositoryIdentity';\n\nconst REGISTRY_DIR_FLAG = '--registry-dir';\n\n/**\n * Where this package keeps its own state.\n *\n * Outside the repository on purpose. A worktree directory inside the checkout\n * would be found by every glob the repository's own tooling runs, and would\n * resolve workspace imports against the parent checkout rather than the\n * worktree, so a session there would build the wrong source while appearing to\n * work.\n */\nexport function doomGitRoot(homeDir: string = os.homedir()): string {\n return path.join(homeDir, '.pi', '.doom', 'git');\n}\n\n/** The directory every worktree for every repository is created under. */\nexport function worktreesRoot(homeDir: string = os.homedir()): string {\n return path.join(doomGitRoot(homeDir), 'worktrees');\n}\n\n/** One registry file per repository, keyed by the shared git directory. */\n\nexport function registryFile(repositoryRoot: string, homeDir: string = os.homedir()): string {\n const key = `${repositoryLabel(repositoryRoot)}--${repositoryId(repositoryRoot)}`;\n return path.join(doomGitRoot(homeDir), 'registry', key, 'worktrees.json');\n}\n\n/**\n * The session registry directory this process's hub is watching.\n *\n * A spawned session only appears in the rail if it registers in the same\n * directory the hub watches, and the hub passes that directory to its children\n * as `--registry-dir` while also inheriting the env var. Recovering it from\n * this process's own argv and env, in the same precedence the hub and the\n * session server use, is what makes a worktree session visible rather than\n * silently invisible.\n */\nexport function hubRegistryDir(\n argv: readonly string[] = process.argv,\n env: NodeJS.ProcessEnv = process.env,\n homeDir: string = os.homedir(),\n): string {\n const index = argv.indexOf(REGISTRY_DIR_FLAG);\n const flagValue = index >= 0 ? argv[index + 1] : undefined;\n return resolveRegistryDir({\n ...(flagValue === undefined ? {} : { flagValue }),\n ...(env[REGISTRY_DIR_ENV] === undefined ? {} : { envValue: env[REGISTRY_DIR_ENV] }),\n homeDir,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,SAAgB,YAAY,UAAkBA,QAAAA,QAAG,QAAQ,GAAW;CAClE,OAAOC,UAAAA,QAAK,KAAK,SAAS,OAAO,SAAS,KAAK;AACjD;;AAGA,SAAgB,cAAc,UAAkBD,QAAAA,QAAG,QAAQ,GAAW;CACpE,OAAOC,UAAAA,QAAK,KAAK,YAAY,OAAO,GAAG,WAAW;AACpD;;AAIA,SAAgB,aAAa,gBAAwB,UAAkBD,QAAAA,QAAG,QAAQ,GAAW;CAC3F,MAAM,MAAM,GAAGE,cAAAA,gBAAgB,cAAc,EAAE,IAAIC,cAAAA,aAAa,cAAc;CAC9E,OAAOF,UAAAA,QAAK,KAAK,YAAY,OAAO,GAAG,YAAY,KAAK,gBAAgB;AAC1E"}
1
+ {"version":3,"file":"index.cjs","names":["os","path","repositoryId","fs","readJson","isDeepStrictEqual"],"sources":["../../../../src/services/paths/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { isDeepStrictEqual } from 'node:util';\n\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '@agimon-ai/doompi-core/web';\n\nimport { WORKTREE_RECORD_VERSION, type WorktreeRecord } from '../../types/worktreeRegistry';\nimport { readJson, writeJsonAtomic } from '../atomicJson';\nimport { repositoryId } from '../repositoryIdentity';\n\nconst REGISTRY_DIR_FLAG = '--registry-dir';\n\n/**\n * Where this package keeps its own state.\n *\n * Outside the repository on purpose. A worktree directory inside the checkout\n * would be found by every glob the repository's own tooling runs, and would\n * resolve workspace imports against the parent checkout rather than the\n * worktree, so a session there would build the wrong source while appearing to\n * work.\n */\nexport function doomGitRoot(homeDir: string = os.homedir()): string {\n return path.join(homeDir, '.pi', '.doom', 'git');\n}\n\n/** The directory every worktree for every repository is created under. */\nexport function worktreesRoot(homeDir: string = os.homedir()): string {\n return path.join(doomGitRoot(homeDir), 'worktrees');\n}\n\n/** One registry file per repository, keyed by the shared git directory. */\nexport function registryFile(repositoryRoot: string, homeDir: string = os.homedir()): string {\n const id = repositoryId(repositoryRoot);\n const root = path.join(doomGitRoot(homeDir), 'registry');\n const canonical = path.join(root, id, 'worktrees.json');\n if (fs.existsSync(canonical)) return canonical;\n let names: string[];\n try {\n names = fs.readdirSync(root);\n } catch {\n return canonical;\n }\n const legacyFiles = names\n .filter((entry) => entry.endsWith(`--${id}`))\n .sort()\n .map((entry) => path.join(root, entry, 'worktrees.json'))\n .filter((file) => fs.existsSync(file));\n if (legacyFiles.length === 0) return canonical;\n const entries = new Map<string, WorktreeRecord>();\n for (const file of legacyFiles) {\n const legacy = readJson<{ version?: unknown; entries?: unknown }>(file);\n if (legacy?.version !== WORKTREE_RECORD_VERSION || !Array.isArray(legacy.entries)) continue;\n for (const value of legacy.entries) {\n if (\n typeof value !== 'object' ||\n value === null ||\n !('id' in value) ||\n typeof value.id !== 'string' ||\n value.id === ''\n )\n continue;\n const entry = value as WorktreeRecord;\n const existing = entries.get(entry.id);\n if (existing !== undefined && !isDeepStrictEqual(existing, entry)) {\n throw new Error(`Conflicting legacy worktree registry entries for '${entry.id}'.`);\n }\n entries.set(entry.id, entry);\n }\n }\n writeJsonAtomic(canonical, { version: WORKTREE_RECORD_VERSION, entries: [...entries.values()] });\n return canonical;\n}\n\n/**\n * The session registry directory this process's hub is watching.\n *\n * A spawned session only appears in the rail if it registers in the same\n * directory the hub watches, and the hub passes that directory to its children\n * as `--registry-dir` while also inheriting the env var. Recovering it from\n * this process's own argv and env, in the same precedence the hub and the\n * session server use, is what makes a worktree session visible rather than\n * silently invisible.\n */\nexport function hubRegistryDir(\n argv: readonly string[] = process.argv,\n env: NodeJS.ProcessEnv = process.env,\n homeDir: string = os.homedir(),\n): string {\n const index = argv.indexOf(REGISTRY_DIR_FLAG);\n const flagValue = index >= 0 ? argv[index + 1] : undefined;\n return resolveRegistryDir({\n ...(flagValue === undefined ? {} : { flagValue }),\n ...(env[REGISTRY_DIR_ENV] === undefined ? {} : { envValue: env[REGISTRY_DIR_ENV] }),\n homeDir,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,YAAY,UAAkBA,QAAAA,QAAG,QAAQ,GAAW;CAClE,OAAOC,UAAAA,QAAK,KAAK,SAAS,OAAO,SAAS,KAAK;AACjD;;AAGA,SAAgB,cAAc,UAAkBD,QAAAA,QAAG,QAAQ,GAAW;CACpE,OAAOC,UAAAA,QAAK,KAAK,YAAY,OAAO,GAAG,WAAW;AACpD;;AAGA,SAAgB,aAAa,gBAAwB,UAAkBD,QAAAA,QAAG,QAAQ,GAAW;CAC3F,MAAM,KAAKE,gBAAAA,aAAa,cAAc;CACtC,MAAM,OAAOD,UAAAA,QAAK,KAAK,YAAY,OAAO,GAAG,UAAU;CACvD,MAAM,YAAYA,UAAAA,QAAK,KAAK,MAAM,IAAI,gBAAgB;CACtD,IAAIE,QAAAA,QAAG,WAAW,SAAS,GAAG,OAAO;CACrC,IAAI;CACJ,IAAI;EACF,QAAQA,QAAAA,QAAG,YAAY,IAAI;CAC7B,QAAQ;EACN,OAAO;CACT;CACA,MAAM,cAAc,MACjB,QAAQ,UAAU,MAAM,SAAS,KAAK,IAAI,CAAC,CAAC,CAC5C,KAAK,CAAC,CACN,KAAK,UAAUF,UAAAA,QAAK,KAAK,MAAM,OAAO,gBAAgB,CAAC,CAAC,CACxD,QAAQ,SAASE,QAAAA,QAAG,WAAW,IAAI,CAAC;CACvC,IAAI,YAAY,WAAW,GAAG,OAAO;CACrC,MAAM,0BAAU,IAAI,IAA4B;CAChD,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,SAASC,cAAAA,SAAmD,IAAI;EACtE,IAAI,QAAQ,YAAA,KAAuC,CAAC,MAAM,QAAQ,OAAO,OAAO,GAAG;EACnF,KAAK,MAAM,SAAS,OAAO,SAAS;GAClC,IACE,OAAO,UAAU,YACjB,UAAU,QACV,EAAE,QAAQ,UACV,OAAO,MAAM,OAAO,YACpB,MAAM,OAAO,IAEb;GACF,MAAM,QAAQ;GACd,MAAM,WAAW,QAAQ,IAAI,MAAM,EAAE;GACrC,IAAI,aAAa,KAAA,KAAa,EAAA,GAACC,UAAAA,kBAAAA,CAAkB,UAAU,KAAK,GAC9D,MAAM,IAAI,MAAM,qDAAqD,MAAM,GAAG,GAAG;GAEnF,QAAQ,IAAI,MAAM,IAAI,KAAK;EAC7B;CACF;CACA,cAAA,gBAAgB,WAAW;EAAE,SAAA;EAAkC,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC;CAAE,CAAC;CAC/F,OAAO;AACT"}
@@ -1,6 +1,9 @@
1
- import { repositoryId, repositoryLabel } from "../repositoryIdentity/index.mjs";
1
+ import { readJson, writeJsonAtomic } from "../atomicJson/index.mjs";
2
+ import { repositoryId } from "../repositoryIdentity/index.mjs";
3
+ import fs from "node:fs";
2
4
  import path from "node:path";
3
5
  import os from "node:os";
6
+ import { isDeepStrictEqual } from "node:util";
4
7
  import "@agimon-ai/doompi-core/web";
5
8
  //#region src/services/paths/index.ts
6
9
  /**
@@ -21,8 +24,35 @@ function worktreesRoot(homeDir = os.homedir()) {
21
24
  }
22
25
  /** One registry file per repository, keyed by the shared git directory. */
23
26
  function registryFile(repositoryRoot, homeDir = os.homedir()) {
24
- const key = `${repositoryLabel(repositoryRoot)}--${repositoryId(repositoryRoot)}`;
25
- return path.join(doomGitRoot(homeDir), "registry", key, "worktrees.json");
27
+ const id = repositoryId(repositoryRoot);
28
+ const root = path.join(doomGitRoot(homeDir), "registry");
29
+ const canonical = path.join(root, id, "worktrees.json");
30
+ if (fs.existsSync(canonical)) return canonical;
31
+ let names;
32
+ try {
33
+ names = fs.readdirSync(root);
34
+ } catch {
35
+ return canonical;
36
+ }
37
+ const legacyFiles = names.filter((entry) => entry.endsWith(`--${id}`)).sort().map((entry) => path.join(root, entry, "worktrees.json")).filter((file) => fs.existsSync(file));
38
+ if (legacyFiles.length === 0) return canonical;
39
+ const entries = /* @__PURE__ */ new Map();
40
+ for (const file of legacyFiles) {
41
+ const legacy = readJson(file);
42
+ if (legacy?.version !== 1 || !Array.isArray(legacy.entries)) continue;
43
+ for (const value of legacy.entries) {
44
+ if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || value.id === "") continue;
45
+ const entry = value;
46
+ const existing = entries.get(entry.id);
47
+ if (existing !== void 0 && !isDeepStrictEqual(existing, entry)) throw new Error(`Conflicting legacy worktree registry entries for '${entry.id}'.`);
48
+ entries.set(entry.id, entry);
49
+ }
50
+ }
51
+ writeJsonAtomic(canonical, {
52
+ version: 1,
53
+ entries: [...entries.values()]
54
+ });
55
+ return canonical;
26
56
  }
27
57
  //#endregion
28
58
  export { doomGitRoot, registryFile, worktreesRoot };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/paths/index.ts"],"sourcesContent":["import os from 'node:os';\nimport path from 'node:path';\n\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '@agimon-ai/doompi-core/web';\n\nimport { repositoryId, repositoryLabel } from '../repositoryIdentity';\n\nconst REGISTRY_DIR_FLAG = '--registry-dir';\n\n/**\n * Where this package keeps its own state.\n *\n * Outside the repository on purpose. A worktree directory inside the checkout\n * would be found by every glob the repository's own tooling runs, and would\n * resolve workspace imports against the parent checkout rather than the\n * worktree, so a session there would build the wrong source while appearing to\n * work.\n */\nexport function doomGitRoot(homeDir: string = os.homedir()): string {\n return path.join(homeDir, '.pi', '.doom', 'git');\n}\n\n/** The directory every worktree for every repository is created under. */\nexport function worktreesRoot(homeDir: string = os.homedir()): string {\n return path.join(doomGitRoot(homeDir), 'worktrees');\n}\n\n/** One registry file per repository, keyed by the shared git directory. */\n\nexport function registryFile(repositoryRoot: string, homeDir: string = os.homedir()): string {\n const key = `${repositoryLabel(repositoryRoot)}--${repositoryId(repositoryRoot)}`;\n return path.join(doomGitRoot(homeDir), 'registry', key, 'worktrees.json');\n}\n\n/**\n * The session registry directory this process's hub is watching.\n *\n * A spawned session only appears in the rail if it registers in the same\n * directory the hub watches, and the hub passes that directory to its children\n * as `--registry-dir` while also inheriting the env var. Recovering it from\n * this process's own argv and env, in the same precedence the hub and the\n * session server use, is what makes a worktree session visible rather than\n * silently invisible.\n */\nexport function hubRegistryDir(\n argv: readonly string[] = process.argv,\n env: NodeJS.ProcessEnv = process.env,\n homeDir: string = os.homedir(),\n): string {\n const index = argv.indexOf(REGISTRY_DIR_FLAG);\n const flagValue = index >= 0 ? argv[index + 1] : undefined;\n return resolveRegistryDir({\n ...(flagValue === undefined ? {} : { flagValue }),\n ...(env[REGISTRY_DIR_ENV] === undefined ? {} : { envValue: env[REGISTRY_DIR_ENV] }),\n homeDir,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,SAAgB,YAAY,UAAkB,GAAG,QAAQ,GAAW;CAClE,OAAO,KAAK,KAAK,SAAS,OAAO,SAAS,KAAK;AACjD;;AAGA,SAAgB,cAAc,UAAkB,GAAG,QAAQ,GAAW;CACpE,OAAO,KAAK,KAAK,YAAY,OAAO,GAAG,WAAW;AACpD;;AAIA,SAAgB,aAAa,gBAAwB,UAAkB,GAAG,QAAQ,GAAW;CAC3F,MAAM,MAAM,GAAG,gBAAgB,cAAc,EAAE,IAAI,aAAa,cAAc;CAC9E,OAAO,KAAK,KAAK,YAAY,OAAO,GAAG,YAAY,KAAK,gBAAgB;AAC1E"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/paths/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { isDeepStrictEqual } from 'node:util';\n\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '@agimon-ai/doompi-core/web';\n\nimport { WORKTREE_RECORD_VERSION, type WorktreeRecord } from '../../types/worktreeRegistry';\nimport { readJson, writeJsonAtomic } from '../atomicJson';\nimport { repositoryId } from '../repositoryIdentity';\n\nconst REGISTRY_DIR_FLAG = '--registry-dir';\n\n/**\n * Where this package keeps its own state.\n *\n * Outside the repository on purpose. A worktree directory inside the checkout\n * would be found by every glob the repository's own tooling runs, and would\n * resolve workspace imports against the parent checkout rather than the\n * worktree, so a session there would build the wrong source while appearing to\n * work.\n */\nexport function doomGitRoot(homeDir: string = os.homedir()): string {\n return path.join(homeDir, '.pi', '.doom', 'git');\n}\n\n/** The directory every worktree for every repository is created under. */\nexport function worktreesRoot(homeDir: string = os.homedir()): string {\n return path.join(doomGitRoot(homeDir), 'worktrees');\n}\n\n/** One registry file per repository, keyed by the shared git directory. */\nexport function registryFile(repositoryRoot: string, homeDir: string = os.homedir()): string {\n const id = repositoryId(repositoryRoot);\n const root = path.join(doomGitRoot(homeDir), 'registry');\n const canonical = path.join(root, id, 'worktrees.json');\n if (fs.existsSync(canonical)) return canonical;\n let names: string[];\n try {\n names = fs.readdirSync(root);\n } catch {\n return canonical;\n }\n const legacyFiles = names\n .filter((entry) => entry.endsWith(`--${id}`))\n .sort()\n .map((entry) => path.join(root, entry, 'worktrees.json'))\n .filter((file) => fs.existsSync(file));\n if (legacyFiles.length === 0) return canonical;\n const entries = new Map<string, WorktreeRecord>();\n for (const file of legacyFiles) {\n const legacy = readJson<{ version?: unknown; entries?: unknown }>(file);\n if (legacy?.version !== WORKTREE_RECORD_VERSION || !Array.isArray(legacy.entries)) continue;\n for (const value of legacy.entries) {\n if (\n typeof value !== 'object' ||\n value === null ||\n !('id' in value) ||\n typeof value.id !== 'string' ||\n value.id === ''\n )\n continue;\n const entry = value as WorktreeRecord;\n const existing = entries.get(entry.id);\n if (existing !== undefined && !isDeepStrictEqual(existing, entry)) {\n throw new Error(`Conflicting legacy worktree registry entries for '${entry.id}'.`);\n }\n entries.set(entry.id, entry);\n }\n }\n writeJsonAtomic(canonical, { version: WORKTREE_RECORD_VERSION, entries: [...entries.values()] });\n return canonical;\n}\n\n/**\n * The session registry directory this process's hub is watching.\n *\n * A spawned session only appears in the rail if it registers in the same\n * directory the hub watches, and the hub passes that directory to its children\n * as `--registry-dir` while also inheriting the env var. Recovering it from\n * this process's own argv and env, in the same precedence the hub and the\n * session server use, is what makes a worktree session visible rather than\n * silently invisible.\n */\nexport function hubRegistryDir(\n argv: readonly string[] = process.argv,\n env: NodeJS.ProcessEnv = process.env,\n homeDir: string = os.homedir(),\n): string {\n const index = argv.indexOf(REGISTRY_DIR_FLAG);\n const flagValue = index >= 0 ? argv[index + 1] : undefined;\n return resolveRegistryDir({\n ...(flagValue === undefined ? {} : { flagValue }),\n ...(env[REGISTRY_DIR_ENV] === undefined ? {} : { envValue: env[REGISTRY_DIR_ENV] }),\n homeDir,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,SAAgB,YAAY,UAAkB,GAAG,QAAQ,GAAW;CAClE,OAAO,KAAK,KAAK,SAAS,OAAO,SAAS,KAAK;AACjD;;AAGA,SAAgB,cAAc,UAAkB,GAAG,QAAQ,GAAW;CACpE,OAAO,KAAK,KAAK,YAAY,OAAO,GAAG,WAAW;AACpD;;AAGA,SAAgB,aAAa,gBAAwB,UAAkB,GAAG,QAAQ,GAAW;CAC3F,MAAM,KAAK,aAAa,cAAc;CACtC,MAAM,OAAO,KAAK,KAAK,YAAY,OAAO,GAAG,UAAU;CACvD,MAAM,YAAY,KAAK,KAAK,MAAM,IAAI,gBAAgB;CACtD,IAAI,GAAG,WAAW,SAAS,GAAG,OAAO;CACrC,IAAI;CACJ,IAAI;EACF,QAAQ,GAAG,YAAY,IAAI;CAC7B,QAAQ;EACN,OAAO;CACT;CACA,MAAM,cAAc,MACjB,QAAQ,UAAU,MAAM,SAAS,KAAK,IAAI,CAAC,CAAC,CAC5C,KAAK,CAAC,CACN,KAAK,UAAU,KAAK,KAAK,MAAM,OAAO,gBAAgB,CAAC,CAAC,CACxD,QAAQ,SAAS,GAAG,WAAW,IAAI,CAAC;CACvC,IAAI,YAAY,WAAW,GAAG,OAAO;CACrC,MAAM,0BAAU,IAAI,IAA4B;CAChD,KAAK,MAAM,QAAQ,aAAa;EAC9B,MAAM,SAAS,SAAmD,IAAI;EACtE,IAAI,QAAQ,YAAA,KAAuC,CAAC,MAAM,QAAQ,OAAO,OAAO,GAAG;EACnF,KAAK,MAAM,SAAS,OAAO,SAAS;GAClC,IACE,OAAO,UAAU,YACjB,UAAU,QACV,EAAE,QAAQ,UACV,OAAO,MAAM,OAAO,YACpB,MAAM,OAAO,IAEb;GACF,MAAM,QAAQ;GACd,MAAM,WAAW,QAAQ,IAAI,MAAM,EAAE;GACrC,IAAI,aAAa,KAAA,KAAa,CAAC,kBAAkB,UAAU,KAAK,GAC9D,MAAM,IAAI,MAAM,qDAAqD,MAAM,GAAG,GAAG;GAEnF,QAAQ,IAAI,MAAM,IAAI,KAAK;EAC7B;CACF;CACA,gBAAgB,WAAW;EAAE,SAAA;EAAkC,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC;CAAE,CAAC;CAC/F,OAAO;AACT"}
@@ -56,10 +56,11 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
56
56
  const record = await operations.spawn(context, {
57
57
  branch: params.branch,
58
58
  ...params.baseRef === void 0 ? {} : { baseRef: params.baseRef },
59
- ...params.name === void 0 ? {} : { name: params.name }
59
+ ...params.name === void 0 ? {} : { name: params.name },
60
+ ...params.task === void 0 ? {} : { task: params.task }
60
61
  }, options);
61
62
  return {
62
- content: text(`Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`, `Path: ${record.path}`, `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`),
63
+ content: text(`Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`, `Path: ${record.path}`, params.task === void 0 ? `Session ${record.sessionId} is starting; it appears nested under this one in the rail.` : `Session ${record.sessionId} is starting with its task; it appears nested under this one in the rail.`),
63
64
  details: {
64
65
  action: params.action,
65
66
  records: [record]
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["WORKTREE_ACTIONS","invalidRequest","WORKTREE_ACTION_FIELDS"],"sources":["../../../../src/services/runWorktree/index.ts"],"sourcesContent":["import {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../schemas/runWorktreeTool';\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\nimport { invalidRequest } from '../errors';\nimport type { WorktreeOperations } from '../worktreeOperations';\n\nexport { RunWorktreeParams };\nexport type { RunWorktreeToolParams, WorktreeAction };\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nexport const RUN_WORKTREE_DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\nexport interface RunWorktreeToolResult {\n content: [{ type: 'text'; text: string }];\n details: RunWorktreeToolDetails;\n}\n\nexport interface RunWorktreeExecutionContext {\n readonly cwd: string;\n readonly sessionId: string;\n}\n\nexport interface RunWorktreeExecutionOptions {\n readonly signal?: AbortSignal;\n readonly onProgress?: (label: string) => void;\n}\n\n/** Rejects calls that do not match one documented action shape. */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n const stringFields = ['baseRef', 'name', 'task', 'message'] as const;\n for (const field of stringFields) {\n if (field in record && (typeof record[field] !== 'string' || record[field].trim() === '')) {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n for (const field of ['force', 'dryRun'] as const) {\n if (field in record && typeof record[field] !== 'boolean') {\n throw invalidRequest(`action='${action}' requires boolean '${field}'.`, `Pass true or false for ${field}.`);\n }\n }\n if (action === WORKTREE_ACTIONS.send && typeof record.message !== 'string') {\n throw invalidRequest(\"action='send' requires 'message'.\", 'Pass the message to send.');\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge ||\n action === WORKTREE_ACTIONS.send ||\n action === WORKTREE_ACTIONS.messages) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nexport async function executeRunWorktreeTool(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n context: RunWorktreeExecutionContext,\n options: RunWorktreeExecutionOptions = {},\n): Promise<RunWorktreeToolResult> {\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(\n context,\n {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n },\n options,\n );\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ...(plan.keptBranches.length === 0\n ? []\n : [`Branches kept, they hold commits: ${plan.keptBranches.join(', ')}`]),\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["WORKTREE_ACTIONS","invalidRequest","WORKTREE_ACTION_FIELDS"],"sources":["../../../../src/services/runWorktree/index.ts"],"sourcesContent":["import {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../schemas/runWorktreeTool';\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\nimport { invalidRequest } from '../errors';\nimport type { WorktreeOperations } from '../worktreeOperations';\n\nexport { RunWorktreeParams };\nexport type { RunWorktreeToolParams, WorktreeAction };\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nexport const RUN_WORKTREE_DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\nexport interface RunWorktreeToolResult {\n content: [{ type: 'text'; text: string }];\n details: RunWorktreeToolDetails;\n}\n\nexport interface RunWorktreeExecutionContext {\n readonly cwd: string;\n readonly sessionId: string;\n}\n\nexport interface RunWorktreeExecutionOptions {\n readonly signal?: AbortSignal;\n readonly onProgress?: (label: string) => void;\n}\n\n/** Rejects calls that do not match one documented action shape. */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n const stringFields = ['baseRef', 'name', 'task', 'message'] as const;\n for (const field of stringFields) {\n if (field in record && (typeof record[field] !== 'string' || record[field].trim() === '')) {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n for (const field of ['force', 'dryRun'] as const) {\n if (field in record && typeof record[field] !== 'boolean') {\n throw invalidRequest(`action='${action}' requires boolean '${field}'.`, `Pass true or false for ${field}.`);\n }\n }\n if (action === WORKTREE_ACTIONS.send && typeof record.message !== 'string') {\n throw invalidRequest(\"action='send' requires 'message'.\", 'Pass the message to send.');\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge ||\n action === WORKTREE_ACTIONS.send ||\n action === WORKTREE_ACTIONS.messages) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nexport async function executeRunWorktreeTool(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n context: RunWorktreeExecutionContext,\n options: RunWorktreeExecutionOptions = {},\n): Promise<RunWorktreeToolResult> {\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(\n context,\n {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n ...(params.task === undefined ? {} : { task: params.task }),\n },\n options,\n );\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n params.task === undefined\n ? `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`\n : `Session ${record.sessionId} is starting with its task; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ...(plan.keptBranches.length === 0\n ? []\n : [`Branches kept, they hold commits: ${plan.keptBranches.join(', ')}`]),\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n"],"mappings":"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"}
@@ -56,10 +56,11 @@ async function executeRunWorktreeTool(params, operations, context, options = {})
56
56
  const record = await operations.spawn(context, {
57
57
  branch: params.branch,
58
58
  ...params.baseRef === void 0 ? {} : { baseRef: params.baseRef },
59
- ...params.name === void 0 ? {} : { name: params.name }
59
+ ...params.name === void 0 ? {} : { name: params.name },
60
+ ...params.task === void 0 ? {} : { task: params.task }
60
61
  }, options);
61
62
  return {
62
- content: text(`Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`, `Path: ${record.path}`, `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`),
63
+ content: text(`Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`, `Path: ${record.path}`, params.task === void 0 ? `Session ${record.sessionId} is starting; it appears nested under this one in the rail.` : `Session ${record.sessionId} is starting with its task; it appears nested under this one in the rail.`),
63
64
  details: {
64
65
  action: params.action,
65
66
  records: [record]
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/runWorktree/index.ts"],"sourcesContent":["import {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../schemas/runWorktreeTool';\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\nimport { invalidRequest } from '../errors';\nimport type { WorktreeOperations } from '../worktreeOperations';\n\nexport { RunWorktreeParams };\nexport type { RunWorktreeToolParams, WorktreeAction };\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nexport const RUN_WORKTREE_DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\nexport interface RunWorktreeToolResult {\n content: [{ type: 'text'; text: string }];\n details: RunWorktreeToolDetails;\n}\n\nexport interface RunWorktreeExecutionContext {\n readonly cwd: string;\n readonly sessionId: string;\n}\n\nexport interface RunWorktreeExecutionOptions {\n readonly signal?: AbortSignal;\n readonly onProgress?: (label: string) => void;\n}\n\n/** Rejects calls that do not match one documented action shape. */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n const stringFields = ['baseRef', 'name', 'task', 'message'] as const;\n for (const field of stringFields) {\n if (field in record && (typeof record[field] !== 'string' || record[field].trim() === '')) {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n for (const field of ['force', 'dryRun'] as const) {\n if (field in record && typeof record[field] !== 'boolean') {\n throw invalidRequest(`action='${action}' requires boolean '${field}'.`, `Pass true or false for ${field}.`);\n }\n }\n if (action === WORKTREE_ACTIONS.send && typeof record.message !== 'string') {\n throw invalidRequest(\"action='send' requires 'message'.\", 'Pass the message to send.');\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge ||\n action === WORKTREE_ACTIONS.send ||\n action === WORKTREE_ACTIONS.messages) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nexport async function executeRunWorktreeTool(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n context: RunWorktreeExecutionContext,\n options: RunWorktreeExecutionOptions = {},\n): Promise<RunWorktreeToolResult> {\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(\n context,\n {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n },\n options,\n );\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ...(plan.keptBranches.length === 0\n ? []\n : [`Branches kept, they hold commits: ${plan.keptBranches.join(', ')}`]),\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/runWorktree/index.ts"],"sourcesContent":["import {\n RunWorktreeParams,\n WORKTREE_ACTION_FIELDS,\n WORKTREE_ACTIONS,\n type WorktreeAction,\n type RunWorktreeToolParams,\n} from '../../schemas/runWorktreeTool';\nimport type { WorktreeRecord } from '../../types/worktreeRegistry';\nimport { invalidRequest } from '../errors';\nimport type { WorktreeOperations } from '../worktreeOperations';\n\nexport { RunWorktreeParams };\nexport type { RunWorktreeToolParams, WorktreeAction };\n\nexport const RUN_WORKTREE_TOOL_NAME = 'run_worktree';\n\nconst IMPLEMENTED_ACTIONS: ReadonlySet<string> = new Set(Object.values(WORKTREE_ACTIONS));\n\nexport const RUN_WORKTREE_DESCRIPTION = `Create and manage git worktrees, each with its own DoomPi session nested under this one.\n\nActions:\n- spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch), 'name' for the rail, 'task' as the first message.\n- close_worktree: stop a worktree's session and delete its directory. Requires 'id'. Refuses a dirty tree unless 'force' is true.\n- list: every worktree for this repository, with status.\n- status: one worktree and its uncommitted files. Requires 'id'.\n- merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.\n- prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.\n- send: send a message to the other side of a worktree. Requires 'id' and 'message'.\n- messages: read messages waiting for you from a worktree. Requires 'id'.\n\nWorktrees live outside the repository, under ~/.pi/.doom/git/worktrees.`;\n\nexport interface RunWorktreeToolDetails {\n action: WorktreeAction;\n records?: WorktreeRecord[];\n}\n\nexport interface RunWorktreeToolResult {\n content: [{ type: 'text'; text: string }];\n details: RunWorktreeToolDetails;\n}\n\nexport interface RunWorktreeExecutionContext {\n readonly cwd: string;\n readonly sessionId: string;\n}\n\nexport interface RunWorktreeExecutionOptions {\n readonly signal?: AbortSignal;\n readonly onProgress?: (label: string) => void;\n}\n\n/** Rejects calls that do not match one documented action shape. */\nexport function validateParams(input: unknown): RunWorktreeToolParams {\n if (typeof input !== 'object' || input === null || Array.isArray(input)) {\n throw invalidRequest('run_worktree parameters must be an object.', 'Call it with one documented action shape.');\n }\n const record = input as Record<string, unknown>;\n const action = record.action;\n if (typeof action !== 'string' || !IMPLEMENTED_ACTIONS.has(action)) {\n throw invalidRequest(\n `Unknown action ${typeof action === 'string' ? `'${action}'` : 'value'}.`,\n `Use one of: ${[...IMPLEMENTED_ACTIONS].join(', ')}.`,\n );\n }\n const allowed = WORKTREE_ACTION_FIELDS[action as WorktreeAction];\n const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));\n if (unexpected.length > 0) {\n throw invalidRequest(\n `action='${action}' does not accept: ${unexpected.join(', ')}.`,\n `Fields for '${action}': ${allowed.join(', ')}.`,\n );\n }\n for (const field of ['branch', 'id'] as const) {\n if (allowed.includes(field) && field in record) {\n const value = record[field];\n if (typeof value !== 'string' || value.trim() === '') {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n }\n const stringFields = ['baseRef', 'name', 'task', 'message'] as const;\n for (const field of stringFields) {\n if (field in record && (typeof record[field] !== 'string' || record[field].trim() === '')) {\n throw invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);\n }\n }\n for (const field of ['force', 'dryRun'] as const) {\n if (field in record && typeof record[field] !== 'boolean') {\n throw invalidRequest(`action='${action}' requires boolean '${field}'.`, `Pass true or false for ${field}.`);\n }\n }\n if (action === WORKTREE_ACTIONS.send && typeof record.message !== 'string') {\n throw invalidRequest(\"action='send' requires 'message'.\", 'Pass the message to send.');\n }\n if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== 'string') {\n throw invalidRequest(\"action='spawn_worktree' requires 'branch'.\", 'Name the branch to create.');\n }\n if (\n (action === WORKTREE_ACTIONS.close_worktree ||\n action === WORKTREE_ACTIONS.status ||\n action === WORKTREE_ACTIONS.merge ||\n action === WORKTREE_ACTIONS.send ||\n action === WORKTREE_ACTIONS.messages) &&\n typeof record.id !== 'string'\n ) {\n throw invalidRequest(`action='${action}' requires 'id'.`, 'Call list to see the current ids.');\n }\n return record as RunWorktreeToolParams;\n}\n\nfunction describe(record: WorktreeRecord): string {\n return `${record.id} ${record.branch} ${record.status} ${record.path}`;\n}\n\nfunction text(...lines: readonly string[]): [{ type: 'text'; text: string }] {\n return [{ type: 'text', text: lines.join('\\n') }];\n}\n\nexport async function executeRunWorktreeTool(\n params: RunWorktreeToolParams,\n operations: WorktreeOperations,\n context: RunWorktreeExecutionContext,\n options: RunWorktreeExecutionOptions = {},\n): Promise<RunWorktreeToolResult> {\n switch (params.action) {\n case WORKTREE_ACTIONS.spawn_worktree: {\n const record = await operations.spawn(\n context,\n {\n branch: params.branch,\n ...(params.baseRef === undefined ? {} : { baseRef: params.baseRef }),\n ...(params.name === undefined ? {} : { name: params.name }),\n ...(params.task === undefined ? {} : { task: params.task }),\n },\n options,\n );\n return {\n content: text(\n `Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`,\n `Path: ${record.path}`,\n params.task === undefined\n ? `Session ${record.sessionId} is starting; it appears nested under this one in the rail.`\n : `Session ${record.sessionId} is starting with its task; it appears nested under this one in the rail.`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.close_worktree: {\n const record = await operations.close(context, params.id, params.force ?? false);\n return {\n content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.list: {\n const records = await operations.list(context);\n return {\n content:\n records.length === 0\n ? text('No worktrees for this repository.')\n : text(`${String(records.length)} worktree(s):`, ...records.map(describe)),\n details: { action: params.action, records },\n };\n }\n case WORKTREE_ACTIONS.status: {\n const { record, dirtyFiles } = await operations.status(context, params.id);\n return {\n content: text(\n describe(record),\n `Session: ${record.sessionId}`,\n dirtyFiles.length === 0\n ? 'Clean.'\n : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(', ')}`,\n ),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.merge: {\n const record = await operations.merge(\n context,\n params.id,\n ...(params.message === undefined ? [] : ([params.message] as const)),\n );\n return {\n content: text(`Merged ${record.branch} into this checkout.`),\n details: { action: params.action, records: [record] },\n };\n }\n case WORKTREE_ACTIONS.send: {\n await operations.send(context, params.id, params.message);\n return { content: text(`Message sent to worktree ${params.id}.`), details: { action: params.action } };\n }\n case WORKTREE_ACTIONS.messages: {\n const waiting = await operations.messages(context, params.id);\n return {\n content:\n waiting.length === 0\n ? text('No messages.')\n : text(\n `${String(waiting.length)} message(s):`,\n ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`),\n ),\n details: { action: params.action },\n };\n }\n case WORKTREE_ACTIONS.prune: {\n const dryRun = params.dryRun ?? false;\n const plan = await operations.prune(context, dryRun);\n const lines = [\n dryRun ? 'Prune plan (nothing was changed):' : 'Pruned:',\n `Remove: ${plan.remove.length === 0 ? 'none' : plan.remove.map((record) => record.id).join(', ')}`,\n `Forget: ${plan.forget.length === 0 ? 'none' : plan.forget.map((record) => record.id).join(', ')}`,\n `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? 'none' : plan.keptDirty.map((record) => record.id).join(', ')}`,\n `Untracked directories, left alone: ${plan.untracked.length === 0 ? 'none' : plan.untracked.join(', ')}`,\n ...(plan.keptBranches.length === 0\n ? []\n : [`Branches kept, they hold commits: ${plan.keptBranches.join(', ')}`]),\n ];\n return { content: text(...lines), details: { action: params.action } };\n }\n }\n}\n"],"mappings":"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"}
@@ -10,6 +10,9 @@ let node_fs = require("node:fs");
10
10
  node_fs = require_runtime.__toESM(node_fs, 1);
11
11
  let node_crypto = require("node:crypto");
12
12
  //#region src/services/worktreeOperations/index.ts
13
+ const GIT_WORKTREE_DELEGATION_KIND = "git.worktree.delegation";
14
+ const GIT_WORKTREE_MESSAGE_KIND = "git.worktree.message";
15
+ const GIT_WORKTREE_REPORT_KIND = "git.worktree.report";
13
16
  function createWorktreeOperations(deps) {
14
17
  const { git } = deps;
15
18
  const sessionService = deps.sessionService;
@@ -23,6 +26,12 @@ function createWorktreeOperations(deps) {
23
26
  if (deps.messageInbox === void 0) throw new require_index$1.HubUnavailableError("The Git message service is unavailable. Start the cockpit and try again.");
24
27
  return deps.messageInbox;
25
28
  };
29
+ const readSessionDelivery = () => deps.sessionDelivery?.();
30
+ const requireSessionDelivery = () => {
31
+ const delivery = readSessionDelivery();
32
+ if (delivery === void 0) throw new require_index$1.HubUnavailableError("The Session delivery service is unavailable. Start the cockpit and try again.");
33
+ return delivery;
34
+ };
26
35
  const probe = {
27
36
  alive: (record) => requireSessionService().isLive(record.sessionId),
28
37
  exists: (path) => node_fs.default.existsSync(path)
@@ -86,6 +95,7 @@ function createWorktreeOperations(deps) {
86
95
  existing: records
87
96
  });
88
97
  if (refusal !== void 0) throw new require_index$1.DoomGitExpectedError("worktree_exists", refusal, false, "Pick another branch name, or close it.");
98
+ const taskDelivery = request.task === void 0 ? void 0 : requireSessionDelivery();
89
99
  const baseRef = request.baseRef ?? await git.currentBranch(root) ?? "HEAD";
90
100
  const id = require_index$2.shortId((0, node_crypto.randomUUID)());
91
101
  const path = require_index$5.worktreeDirectory({
@@ -155,6 +165,23 @@ function createWorktreeOperations(deps) {
155
165
  const kept = await rollback();
156
166
  throw new require_index$1.DoomGitExpectedError("registry_write_failed", `The worktree was created but the registry could not be written (${error.message}).${kept}`, true, "Check the registry file for permissions or free space, then try again.");
157
167
  }
168
+ if (request.task !== void 0 && taskDelivery !== void 0) try {
169
+ const receipt = await taskDelivery.deliver({
170
+ recipientKey: sessionId,
171
+ kind: GIT_WORKTREE_DELEGATION_KIND,
172
+ prompt: request.task,
173
+ metadata: {
174
+ worktreeId: record.id,
175
+ fromRole: "parent",
176
+ text: request.task,
177
+ sentAt: record.createdAt
178
+ }
179
+ });
180
+ const admission = await taskDelivery.waitForAdmission(receipt.deliveryId);
181
+ if (admission !== "admitted") throw new Error(admission === "recovery_required" ? "The recipient could not admit the task." : "The recipient did not confirm task admission before the timeout.");
182
+ } catch (error) {
183
+ throw new require_index$1.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.`);
184
+ }
158
185
  return record;
159
186
  },
160
187
  async close(context, id, force) {
@@ -211,20 +238,54 @@ function createWorktreeOperations(deps) {
211
238
  const record = require$1(records, id);
212
239
  const peer = peerFor(context, record);
213
240
  requirePeerLive(peer.sessionId);
241
+ const sentAt = (/* @__PURE__ */ new Date()).toISOString();
242
+ const delivery = readSessionDelivery();
243
+ if (delivery !== void 0) {
244
+ const kind = peer.party === "parent" ? GIT_WORKTREE_MESSAGE_KIND : GIT_WORKTREE_REPORT_KIND;
245
+ const receipt = await delivery.deliver({
246
+ recipientKey: peer.sessionId,
247
+ kind,
248
+ prompt: peer.party === "parent" ? `Parent message for worktree ${record.id}:\n\n${message}` : `Report from worktree ${record.id}:\n\n${message}`,
249
+ metadata: {
250
+ worktreeId: record.id,
251
+ fromRole: peer.party,
252
+ text: message,
253
+ sentAt
254
+ }
255
+ });
256
+ const admission = await delivery.waitForAdmission(receipt.deliveryId);
257
+ if (admission !== "admitted") throw new require_index$1.DoomGitExpectedError("message_delivery_failed", `Worktree ${record.id} persisted the message, but its session did not confirm admission.`, true, admission === "recovery_required" ? "Call messages from the recipient session to recover the persisted text." : "The message remains durable; retry after the recipient is ready.");
258
+ return;
259
+ }
214
260
  requireMessageInbox().send(peer.sessionId, {
215
261
  version: 1,
216
262
  worktreeId: record.id,
217
263
  fromSessionId: context.sessionId,
218
264
  from: peer.party,
219
265
  text: message,
220
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
266
+ sentAt
221
267
  });
222
268
  },
223
269
  async messages(context, id) {
224
270
  const { records } = await resolve(context);
225
271
  const record = require$1(records, id);
226
- peerFor(context, record);
227
- return requireMessageInbox().receive(record.id);
272
+ const peer = peerFor(context, record);
273
+ const delivery = readSessionDelivery();
274
+ if (delivery === void 0) return requireMessageInbox().receive(record.id);
275
+ return delivery.inbox({
276
+ consumed: false,
277
+ metadata: { worktreeId: record.id }
278
+ }).filter((entry) => entry.senderKey === peer.sessionId && (entry.state === "admitted" || entry.state === "recovery_required") && (peer.party === "parent" ? entry.kind === GIT_WORKTREE_REPORT_KIND : entry.kind === GIT_WORKTREE_MESSAGE_KIND || entry.kind === GIT_WORKTREE_DELEGATION_KIND)).map((entry) => {
279
+ delivery.consume(entry.deliveryId);
280
+ return {
281
+ version: 1,
282
+ worktreeId: record.id,
283
+ fromSessionId: entry.senderKey,
284
+ from: peer.party === "parent" ? "child" : "parent",
285
+ text: entry.state === "recovery_required" ? `[Recovery required: prompt admission was not confirmed] ${entry.metadata.text ?? entry.prompt}` : entry.metadata.text ?? entry.prompt,
286
+ sentAt: entry.metadata.sentAt ?? ""
287
+ };
288
+ });
228
289
  },
229
290
  async prune(context, dryRun) {
230
291
  const { root, store, records } = await resolve(context);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["mirrorComposition","HubUnavailableError","fs","DoomGitExpectedError","createWorktreeRegistry","registryFile","require","refuseSpawn","shortId","randomUUID","worktreeDirectory","worktreesRoot","repositoryLabel","repositoryId","refuseClose","reconcile","MAX_WORKTREE_MESSAGE_BYTES","planPrune"],"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';\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\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}\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 /** Session-local inbox for parent/worktree messages. */\n messageInbox?: WorktreeMessageInbox;\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 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 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 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 requireMessageInbox().send(peer.sessionId, {\n version: 1,\n worktreeId: record.id,\n fromSessionId: context.sessionId,\n from: peer.party,\n text: message,\n sentAt: new Date().toISOString(),\n });\n },\n\n async messages(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n peerFor(context, record);\n return requireMessageInbox().receive(record.id);\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.cjs","names":["mirrorComposition","HubUnavailableError","fs","DoomGitExpectedError","createWorktreeRegistry","registryFile","require","refuseSpawn","shortId","randomUUID","worktreeDirectory","worktreesRoot","repositoryLabel","repositoryId","refuseClose","reconcile","MAX_WORKTREE_MESSAGE_BYTES","planPrune"],"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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"}
@@ -2,6 +2,7 @@ import { WorktreeMessage } from "../worktreeEvents/index.cjs";
2
2
  import { WorktreeRecord } from "../../types/worktreeRegistry.cjs";
3
3
  import { planPrune } from "../worktreeNaming/index.cjs";
4
4
  import "@agimon-ai/doompi-core/hub-channel";
5
+ import "@agimon-ai/doompi-session";
5
6
  //#region src/services/worktreeOperations/index.d.ts
6
7
  export interface WorktreeContext {
7
8
  /** Where the calling session is working, used to find the repository. */
@@ -13,6 +14,7 @@ export interface SpawnWorktreeRequest {
13
14
  branch: string;
14
15
  baseRef?: string;
15
16
  name?: string;
17
+ task?: string;
16
18
  }
17
19
  export interface SpawnOptions {
18
20
  /** Aborted when the caller gives up, so a spawn nobody is waiting for stops. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../../../src/services/worktreeOperations/index.ts"],"mappings":";;;;;iBAeiB;;EAEf;;EAEA;;iBAGe;EACf;EACA;EACA;;iBAGe;;EAEf,SAAS;;EAET,cAAc;;iBAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9B"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../../../src/services/worktreeOperations/index.ts"],"mappings":";;;;;;iBAmBiB;;EAEf;;EAEA;;iBAGe;EACf;EACA;EACA;EACA;;iBAGe;;EAEf,SAAS;;EAET,cAAc;;iBAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9B"}
@@ -1,6 +1,7 @@
1
1
  import { WorktreeMessage } from "../worktreeEvents/index.mjs";
2
2
  import { WorktreeRecord } from "../../types/worktreeRegistry.mjs";
3
3
  import { planPrune } from "../worktreeNaming/index.mjs";
4
+ import "@agimon-ai/doompi-session";
4
5
  import "@agimon-ai/doompi-core/hub-channel";
5
6
  //#region src/services/worktreeOperations/index.d.ts
6
7
  export interface WorktreeContext {
@@ -13,6 +14,7 @@ export interface SpawnWorktreeRequest {
13
14
  branch: string;
14
15
  baseRef?: string;
15
16
  name?: string;
17
+ task?: string;
16
18
  }
17
19
  export interface SpawnOptions {
18
20
  /** Aborted when the caller gives up, so a spawn nobody is waiting for stops. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/worktreeOperations/index.ts"],"mappings":";;;;;iBAeiB;;EAEf;;EAEA;;iBAGe;EACf;EACA;EACA;;iBAGe;;EAEf,SAAS;;EAET,cAAc;;iBAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/worktreeOperations/index.ts"],"mappings":";;;;;;iBAmBiB;;EAEf;;EAEA;;iBAGe;EACf;EACA;EACA;EACA;;iBAGe;;EAEf,SAAS;;EAET,cAAc;;iBAGC;EACf,MAAM,SAAS,iBAAiB,SAAS,sBAAsB,UAAU,eAAe,QAAQ;EAChG,MAAM,SAAS,iBAAiB,YAAY,iBAAiB,QAAQ;EACrE,KAAK,SAAS,kBAAkB,QAAQ;EACxC,OAAO,SAAS,iBAAiB,aAAa;IAAU,QAAQ;IAAgB;;EAChF,MAAM,SAAS,iBAAiB,YAAY,mBAAmB,QAAQ;EACvE,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ,kBAAkB;EAC5E,KAAK,SAAS,iBAAiB,YAAY,kBAAkB;EAC7D,SAAS,SAAS,iBAAiB,aAAa,QAA9B"}
@@ -8,6 +8,9 @@ import { createWorktreeRegistry } from "../worktreeRegistry/index.mjs";
8
8
  import fs from "node:fs";
9
9
  import { randomUUID } from "node:crypto";
10
10
  //#region src/services/worktreeOperations/index.ts
11
+ const GIT_WORKTREE_DELEGATION_KIND = "git.worktree.delegation";
12
+ const GIT_WORKTREE_MESSAGE_KIND = "git.worktree.message";
13
+ const GIT_WORKTREE_REPORT_KIND = "git.worktree.report";
11
14
  function createWorktreeOperations(deps) {
12
15
  const { git } = deps;
13
16
  const sessionService = deps.sessionService;
@@ -21,6 +24,12 @@ function createWorktreeOperations(deps) {
21
24
  if (deps.messageInbox === void 0) throw new HubUnavailableError("The Git message service is unavailable. Start the cockpit and try again.");
22
25
  return deps.messageInbox;
23
26
  };
27
+ const readSessionDelivery = () => deps.sessionDelivery?.();
28
+ const requireSessionDelivery = () => {
29
+ const delivery = readSessionDelivery();
30
+ if (delivery === void 0) throw new HubUnavailableError("The Session delivery service is unavailable. Start the cockpit and try again.");
31
+ return delivery;
32
+ };
24
33
  const probe = {
25
34
  alive: (record) => requireSessionService().isLive(record.sessionId),
26
35
  exists: (path) => fs.existsSync(path)
@@ -84,6 +93,7 @@ function createWorktreeOperations(deps) {
84
93
  existing: records
85
94
  });
86
95
  if (refusal !== void 0) throw new DoomGitExpectedError("worktree_exists", refusal, false, "Pick another branch name, or close it.");
96
+ const taskDelivery = request.task === void 0 ? void 0 : requireSessionDelivery();
87
97
  const baseRef = request.baseRef ?? await git.currentBranch(root) ?? "HEAD";
88
98
  const id = shortId(randomUUID());
89
99
  const path = worktreeDirectory({
@@ -153,6 +163,23 @@ function createWorktreeOperations(deps) {
153
163
  const kept = await rollback();
154
164
  throw new DoomGitExpectedError("registry_write_failed", `The worktree was created but the registry could not be written (${error.message}).${kept}`, true, "Check the registry file for permissions or free space, then try again.");
155
165
  }
166
+ if (request.task !== void 0 && taskDelivery !== void 0) try {
167
+ const receipt = await taskDelivery.deliver({
168
+ recipientKey: sessionId,
169
+ kind: GIT_WORKTREE_DELEGATION_KIND,
170
+ prompt: request.task,
171
+ metadata: {
172
+ worktreeId: record.id,
173
+ fromRole: "parent",
174
+ text: request.task,
175
+ sentAt: record.createdAt
176
+ }
177
+ });
178
+ const admission = await taskDelivery.waitForAdmission(receipt.deliveryId);
179
+ if (admission !== "admitted") throw new Error(admission === "recovery_required" ? "The recipient could not admit the task." : "The recipient did not confirm task admission before the timeout.");
180
+ } catch (error) {
181
+ 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
+ }
156
183
  return record;
157
184
  },
158
185
  async close(context, id, force) {
@@ -209,20 +236,54 @@ function createWorktreeOperations(deps) {
209
236
  const record = require(records, id);
210
237
  const peer = peerFor(context, record);
211
238
  requirePeerLive(peer.sessionId);
239
+ const sentAt = (/* @__PURE__ */ new Date()).toISOString();
240
+ const delivery = readSessionDelivery();
241
+ if (delivery !== void 0) {
242
+ const kind = peer.party === "parent" ? GIT_WORKTREE_MESSAGE_KIND : GIT_WORKTREE_REPORT_KIND;
243
+ const receipt = await delivery.deliver({
244
+ recipientKey: peer.sessionId,
245
+ kind,
246
+ prompt: peer.party === "parent" ? `Parent message for worktree ${record.id}:\n\n${message}` : `Report from worktree ${record.id}:\n\n${message}`,
247
+ metadata: {
248
+ worktreeId: record.id,
249
+ fromRole: peer.party,
250
+ text: message,
251
+ sentAt
252
+ }
253
+ });
254
+ const admission = await delivery.waitForAdmission(receipt.deliveryId);
255
+ if (admission !== "admitted") throw new DoomGitExpectedError("message_delivery_failed", `Worktree ${record.id} persisted the message, but its session did not confirm admission.`, true, admission === "recovery_required" ? "Call messages from the recipient session to recover the persisted text." : "The message remains durable; retry after the recipient is ready.");
256
+ return;
257
+ }
212
258
  requireMessageInbox().send(peer.sessionId, {
213
259
  version: 1,
214
260
  worktreeId: record.id,
215
261
  fromSessionId: context.sessionId,
216
262
  from: peer.party,
217
263
  text: message,
218
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
264
+ sentAt
219
265
  });
220
266
  },
221
267
  async messages(context, id) {
222
268
  const { records } = await resolve(context);
223
269
  const record = require(records, id);
224
- peerFor(context, record);
225
- return requireMessageInbox().receive(record.id);
270
+ const peer = peerFor(context, record);
271
+ const delivery = readSessionDelivery();
272
+ if (delivery === void 0) return requireMessageInbox().receive(record.id);
273
+ return delivery.inbox({
274
+ consumed: false,
275
+ metadata: { worktreeId: record.id }
276
+ }).filter((entry) => entry.senderKey === peer.sessionId && (entry.state === "admitted" || entry.state === "recovery_required") && (peer.party === "parent" ? entry.kind === GIT_WORKTREE_REPORT_KIND : entry.kind === GIT_WORKTREE_MESSAGE_KIND || entry.kind === GIT_WORKTREE_DELEGATION_KIND)).map((entry) => {
277
+ delivery.consume(entry.deliveryId);
278
+ return {
279
+ version: 1,
280
+ worktreeId: record.id,
281
+ fromSessionId: entry.senderKey,
282
+ from: peer.party === "parent" ? "child" : "parent",
283
+ text: entry.state === "recovery_required" ? `[Recovery required: prompt admission was not confirmed] ${entry.metadata.text ?? entry.prompt}` : entry.metadata.text ?? entry.prompt,
284
+ sentAt: entry.metadata.sentAt ?? ""
285
+ };
286
+ });
226
287
  },
227
288
  async prune(context, dryRun) {
228
289
  const { root, store, records } = await resolve(context);
@@ -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';\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\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}\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 /** Session-local inbox for parent/worktree messages. */\n messageInbox?: WorktreeMessageInbox;\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 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 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 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 requireMessageInbox().send(peer.sessionId, {\n version: 1,\n worktreeId: record.id,\n fromSessionId: context.sessionId,\n from: peer.party,\n text: message,\n sentAt: new Date().toISOString(),\n });\n },\n\n async messages(context, id) {\n const { records } = await resolve(context);\n const record = require(records, id);\n peerFor(context, record);\n return requireMessageInbox().receive(record.id);\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}\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-git",
3
- "version": "0.0.1-alpha.12",
3
+ "version": "0.0.1-alpha.13",
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,16 +67,17 @@
67
67
  "access": "public"
68
68
  },
69
69
  "dependencies": {
70
- "@agimon-ai/doompi-core": "0.0.1-alpha.76",
71
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.34",
70
+ "@agimon-ai/doompi-core": "0.0.1-alpha.77",
71
+ "@agimon-ai/doompi-session": "0.0.1-alpha.1",
72
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.35",
72
73
  "@deepseek-ai/cordis": "4.0.2",
73
74
  "@tanstack/react-store": "0.11.1",
74
75
  "@tanstack/store": "0.11.1",
75
76
  "typebox": "1.3.30"
76
77
  },
77
78
  "devDependencies": {
78
- "@agimon-ai/doompi-build": "0.0.1-alpha.5",
79
- "@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.71",
79
+ "@agimon-ai/doompi-build": "0.0.1-alpha.6",
80
+ "@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.72",
80
81
  "@earendil-works/pi-agent-core": "0.85.1",
81
82
  "@earendil-works/pi-coding-agent": "0.85.1",
82
83
  "@types/node": "26.5.1",