@iloom/cli 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +24 -0
  2. package/dist/{ClaudeContextManager-DK77227F.js → ClaudeContextManager-DQFKIMEP.js} +5 -5
  3. package/dist/{ClaudeService-W3SA7HVG.js → ClaudeService-CJS32WG2.js} +4 -4
  4. package/dist/{LoomLauncher-S3YGJRJQ.js → LoomLauncher-4UG2E4CD.js} +19 -24
  5. package/dist/LoomLauncher-4UG2E4CD.js.map +1 -0
  6. package/dist/MetadataManager-WXUVXKUS.js +10 -0
  7. package/dist/PRManager-7DSIMCAD.js +16 -0
  8. package/dist/{PromptTemplateManager-2TDZAUC6.js → PromptTemplateManager-72FEOGT6.js} +2 -2
  9. package/dist/README.md +24 -0
  10. package/dist/{SettingsManager-FJFU6JJD.js → SettingsManager-XPR4TEQL.js} +2 -2
  11. package/dist/agents/iloom-issue-analyze-and-plan.md +41 -7
  12. package/dist/agents/iloom-issue-analyzer.md +38 -8
  13. package/dist/agents/iloom-issue-complexity-evaluator.md +45 -15
  14. package/dist/agents/iloom-issue-enhancer.md +60 -18
  15. package/dist/agents/iloom-issue-implementer.md +29 -7
  16. package/dist/agents/iloom-issue-planner.md +36 -7
  17. package/dist/agents/iloom-issue-reviewer.md +30 -7
  18. package/dist/{chunk-JC5HXN75.js → chunk-3CMGCRB5.js} +2 -2
  19. package/dist/{chunk-G6CIIJLT.js → chunk-4YTILIIH.js} +7 -8
  20. package/dist/chunk-4YTILIIH.js.map +1 -0
  21. package/dist/{chunk-55TB3FSG.js → chunk-AS2IRKLU.js} +2 -2
  22. package/dist/{chunk-POI7KLBH.js → chunk-CDQEK2WD.js} +5 -5
  23. package/dist/{chunk-74VMN2KC.js → chunk-DKQ4SUII.js} +16 -1
  24. package/dist/chunk-DKQ4SUII.js.map +1 -0
  25. package/dist/{chunk-BIIQHEXJ.js → chunk-GVRO4PWE.js} +12 -8
  26. package/dist/chunk-GVRO4PWE.js.map +1 -0
  27. package/dist/{chunk-TMZAVPGF.js → chunk-HABINPX2.js} +71 -15
  28. package/dist/{chunk-TMZAVPGF.js.map → chunk-HABINPX2.js.map} +1 -1
  29. package/dist/{chunk-2W2FBL5G.js → chunk-LN4H3A6A.js} +66 -7
  30. package/dist/chunk-LN4H3A6A.js.map +1 -0
  31. package/dist/{chunk-VWNS6DH5.js → chunk-OOU3DKNT.js} +13 -7
  32. package/dist/chunk-OOU3DKNT.js.map +1 -0
  33. package/dist/chunk-P2ZQ5LKB.js +347 -0
  34. package/dist/chunk-P2ZQ5LKB.js.map +1 -0
  35. package/dist/{chunk-HD5SUKI2.js → chunk-RFUOIUQF.js} +49 -6
  36. package/dist/{chunk-HD5SUKI2.js.map → chunk-RFUOIUQF.js.map} +1 -1
  37. package/dist/{chunk-OF7BNW4D.js → chunk-RJKMF6BC.js} +30 -4
  38. package/dist/chunk-RJKMF6BC.js.map +1 -0
  39. package/dist/{chunk-O7WHXLCB.js → chunk-RNZMHJK7.js} +18 -4
  40. package/dist/chunk-RNZMHJK7.js.map +1 -0
  41. package/dist/{chunk-UPUAQYAW.js → chunk-S65T4O6I.js} +2 -2
  42. package/dist/{chunk-IARWMDAX.js → chunk-T5IIUG4Z.js} +98 -16
  43. package/dist/chunk-T5IIUG4Z.js.map +1 -0
  44. package/dist/{chunk-IJ7IGJT3.js → chunk-YZTDGPFB.js} +18 -1
  45. package/dist/chunk-YZTDGPFB.js.map +1 -0
  46. package/dist/{cleanup-KDLVTT7M.js → cleanup-MIDJVSIU.js} +14 -14
  47. package/dist/cli.js +228 -354
  48. package/dist/cli.js.map +1 -1
  49. package/dist/{contribute-HY372S6F.js → contribute-RS3DO3WP.js} +4 -4
  50. package/dist/{dev-server-JCJGQ3PV.js → dev-server-ASH7HJVI.js} +30 -16
  51. package/dist/dev-server-ASH7HJVI.js.map +1 -0
  52. package/dist/{feedback-7PVBQNLJ.js → feedback-RVIGHBJG.js} +5 -4
  53. package/dist/{feedback-7PVBQNLJ.js.map → feedback-RVIGHBJG.js.map} +1 -1
  54. package/dist/{git-4BVOOOOV.js → git-OQAPUPLP.js} +16 -6
  55. package/dist/git-OQAPUPLP.js.map +1 -0
  56. package/dist/{ignite-3B264M7K.js → ignite-XJALWFAT.js} +57 -22
  57. package/dist/ignite-XJALWFAT.js.map +1 -0
  58. package/dist/index.d.ts +58 -7
  59. package/dist/index.js +104 -7
  60. package/dist/index.js.map +1 -1
  61. package/dist/{init-LBA6NUK2.js → init-F6PFMSU5.js} +7 -7
  62. package/dist/init-F6PFMSU5.js.map +1 -0
  63. package/dist/mcp/recap-server.js +264 -0
  64. package/dist/mcp/recap-server.js.map +1 -0
  65. package/dist/{open-OGCV32Z4.js → open-KW4NTLXH.js} +16 -17
  66. package/dist/{open-OGCV32Z4.js.map → open-KW4NTLXH.js.map} +1 -1
  67. package/dist/{projects-P55273AB.js → projects-QEAEBAT2.js} +2 -2
  68. package/dist/prompts/init-prompt.txt +31 -72
  69. package/dist/prompts/issue-prompt.txt +115 -15
  70. package/dist/prompts/pr-prompt.txt +49 -1
  71. package/dist/prompts/regular-prompt.txt +80 -20
  72. package/dist/{rebase-4T5FQHNH.js → rebase-WZHHE5LU.js} +6 -6
  73. package/dist/recap-33NPZ3ZO.js +117 -0
  74. package/dist/recap-33NPZ3ZO.js.map +1 -0
  75. package/dist/{run-HNOP6WE2.js → run-HRYQ7TR7.js} +16 -17
  76. package/dist/{run-HNOP6WE2.js.map → run-HRYQ7TR7.js.map} +1 -1
  77. package/dist/schema/settings.schema.json +13 -2
  78. package/dist/{shell-DE3HKJSM.js → shell-JMU5XTHW.js} +6 -6
  79. package/dist/{summary-GDT7DTRI.js → summary-4SSGGH7N.js} +17 -9
  80. package/dist/summary-4SSGGH7N.js.map +1 -0
  81. package/dist/{test-git-YMAE57UP.js → test-git-6SAIRBUD.js} +4 -4
  82. package/dist/{test-prefix-YCKL6CMT.js → test-prefix-RLVRK5ZD.js} +4 -4
  83. package/package.json +1 -1
  84. package/dist/LoomLauncher-S3YGJRJQ.js.map +0 -1
  85. package/dist/chunk-2W2FBL5G.js.map +0 -1
  86. package/dist/chunk-74VMN2KC.js.map +0 -1
  87. package/dist/chunk-BIIQHEXJ.js.map +0 -1
  88. package/dist/chunk-G6CIIJLT.js.map +0 -1
  89. package/dist/chunk-IARWMDAX.js.map +0 -1
  90. package/dist/chunk-IJ7IGJT3.js.map +0 -1
  91. package/dist/chunk-O7WHXLCB.js.map +0 -1
  92. package/dist/chunk-OF7BNW4D.js.map +0 -1
  93. package/dist/chunk-QRBOPFAA.js +0 -48
  94. package/dist/chunk-QRBOPFAA.js.map +0 -1
  95. package/dist/chunk-VWNS6DH5.js.map +0 -1
  96. package/dist/dev-server-JCJGQ3PV.js.map +0 -1
  97. package/dist/ignite-3B264M7K.js.map +0 -1
  98. package/dist/summary-GDT7DTRI.js.map +0 -1
  99. /package/dist/{ClaudeContextManager-DK77227F.js.map → ClaudeContextManager-DQFKIMEP.js.map} +0 -0
  100. /package/dist/{ClaudeService-W3SA7HVG.js.map → ClaudeService-CJS32WG2.js.map} +0 -0
  101. /package/dist/{PromptTemplateManager-2TDZAUC6.js.map → MetadataManager-WXUVXKUS.js.map} +0 -0
  102. /package/dist/{SettingsManager-FJFU6JJD.js.map → PRManager-7DSIMCAD.js.map} +0 -0
  103. /package/dist/{git-4BVOOOOV.js.map → PromptTemplateManager-72FEOGT6.js.map} +0 -0
  104. /package/dist/{init-LBA6NUK2.js.map → SettingsManager-XPR4TEQL.js.map} +0 -0
  105. /package/dist/{chunk-JC5HXN75.js.map → chunk-3CMGCRB5.js.map} +0 -0
  106. /package/dist/{chunk-55TB3FSG.js.map → chunk-AS2IRKLU.js.map} +0 -0
  107. /package/dist/{chunk-POI7KLBH.js.map → chunk-CDQEK2WD.js.map} +0 -0
  108. /package/dist/{chunk-UPUAQYAW.js.map → chunk-S65T4O6I.js.map} +0 -0
  109. /package/dist/{cleanup-KDLVTT7M.js.map → cleanup-MIDJVSIU.js.map} +0 -0
  110. /package/dist/{contribute-HY372S6F.js.map → contribute-RS3DO3WP.js.map} +0 -0
  111. /package/dist/{projects-P55273AB.js.map → projects-QEAEBAT2.js.map} +0 -0
  112. /package/dist/{rebase-4T5FQHNH.js.map → rebase-WZHHE5LU.js.map} +0 -0
  113. /package/dist/{shell-DE3HKJSM.js.map → shell-JMU5XTHW.js.map} +0 -0
  114. /package/dist/{test-git-YMAE57UP.js.map → test-git-6SAIRBUD.js.map} +0 -0
  115. /package/dist/{test-prefix-YCKL6CMT.js.map → test-prefix-RLVRK5ZD.js.map} +0 -0
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- MetadataManager
4
- } from "./chunk-IJ7IGJT3.js";
5
2
  import {
6
3
  SettingsManager
7
- } from "./chunk-VWNS6DH5.js";
4
+ } from "./chunk-OOU3DKNT.js";
5
+ import {
6
+ MetadataManager
7
+ } from "./chunk-YZTDGPFB.js";
8
8
  import {
9
9
  logger
10
10
  } from "./chunk-UYVWLISQ.js";
@@ -358,7 +358,7 @@ async function getDefaultBranch(path2 = process.cwd()) {
358
358
  }
359
359
  async function findAllBranchesForIssue(issueNumber, path2 = process.cwd(), settingsManager) {
360
360
  if (!settingsManager) {
361
- const { SettingsManager: SM } = await import("./SettingsManager-FJFU6JJD.js");
361
+ const { SettingsManager: SM } = await import("./SettingsManager-XPR4TEQL.js");
362
362
  settingsManager = new SM();
363
363
  }
364
364
  const protectedBranches = await settingsManager.getProtectedBranches(path2);
@@ -449,6 +449,8 @@ async function pushBranchToRemote(branchName, worktreePath, options) {
449
449
  throw new Error(
450
450
  `Failed to push changes to origin/${branchName}
451
451
 
452
+ Git error: ${errorMessage}
453
+
452
454
  Possible causes:
453
455
  \u2022 Remote branch was deleted
454
456
  \u2022 Push was rejected (non-fast-forward)
@@ -462,6 +464,8 @@ async function pushBranchToRemote(branchName, worktreePath, options) {
462
464
  throw new Error(
463
465
  `Failed to push changes to origin/${branchName}: Network connectivity issues
464
466
 
467
+ Git error: ${errorMessage}
468
+
465
469
  Check your internet connection and try again.`
466
470
  );
467
471
  }
@@ -469,6 +473,8 @@ async function pushBranchToRemote(branchName, worktreePath, options) {
469
473
  throw new Error(
470
474
  `Failed to push changes: Remote 'origin' not found
471
475
 
476
+ Git error: ${errorMessage}
477
+
472
478
  Configure remote: git remote add origin <url>`
473
479
  );
474
480
  }
@@ -614,6 +620,54 @@ async function getMergeTargetBranch(worktreePath = process.cwd(), options) {
614
620
  logger.debug(`Using configured main branch as merge target: ${mainBranch}`);
615
621
  return mainBranch;
616
622
  }
623
+ var PLACEHOLDER_COMMIT_PREFIX = "[iloom-temp]";
624
+ async function isPlaceholderCommit(cwd = process.cwd()) {
625
+ try {
626
+ const subject = await executeGitCommand(["log", "-1", "--format=%s", "HEAD"], { cwd });
627
+ return subject.trim().startsWith(PLACEHOLDER_COMMIT_PREFIX);
628
+ } catch {
629
+ return false;
630
+ }
631
+ }
632
+ async function findPlaceholderCommitSha(worktreePath) {
633
+ try {
634
+ const log = await executeGitCommand(
635
+ ["log", "--format=%H", "--fixed-strings", "--grep", PLACEHOLDER_COMMIT_PREFIX, "-n", "1"],
636
+ { cwd: worktreePath }
637
+ );
638
+ const sha = log.trim();
639
+ if (sha.length === 0) {
640
+ return null;
641
+ }
642
+ const subject = await executeGitCommand(
643
+ ["log", "-1", "--format=%s", sha],
644
+ { cwd: worktreePath }
645
+ );
646
+ if (!subject.trim().startsWith(PLACEHOLDER_COMMIT_PREFIX)) {
647
+ return null;
648
+ }
649
+ return sha;
650
+ } catch {
651
+ return null;
652
+ }
653
+ }
654
+ async function removePlaceholderCommitFromHead(worktreePath) {
655
+ if (!await isPlaceholderCommit(worktreePath)) {
656
+ return false;
657
+ }
658
+ await executeGitCommand(["reset", "--soft", "HEAD~1"], { cwd: worktreePath });
659
+ return true;
660
+ }
661
+ async function removePlaceholderCommitFromHistory(worktreePath, placeholderSha) {
662
+ const parentSha = await executeGitCommand(
663
+ ["rev-parse", `${placeholderSha}^`],
664
+ { cwd: worktreePath }
665
+ );
666
+ await executeGitCommand(
667
+ ["rebase", "--onto", parentSha.trim(), placeholderSha],
668
+ { cwd: worktreePath }
669
+ );
670
+ }
617
671
 
618
672
  export {
619
673
  executeGitCommand,
@@ -642,6 +696,11 @@ export {
642
696
  isBranchMergedIntoMain,
643
697
  isRemoteBranchUpToDate,
644
698
  checkRemoteBranchStatus,
645
- getMergeTargetBranch
699
+ getMergeTargetBranch,
700
+ PLACEHOLDER_COMMIT_PREFIX,
701
+ isPlaceholderCommit,
702
+ findPlaceholderCommitSha,
703
+ removePlaceholderCommitFromHead,
704
+ removePlaceholderCommitFromHistory
646
705
  };
647
- //# sourceMappingURL=chunk-2W2FBL5G.js.map
706
+ //# sourceMappingURL=chunk-LN4H3A6A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/git.ts"],"sourcesContent":["import path from 'path'\nimport { execa, type ExecaError } from 'execa'\nimport { type GitWorktree } from '../types/worktree.js'\nimport { SettingsManager, type SettingsManager as SettingsManagerType } from '../lib/SettingsManager.js'\nimport { MetadataManager } from '../lib/MetadataManager.js'\nimport { logger } from './logger.js'\n\n/**\n * Execute a Git command and return the stdout result\n * Throws an error if the command fails\n */\nexport async function executeGitCommand(\n args: string[],\n options?: { cwd?: string; timeout?: number; stdio?: 'inherit' | 'pipe'; env?: NodeJS.ProcessEnv }\n): Promise<string> {\n try {\n const result = await execa('git', args, {\n cwd: options?.cwd ?? process.cwd(),\n timeout: options?.timeout ?? 30000,\n encoding: 'utf8',\n stdio: options?.stdio ?? 'pipe',\n verbose: logger.isDebugEnabled(),\n // Spread env conditionally - only include if defined\n ...(options?.env && { env: options.env }),\n })\n\n return result.stdout\n } catch (error) {\n const execaError = error as ExecaError\n const stderr = execaError.stderr ?? execaError.message ?? 'Unknown Git error'\n throw new Error(`Git command failed: ${stderr}`)\n }\n}\n\n/**\n * Parse git worktree list output into structured data\n * @param output - The output from git worktree list --porcelain\n * @param defaultBranch - Default branch name to use for bare repositories (defaults to 'main')\n */\nexport function parseWorktreeList(output: string, defaultBranch?: string): GitWorktree[] {\n const worktrees: GitWorktree[] = []\n const lines = output.trim().split('\\n')\n\n let i = 0\n while (i < lines.length) {\n const pathLine = lines[i]\n if (!pathLine?.startsWith('worktree ')) {\n i++\n continue\n }\n\n // Parse path line: \"worktree /path/to/worktree\"\n const pathMatch = pathLine.match(/^worktree (.+)$/)\n if (!pathMatch) {\n i++\n continue\n }\n\n let branch = ''\n let commit = ''\n let detached = false\n let bare = false\n let locked = false\n let lockReason: string | undefined\n\n // Process subsequent lines for this worktree\n i++\n while (i < lines.length && !lines[i]?.startsWith('worktree ')) {\n const line = lines[i]?.trim()\n if (!line) {\n i++\n continue\n }\n\n if (line === 'bare') {\n bare = true\n branch = defaultBranch ?? 'main' // Default assumption for bare repo\n } else if (line === 'detached') {\n detached = true\n branch = 'HEAD'\n } else if (line.startsWith('locked')) {\n locked = true\n const lockMatch = line.match(/^locked (.+)$/)\n lockReason = lockMatch?.[1]\n branch = branch || 'unknown'\n } else if (line.startsWith('HEAD ')) {\n // Parse commit line: \"HEAD abc123def456...\"\n const commitMatch = line.match(/^HEAD ([a-f0-9]+)/)\n if (commitMatch) {\n commit = commitMatch[1] ?? ''\n }\n } else if (line.startsWith('branch ')) {\n // Parse branch line: \"branch refs/heads/feature-branch\"\n const branchMatch = line.match(/^branch refs\\/heads\\/(.+)$/)\n branch = branchMatch?.[1] ?? line.replace('branch ', '')\n }\n\n i++\n }\n\n const worktree: GitWorktree = {\n path: pathMatch[1] ?? '',\n branch,\n commit,\n bare,\n detached,\n locked,\n }\n\n if (lockReason !== undefined) {\n worktree.lockReason = lockReason\n }\n\n worktrees.push(worktree)\n }\n\n return worktrees\n}\n\n/**\n * Check if a branch name follows PR naming patterns\n */\nexport function isPRBranch(branchName: string): boolean {\n const prPatterns = [\n /^pr\\/\\d+/i, // pr/123, pr/123-feature-name\n /^pull\\/\\d+/i, // pull/123\n /^\\d+[-_]/, // 123-feature-name, 123_feature_name\n /^feature\\/pr[-_]?\\d+/i, // feature/pr123, feature/pr-123\n /^hotfix\\/pr[-_]?\\d+/i, // hotfix/pr123\n ]\n\n return prPatterns.some(pattern => pattern.test(branchName))\n}\n\n/**\n * Extract PR number from branch name\n */\nexport function extractPRNumber(branchName: string): number | null {\n const patterns = [\n /^pr\\/(\\d+)/i, // pr/123\n /^pull\\/(\\d+)/i, // pull/123\n /^(\\d+)[-_]/, // 123-feature-name\n /^feature\\/pr[-_]?(\\d+)/i, // feature/pr123\n /^hotfix\\/pr[-_]?(\\d+)/i, // hotfix/pr123\n /pr[-_]?(\\d+)/i, // anywhere with pr123 or pr-123\n ]\n\n for (const pattern of patterns) {\n const match = branchName.match(pattern)\n if (match?.[1]) {\n const num = parseInt(match[1], 10)\n if (!isNaN(num)) return num\n }\n }\n\n return null\n}\n\n/**\n * Extract issue number from branch name\n * Supports both new format (issue-{issueId}__{slug}) and old format (issue-{number}-{slug})\n * @returns string issue ID (alphanumeric) or null if not found\n */\nexport function extractIssueNumber(branchName: string): string | null {\n // Priority 1: New format - issue-{issueId}__ (alphanumeric ID with double underscore)\n const newFormatPattern = /issue-([^_]+)__/i\n const newMatch = branchName.match(newFormatPattern)\n if (newMatch?.[1]) return newMatch[1]\n\n // Priority 2: Old format - issue-{number}- or issue-{number}$ (numeric only, dash or end)\n const oldFormatPattern = /issue-(\\d+)(?:-|$)/i\n const oldMatch = branchName.match(oldFormatPattern)\n if (oldMatch?.[1]) return oldMatch[1]\n\n // Priority 3: Alphanumeric ID at end (either format without description)\n const alphanumericEndPattern = /issue-([^_\\s/]+)$/i\n const alphanumericMatch = branchName.match(alphanumericEndPattern)\n if (alphanumericMatch?.[1]) return alphanumericMatch[1]\n\n // Priority 4: Legacy patterns (issue_N, leading number)\n const legacyPatterns = [\n /issue_(\\d+)/i, // issue_42\n /^(\\d+)-/, // 42-feature-name\n ]\n for (const pattern of legacyPatterns) {\n const match = branchName.match(pattern)\n if (match?.[1]) return match[1]\n }\n\n return null\n}\n\n/**\n * Check if a path follows worktree naming patterns\n */\nexport function isWorktreePath(path: string): boolean {\n const worktreePatterns = [\n /\\/worktrees?\\//i, // Contains /worktree/ or /worktrees/\n /\\/workspace[-_]?\\d+/i, // workspace123, workspace-123\n /\\/issue[-_]?\\d+/i, // issue123, issue-123\n /\\/pr[-_]?\\d+/i, // pr123, pr-123\n /-worktree$/i, // ends with -worktree\n /\\.worktree$/i, // ends with .worktree\n ]\n\n return worktreePatterns.some(pattern => pattern.test(path))\n}\n\n/**\n * Generate a worktree path based on branch name and root directory\n * For PRs, adds _pr_<PR_NUM> suffix to distinguish from issue branches\n */\nexport function generateWorktreePath(\n branchName: string,\n rootDir: string = process.cwd(),\n options?: { isPR?: boolean; prNumber?: number; prefix?: string }\n): string {\n // Replace slashes with dashes (matches bash line 593)\n let sanitized = branchName.replace(/\\//g, '-')\n\n // Add PR suffix if this is a PR (matches bash lines 595-597)\n if (options?.isPR && options?.prNumber) {\n sanitized = `${sanitized}_pr_${options.prNumber}`\n }\n\n const parentDir = path.dirname(rootDir)\n\n // Handle prefix logic\n let prefix: string\n\n if (options?.prefix === undefined) {\n // No prefix in options - calculate default: <basename>-looms\n const mainFolderName = path.basename(rootDir)\n prefix = mainFolderName ? `${mainFolderName}-looms/` : 'looms/'\n } else if (options.prefix === '') {\n // Empty string = no prefix mode\n prefix = ''\n } else {\n // Custom prefix provided\n prefix = options.prefix\n\n // Check if prefix contains forward slashes (nested directory structure)\n const hasNestedPath = prefix.includes('/')\n\n if (hasNestedPath) {\n // Check if it ends with a separator character (dash, underscore, or slash)\n const endsWithSeparator = /[-_/]$/.test(prefix)\n\n if (!endsWithSeparator) {\n // Has nested path but no trailing separator: auto-append hyphen\n // Example: \"temp/looms\" becomes \"temp/looms-\"\n prefix = `${prefix}-`\n }\n // If it already ends with -, _, or /, keep as-is\n } else {\n // Single-level prefix: auto-append separator if it doesn't end with one\n const endsWithSeparator = /[-_]$/.test(prefix)\n if (!endsWithSeparator) {\n prefix = `${prefix}-`\n }\n }\n }\n\n // Apply prefix (or not, if empty)\n if (prefix === '') {\n return path.join(parentDir, sanitized)\n } else if (prefix.endsWith('/')) {\n // Forward slash = nested directory, use path.join for proper handling\n return path.join(parentDir, prefix, sanitized)\n } else if (prefix.includes('/')) {\n // Contains slash but doesn't end with slash = nested with separator (e.g., \"looms/myprefix-\")\n // Split and handle: last part is prefix with separator, rest is directory path\n const lastSlashIndex = prefix.lastIndexOf('/')\n const dirPath = prefix.substring(0, lastSlashIndex)\n const prefixWithSeparator = prefix.substring(lastSlashIndex + 1)\n return path.join(parentDir, dirPath, `${prefixWithSeparator}${sanitized}`)\n } else {\n // Dash/underscore separator = single directory name\n return path.join(parentDir, `${prefix}${sanitized}`)\n }\n}\n\n/**\n * Validate that a directory is a valid Git repository\n */\nexport async function isValidGitRepo(path: string): Promise<boolean> {\n try {\n await executeGitCommand(['rev-parse', '--git-dir'], { cwd: path })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Get the current branch name for a repository\n */\nexport async function getCurrentBranch(path: string = process.cwd()): Promise<string | null> {\n try {\n const result = await executeGitCommand(['branch', '--show-current'], { cwd: path })\n return result.trim()\n } catch {\n return null\n }\n}\n\n/**\n * Check if a branch exists (local or remote)\n */\nexport async function branchExists(\n branchName: string,\n path: string = process.cwd(),\n includeRemote = true\n): Promise<boolean> {\n try {\n // Check local branches\n const localResult = await executeGitCommand(['branch', '--list', branchName], { cwd: path })\n if (localResult.trim()) {\n return true\n }\n\n // Check remote branches if requested\n if (includeRemote) {\n const remoteResult = await executeGitCommand(['branch', '-r', '--list', `*/${branchName}`], {\n cwd: path,\n })\n if (remoteResult.trim()) {\n return true\n }\n }\n\n return false\n } catch {\n return false\n }\n}\n\n/**\n * Get the root directory of the current worktree\n * Returns the worktree root when in a linked worktree, or main repo root when in main worktree\n */\nexport async function getWorktreeRoot(path: string = process.cwd()): Promise<string | null> {\n try {\n const result = await executeGitCommand(['rev-parse', '--show-toplevel'], { cwd: path })\n return result.trim()\n } catch {\n return null\n }\n}\n\n/**\n * Get the main repository root directory\n * Returns the main repo root even when called from a linked worktree\n */\nexport async function getRepoRoot(path: string = process.cwd()): Promise<string | null> {\n try {\n // Get the common git directory (shared by all worktrees)\n const gitCommonDir = await executeGitCommand(\n ['rev-parse', '--path-format=absolute', '--git-common-dir'],\n { cwd: path }\n )\n const trimmedPath = gitCommonDir.trim()\n\n // Handle linked worktree: /path/to/repo/.git/worktrees/worktree-name -> /path/to/repo\n // Handle main worktree: /path/to/repo/.git -> /path/to/repo\n const repoRoot = trimmedPath\n .replace(/\\/\\.git\\/worktrees\\/[^/]+$/, '') // Remove /.git/worktrees/name suffix\n .replace(/\\/\\.git$/, '') // Remove /.git suffix\n\n return repoRoot\n } catch(error) {\n logger.warn(`Failed to determine repo root from git-common-dir: ${path}`, error instanceof Error ? error.message : String(error))\n return null\n }\n}\n\n/**\n * Find the worktree path where main branch is checked out\n * Copies bash script approach: parse git worktree list to find main\n */\nexport async function findMainWorktreePath(\n path: string = process.cwd(),\n options?: { mainBranch?: string }\n): Promise<string> {\n try {\n const output = await executeGitCommand(['worktree', 'list', '--porcelain'], { cwd: path })\n const worktrees = parseWorktreeList(output, options?.mainBranch)\n\n // Guard: empty worktree list\n if (worktrees.length === 0) {\n throw new Error('No worktrees found in repository')\n }\n\n // Tier 1: Check for specified mainBranch in options\n if (options?.mainBranch) {\n const specified = worktrees.find(wt => wt.branch === options.mainBranch)\n if (!specified?.path) {\n throw new Error(\n `No worktree found with branch '${options.mainBranch}' (specified in settings). Available worktrees: ${worktrees.map(wt => `${wt.path} (${wt.branch})`).join(', ')}`\n )\n }\n return specified.path\n }\n\n // Tier 2: Look for \"main\" branch\n const mainBranch = worktrees.find(wt => wt.branch === 'main')\n if (mainBranch?.path) {\n return mainBranch.path\n }\n\n // Tier 3: Use first worktree (primary worktree)\n const firstWorktree = worktrees[0]\n if (!firstWorktree?.path) {\n throw new Error('Failed to determine primary worktree path')\n }\n return firstWorktree.path\n } catch (error) {\n if (\n error instanceof Error &&\n (error.message.includes('No worktree found with branch') ||\n error.message.includes('No worktrees found') ||\n error.message.includes('Failed to determine primary worktree'))\n ) {\n // Re-throw our specific errors\n throw error\n }\n throw new Error(`Failed to find main worktree: ${error instanceof Error ? error.message : String(error)}`)\n }\n}\n\n/**\n * Find main worktree path with automatic settings loading\n *\n * This is a convenience wrapper that:\n * 1. Loads project settings from .iloom/settings.json\n * 2. Extracts mainBranch configuration if present\n * 3. Calls findMainWorktreePath with appropriate options\n *\n * @param path - Path to search from (defaults to process.cwd())\n * @param settingsManager - Optional SettingsManager instance (for DI/testing)\n * @returns Path to main worktree\n * @throws Error if main worktree cannot be found\n */\nexport async function findMainWorktreePathWithSettings(\n path?: string,\n settingsManager?: SettingsManagerType\n): Promise<string> {\n // Lazy load SettingsManager to avoid circular dependencies\n settingsManager ??= new SettingsManager()\n\n const settings = await settingsManager.loadSettings(path)\n const findOptions = settings.mainBranch ? { mainBranch: settings.mainBranch } : undefined\n return findMainWorktreePath(path, findOptions)\n}\n\n/**\n * Find the worktree path where a specific branch is checked out\n *\n * Used by MergeManager to find the correct worktree for child loom merges.\n * When finishing a child loom, we need to find where the PARENT branch is\n * checked out (the merge target), not where settings.mainBranch is checked out.\n *\n * @param branchName - The branch name to find\n * @param path - Path to search from (defaults to process.cwd())\n * @returns Path to worktree where the branch is checked out\n * @throws Error if no worktree has the specified branch checked out\n */\nexport async function findWorktreeForBranch(\n branchName: string,\n path: string = process.cwd()\n): Promise<string> {\n try {\n const output = await executeGitCommand(['worktree', 'list', '--porcelain'], { cwd: path })\n const worktrees = parseWorktreeList(output, branchName)\n\n // Guard: empty worktree list\n if (worktrees.length === 0) {\n throw new Error('No worktrees found in repository')\n }\n\n // Find the worktree with the specified branch\n const targetWorktree = worktrees.find(wt => wt.branch === branchName)\n if (!targetWorktree?.path) {\n throw new Error(\n `No worktree found with branch '${branchName}' checked out. ` +\n `Available worktrees: ${worktrees.map(wt => `${wt.path} (${wt.branch})`).join(', ')}`\n )\n }\n return targetWorktree.path\n } catch (error) {\n if (\n error instanceof Error &&\n (error.message.includes('No worktree found with branch') ||\n error.message.includes('No worktrees found'))\n ) {\n // Re-throw our specific errors\n throw error\n }\n throw new Error(`Failed to find worktree for branch '${branchName}': ${error instanceof Error ? error.message : String(error)}`)\n }\n}\n\n/**\n * Check if there are uncommitted changes in a repository\n */\nexport async function hasUncommittedChanges(path: string = process.cwd()): Promise<boolean> {\n try {\n const result = await executeGitCommand(['status', '--porcelain'], { cwd: path })\n return result.trim().length > 0\n } catch {\n return false\n }\n}\n\n/**\n * Get the default branch name for a repository\n */\nexport async function getDefaultBranch(path: string = process.cwd()): Promise<string> {\n try {\n // Try to get from remote\n const remoteResult = await executeGitCommand(['symbolic-ref', 'refs/remotes/origin/HEAD'], {\n cwd: path,\n })\n const match = remoteResult.match(/refs\\/remotes\\/origin\\/(.+)/)\n if (match) return match[1] ?? 'main'\n\n // Fallback to common default branch names\n const commonDefaults = ['main', 'master', 'develop']\n for (const branch of commonDefaults) {\n if (await branchExists(branch, path)) {\n return branch\n }\n }\n\n return 'main' // Final fallback\n } catch {\n return 'main'\n }\n}\n\n/**\n * Find all branches related to a GitHub issue or PR number\n * Matches patterns like:\n * - Issue patterns: issue-25, issue/25, 25-feature, feat-25, feat/issue-25\n * - PR patterns: pr/25, pull/25, pr-25, feature/pr-25\n *\n * Based on bash cleanup-worktree.sh find_issue_branches() (lines 133-154)\n *\n * @param issueNumber - The issue or PR number to search for\n * @param path - Working directory to search from (defaults to process.cwd())\n * @param settingsManager - Optional SettingsManager instance (for DI/testing)\n */\nexport async function findAllBranchesForIssue(\n issueNumber: string | number,\n path: string = process.cwd(),\n settingsManager?: SettingsManagerType\n): Promise<string[]> {\n // Lazy load SettingsManager to avoid circular dependencies\n if (!settingsManager) {\n const { SettingsManager: SM } = await import('../lib/SettingsManager.js')\n settingsManager = new SM()\n }\n\n // Get protected branches list from centralized method\n const protectedBranches = await settingsManager.getProtectedBranches(path)\n\n // Get all branches (local and remote)\n const output = await executeGitCommand(['branch', '-a'], { cwd: path })\n\n const branches: string[] = []\n const lines = output.split('\\n').filter(Boolean)\n\n for (const line of lines) {\n // Skip remotes/origin/HEAD pointer\n if (line.includes('remotes/origin/HEAD')) {\n continue\n }\n\n // Clean the branch name:\n // 1. Remove git status markers (* + spaces at start)\n let cleanBranch = line.replace(/^[*+ ]+/, '')\n\n // 2. Remove 'origin/' prefix if present\n cleanBranch = cleanBranch.replace(/^origin\\//, '')\n\n // 3. Remove 'remotes/origin/' prefix if present\n cleanBranch = cleanBranch.replace(/^remotes\\/origin\\//, '')\n\n // 4. Trim any remaining whitespace\n cleanBranch = cleanBranch.trim()\n\n // Skip protected branches\n if (protectedBranches.includes(cleanBranch)) {\n continue\n }\n\n // Check if branch contains issue number with strict word boundary pattern\n // The issue number must NOT be:\n // - Part of a larger number (preceded or followed by a digit)\n // - After an unknown word (like \"tissue-25\")\n // The issue number CAN be:\n // - At start: \"25-feature\"\n // - After known prefix + separator: \"issue-25\", \"feat-25\", \"fix-25\", \"pr-25\"\n // - After just a separator with no prefix: test_25 (separator at start)\n\n // First check: not part of a larger number\n const notPartOfNumber = new RegExp(`(?<!\\\\d)${issueNumber}(?!\\\\d)`)\n if (!notPartOfNumber.test(cleanBranch)) {\n continue\n }\n\n // Second check: if preceded by letters, validate they're known issue-related prefixes\n // This prevents \"tissue-25\" but allows \"issue-25\", \"feat-25\", etc.\n const beforeNumber = cleanBranch.substring(0, cleanBranch.indexOf(String(issueNumber)))\n\n if (beforeNumber) {\n // Extract the last word (letters) before the number\n const lastWord = beforeNumber.match(/([a-zA-Z]+)[-_/\\s]*$/)\n if (lastWord?.[1]) {\n const word = lastWord[1].toLowerCase()\n // Known prefixes for issue-related branches\n const knownPrefixes = [\n 'issue', 'issues',\n 'feat', 'feature', 'features',\n 'fix', 'fixes', 'bugfix', 'hotfix',\n 'pr', 'pull',\n 'test', 'tests',\n 'chore',\n 'docs',\n 'refactor',\n 'perf',\n 'style',\n 'ci',\n 'build',\n 'revert'\n ]\n\n // If we found a word and it's NOT in the known list, skip this branch\n if (!knownPrefixes.includes(word)) {\n continue\n }\n }\n }\n\n // Passed all checks - add to results\n if (!branches.includes(cleanBranch)) {\n branches.push(cleanBranch)\n }\n }\n\n return branches\n}\n\n/**\n * Check if a repository is empty (has no commits yet)\n * @param path - Repository path to check (defaults to process.cwd())\n * @returns true if repository has no commits, false otherwise\n */\nexport async function isEmptyRepository(path: string = process.cwd()): Promise<boolean> {\n try {\n await executeGitCommand(['rev-parse', '--verify', 'HEAD'], { cwd: path })\n return false // HEAD exists, repo has commits\n } catch {\n return true // HEAD doesn't exist, repo is empty\n }\n}\n\n/**\n * Ensure repository has at least one commit\n * Creates an initial empty commit if repository is empty\n * @param path - Repository path (defaults to process.cwd())\n */\nexport async function ensureRepositoryHasCommits(path: string = process.cwd()): Promise<void> {\n const isEmpty = await isEmptyRepository(path)\n if (isEmpty) {\n await executeGitCommand(['commit', '--no-verify', '--allow-empty', '-m', 'Initial commit'], { cwd: path })\n }\n}\n\n/**\n * Push a branch to remote repository\n * Used for PR workflow to push changes to remote without merging locally\n *\n * @param branchName - The branch name to push\n * @param worktreePath - The worktree path where the branch is checked out\n * @param options - Push options\n * @throws Error if push fails\n */\nexport async function pushBranchToRemote(\n branchName: string,\n worktreePath: string,\n options?: { dryRun?: boolean }\n): Promise<void> {\n if (options?.dryRun) {\n // In dry-run mode, just log what would be done\n return\n }\n\n try {\n // Execute: git push origin <branch-name>\n // This matches the bash script behavior (merge-and-clean.sh line 359)\n await executeGitCommand(['push', 'origin', branchName], {\n cwd: worktreePath,\n timeout: 120000, // 120 second timeout for push operations\n })\n } catch (error) {\n // Provide helpful error message based on common push failures\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n // Check for common error patterns and provide context, but ALWAYS include original error\n if (errorMessage.includes('failed to push') || errorMessage.includes('rejected')) {\n throw new Error(\n `Failed to push changes to origin/${branchName}\\n\\n` +\n ` Git error: ${errorMessage}\\n\\n` +\n ` Possible causes:\\n` +\n ` • Remote branch was deleted\\n` +\n ` • Push was rejected (non-fast-forward)\\n` +\n ` • Network connectivity issues\\n\\n` +\n ` To retry: il finish --pr <number>\\n` +\n ` To force push: git push origin ${branchName} --force`\n )\n }\n\n if (errorMessage.includes('Could not resolve host') || errorMessage.includes('network')) {\n throw new Error(\n `Failed to push changes to origin/${branchName}: Network connectivity issues\\n\\n` +\n ` Git error: ${errorMessage}\\n\\n` +\n ` Check your internet connection and try again.`\n )\n }\n\n if (errorMessage.includes('No such remote')) {\n throw new Error(\n `Failed to push changes: Remote 'origin' not found\\n\\n` +\n ` Git error: ${errorMessage}\\n\\n` +\n ` Configure remote: git remote add origin <url>`\n )\n }\n\n // For other errors, re-throw with original message\n throw new Error(`Failed to push to remote: ${errorMessage}`)\n }\n}\n\n/**\n * Check if a file is tracked by git\n * Uses git ls-files to check if file is in the index\n * @param filePath - Absolute or relative path to the file\n * @param cwd - Working directory (defaults to process.cwd())\n * @returns true if file is tracked, false otherwise\n */\nexport async function isFileTrackedByGit(\n filePath: string,\n cwd: string = process.cwd()\n): Promise<boolean> {\n try {\n const result = await executeGitCommand(\n ['ls-files', '--error-unmatch', filePath],\n { cwd }\n )\n return result.trim().length > 0\n } catch (error) {\n // Only return false if it's the specific \"pathspec did not match\" error\n const errorMessage = error instanceof Error ? error.message : String(error)\n if (errorMessage.includes('pathspec') && errorMessage.includes('did not match')) {\n return false\n }\n // Re-throw other errors\n throw error\n }\n}\n\n/**\n * Check if a file is gitignored\n * Uses `git check-ignore` which handles nested gitignore files and global patterns\n *\n * @param filePath - Path to file to check (relative to repo root)\n * @param cwd - Working directory (defaults to process.cwd())\n * @returns true if file IS ignored, false if NOT ignored or on error\n */\nexport async function isFileGitignored(\n filePath: string,\n cwd: string = process.cwd()\n): Promise<boolean> {\n try {\n await executeGitCommand(['check-ignore', '-q', filePath], { cwd })\n return true // Exit 0 = file IS ignored\n } catch {\n return false // Exit 1 = NOT ignored (or error)\n }\n}\n\n/**\n * Check if a branch is merged into the main branch\n *\n * Uses `git merge-base --is-ancestor` which is more reliable than `git branch -d`'s check.\n * The `-d` flag checks against current HEAD, which can give false positives when:\n * - Running from a worktree where main isn't checked out\n * - Squash or rebase merges were used\n *\n * This function explicitly checks if the branch tip is an ancestor of the main branch,\n * providing consistent results regardless of which worktree the command runs from.\n *\n * @param branchName - The branch to check\n * @param mainBranch - The main branch to check against (defaults to 'main')\n * @param cwd - Working directory (defaults to process.cwd())\n * @returns true if branch is merged into main, false otherwise\n */\nexport async function isBranchMergedIntoMain(\n branchName: string,\n mainBranch: string = 'main',\n cwd: string = process.cwd()\n): Promise<boolean> {\n try {\n // git merge-base --is-ancestor exits 0 if branchName is ancestor of mainBranch, 1 if not\n await executeGitCommand(['merge-base', '--is-ancestor', branchName, mainBranch], { cwd })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Check if a branch exists on the remote (origin) and is up-to-date with local\n * Useful for GitHub-PR workflows to ensure branch has been pushed and is current\n *\n * @param branchName - Name of the branch to check\n * @param cwd - Working directory to run git command in\n * @returns Promise<boolean> - true if remote branch exists and matches local HEAD, false otherwise\n */\nexport async function isRemoteBranchUpToDate(\n branchName: string,\n cwd: string\n): Promise<boolean> {\n try {\n // First, check if remote branch exists and get its commit hash\n const remoteResult = await executeGitCommand(['ls-remote', '--heads', 'origin', branchName], { cwd })\n\n if (remoteResult.trim().length === 0) {\n // Remote branch doesn't exist\n return false\n }\n\n // Extract the commit hash from ls-remote output (format: \"hash\\trefs/heads/branchname\")\n const remoteCommit = remoteResult.trim().split('\\t')[0]\n\n // Get the local branch's HEAD commit\n const localCommit = await executeGitCommand(['rev-parse', branchName], { cwd })\n\n // Both must exist and match\n return localCommit.trim() === remoteCommit\n } catch {\n return false\n }\n}\n\n/**\n * Result of checking remote branch status for safety validation\n */\nexport interface RemoteBranchStatus {\n /** Whether the remote branch exists */\n exists: boolean\n /** Whether the remote is ahead of local (has commits not present locally) */\n remoteAhead: boolean\n /** Whether local is ahead of remote (has unpushed commits) */\n localAhead: boolean\n /** Whether a network error occurred during the check */\n networkError: boolean\n /** Error message if network error occurred */\n errorMessage?: string\n}\n\n/**\n * Check the status of a remote branch for safety validation during cleanup\n * This function provides detailed status needed for the 5-point safety check:\n *\n * The key insight: we care about DATA LOSS, not about remote state\n * - Remote ahead of local is SAFE (commits exist on remote, no data loss)\n * - Local ahead of remote is DANGEROUS (unpushed commits would be lost)\n *\n * 5-point safety logic:\n * 1. Network error -> BLOCK (can't verify safety)\n * 2. Remote ahead of local -> OK (no data loss - commits exist on remote)\n * 3. Local ahead of remote (unpushed commits) -> BLOCK (data loss risk)\n * 4. No remote, merged to main -> OK (work is in main)\n * 5. No remote, NOT merged to main -> BLOCK (unmerged work would be lost)\n *\n * @param branchName - Name of the branch to check\n * @param cwd - Working directory to run git command in\n * @returns Promise<RemoteBranchStatus> - Detailed status of the remote branch\n */\nexport async function checkRemoteBranchStatus(\n branchName: string,\n cwd: string\n): Promise<RemoteBranchStatus> {\n try {\n // First, fetch to ensure we have the latest remote refs\n // This is important to accurately detect if remote is ahead\n try {\n await executeGitCommand(['fetch', 'origin', branchName], { cwd, timeout: 30000 })\n } catch (fetchError) {\n // Fetch failing for a specific branch is OK - branch might not exist on remote\n // We'll detect this in the ls-remote call\n const fetchErrorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError)\n\n // Check if this is a network error vs branch not found\n if (fetchErrorMessage.includes('Could not resolve host') ||\n fetchErrorMessage.includes('unable to access') ||\n fetchErrorMessage.includes('network') ||\n fetchErrorMessage.includes('Connection refused') ||\n fetchErrorMessage.includes('Connection timed out')) {\n return {\n exists: false,\n remoteAhead: false,\n localAhead: false,\n networkError: true,\n errorMessage: fetchErrorMessage\n }\n }\n // Otherwise continue - branch might just not exist on remote\n }\n\n // Check if remote branch exists using ls-remote\n const remoteResult = await executeGitCommand(['ls-remote', '--heads', 'origin', branchName], { cwd })\n\n if (remoteResult.trim().length === 0) {\n // Remote branch doesn't exist\n return {\n exists: false,\n remoteAhead: false,\n localAhead: false,\n networkError: false\n }\n }\n\n // Remote branch exists - check if it's ahead of local\n // Extract the remote commit hash\n const remoteCommit = remoteResult.trim().split('\\t')[0]\n\n // Guard against undefined (shouldn't happen but TypeScript wants it)\n if (!remoteCommit) {\n return {\n exists: false,\n remoteAhead: false,\n localAhead: false,\n networkError: false\n }\n }\n\n // Get the local branch's HEAD commit\n const localCommit = await executeGitCommand(['rev-parse', branchName], { cwd })\n const localCommitTrimmed = localCommit.trim()\n\n if (remoteCommit === localCommitTrimmed) {\n // Remote and local are at the same commit - safe (no unpushed commits)\n return {\n exists: true,\n remoteAhead: false,\n localAhead: false,\n networkError: false\n }\n }\n\n // Commits differ - check if remote is ahead of local\n // Use merge-base to find common ancestor, then compare\n try {\n // Check if localCommit is an ancestor of remoteCommit (meaning remote is ahead)\n await executeGitCommand(['merge-base', '--is-ancestor', localCommitTrimmed, remoteCommit], { cwd })\n // If we get here, local IS an ancestor of remote, meaning remote is ahead\n // This is SAFE - no data loss because commits exist on remote\n return {\n exists: true,\n remoteAhead: true,\n localAhead: false,\n networkError: false\n }\n } catch {\n // Local is NOT an ancestor of remote\n // This means local is ahead or branches have diverged\n // Either way, local has unpushed commits - this is DANGEROUS (data loss risk)\n return {\n exists: true,\n remoteAhead: false,\n localAhead: true,\n networkError: false\n }\n }\n } catch (error) {\n // Check if this is a network error\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n if (errorMessage.includes('Could not resolve host') ||\n errorMessage.includes('unable to access') ||\n errorMessage.includes('network') ||\n errorMessage.includes('Connection refused') ||\n errorMessage.includes('Connection timed out')) {\n return {\n exists: false,\n remoteAhead: false,\n localAhead: false,\n networkError: true,\n errorMessage\n }\n }\n\n // For other errors, assume remote doesn't exist\n return {\n exists: false,\n remoteAhead: false,\n localAhead: false,\n networkError: false\n }\n }\n}\n\n/**\n * Get the merge target branch for a loom\n * Priority: parent loom metadata (parentLoom.branchName) > configured main branch > 'main'\n *\n * This is the shared utility for determining where a branch should merge to.\n * Child looms merge to their parent branch, standalone looms merge to main.\n *\n * @param worktreePath - Path to load metadata/settings from (defaults to process.cwd())\n * @param options - Optional dependency injection for testing\n * @returns The branch name to merge into\n */\nexport async function getMergeTargetBranch(\n worktreePath: string = process.cwd(),\n options?: {\n settingsManager?: SettingsManagerType\n metadataManager?: MetadataManager\n }\n): Promise<string> {\n const settingsManager = options?.settingsManager ?? new SettingsManager()\n const metadataManager = options?.metadataManager ?? new MetadataManager()\n\n // Check for parent loom metadata first (child looms merge to parent)\n logger.debug(`Checking for parent loom metadata at: ${worktreePath}`)\n const metadata = await metadataManager.readMetadata(worktreePath)\n if (metadata?.parentLoom?.branchName) {\n logger.debug(`Using parent branch as merge target: ${metadata.parentLoom.branchName}`)\n return metadata.parentLoom.branchName\n }\n logger.debug('No parent loom metadata found, falling back to settings')\n\n // Fall back to configured main branch\n const settings = await settingsManager.loadSettings(worktreePath)\n const mainBranch = settings.mainBranch ?? 'main'\n logger.debug(`Using configured main branch as merge target: ${mainBranch}`)\n return mainBranch\n}\n\n/**\n * Placeholder commit prefix used by github-draft-pr mode.\n * Created during il start to enable draft PR creation (GitHub requires at least one commit ahead of base).\n * Removed during il finish before the final push to maintain clean commit history.\n */\nexport const PLACEHOLDER_COMMIT_PREFIX = '[iloom-temp]'\n\n/**\n * Check if HEAD commit is a placeholder commit\n * @param cwd - Working directory (defaults to process.cwd())\n * @returns true if HEAD is a placeholder commit\n */\nexport async function isPlaceholderCommit(cwd: string = process.cwd()): Promise<boolean> {\n try {\n const subject = await executeGitCommand(['log', '-1', '--format=%s', 'HEAD'], { cwd })\n return subject.trim().startsWith(PLACEHOLDER_COMMIT_PREFIX)\n } catch {\n // No HEAD (empty repo) or other error - not a placeholder\n return false\n }\n}\n\n/**\n * Find placeholder commit SHA in history using git log --grep\n * @param worktreePath - Working directory\n * @returns SHA of placeholder commit if found, null otherwise\n */\nexport async function findPlaceholderCommitSha(worktreePath: string): Promise<string | null> {\n try {\n // Search commit history for placeholder prefix\n // Use --fixed-strings to treat the pattern literally (brackets are regex special chars)\n const log = await executeGitCommand(\n ['log', '--format=%H', '--fixed-strings', '--grep', PLACEHOLDER_COMMIT_PREFIX, '-n', '1'],\n { cwd: worktreePath }\n )\n const sha = log.trim()\n if (sha.length === 0) {\n return null\n }\n\n // Verify the found commit actually has the placeholder prefix in its subject\n // This guards against git grep matching in commit body instead of subject\n const subject = await executeGitCommand(\n ['log', '-1', '--format=%s', sha],\n { cwd: worktreePath }\n )\n if (!subject.trim().startsWith(PLACEHOLDER_COMMIT_PREFIX)) {\n return null\n }\n\n return sha\n } catch {\n return null\n }\n}\n\n/**\n * Remove placeholder commit when it's HEAD\n * Uses soft reset to preserve any staged changes\n * @param worktreePath - Working directory\n * @returns true if placeholder was removed\n */\nexport async function removePlaceholderCommitFromHead(worktreePath: string): Promise<boolean> {\n if (!await isPlaceholderCommit(worktreePath)) {\n return false\n }\n await executeGitCommand(['reset', '--soft', 'HEAD~1'], { cwd: worktreePath })\n return true\n}\n\n/**\n * Remove placeholder commit from history using rebase\n * Used when user has made commits on top of placeholder\n * @param worktreePath - Working directory\n * @param placeholderSha - SHA of the placeholder commit to remove\n * @throws Error if rebase fails\n */\nexport async function removePlaceholderCommitFromHistory(\n worktreePath: string,\n placeholderSha: string\n): Promise<void> {\n // Get parent of placeholder commit\n const parentSha = await executeGitCommand(\n ['rev-parse', `${placeholderSha}^`],\n { cwd: worktreePath }\n )\n\n // Rebase to drop the placeholder: rebase --onto parent^ placeholder\n await executeGitCommand(\n ['rebase', '--onto', parentSha.trim(), placeholderSha],\n { cwd: worktreePath }\n )\n}\n"],"mappings":";;;;;;;;;;;;AAAA,OAAO,UAAU;AACjB,SAAS,aAA8B;AAUvC,eAAsB,kBACpB,MACA,SACiB;AACjB,MAAI;AACF,UAAM,SAAS,MAAM,MAAM,OAAO,MAAM;AAAA,MACtC,MAAK,mCAAS,QAAO,QAAQ,IAAI;AAAA,MACjC,UAAS,mCAAS,YAAW;AAAA,MAC7B,UAAU;AAAA,MACV,QAAO,mCAAS,UAAS;AAAA,MACzB,SAAS,OAAO,eAAe;AAAA;AAAA,MAE/B,IAAI,mCAAS,QAAO,EAAE,KAAK,QAAQ,IAAI;AAAA,IACzC,CAAC;AAED,WAAO,OAAO;AAAA,EAChB,SAAS,OAAO;AACd,UAAM,aAAa;AACnB,UAAM,SAAS,WAAW,UAAU,WAAW,WAAW;AAC1D,UAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE;AAAA,EACjD;AACF;AAOO,SAAS,kBAAkB,QAAgB,eAAuC;AAvCzF;AAwCE,QAAM,YAA2B,CAAC;AAClC,QAAM,QAAQ,OAAO,KAAK,EAAE,MAAM,IAAI;AAEtC,MAAI,IAAI;AACR,SAAO,IAAI,MAAM,QAAQ;AACvB,UAAM,WAAW,MAAM,CAAC;AACxB,QAAI,EAAC,qCAAU,WAAW,eAAc;AACtC;AACA;AAAA,IACF;AAGA,UAAM,YAAY,SAAS,MAAM,iBAAiB;AAClD,QAAI,CAAC,WAAW;AACd;AACA;AAAA,IACF;AAEA,QAAI,SAAS;AACb,QAAI,SAAS;AACb,QAAI,WAAW;AACf,QAAI,OAAO;AACX,QAAI,SAAS;AACb,QAAI;AAGJ;AACA,WAAO,IAAI,MAAM,UAAU,GAAC,WAAM,CAAC,MAAP,mBAAU,WAAW,eAAc;AAC7D,YAAM,QAAO,WAAM,CAAC,MAAP,mBAAU;AACvB,UAAI,CAAC,MAAM;AACT;AACA;AAAA,MACF;AAEA,UAAI,SAAS,QAAQ;AACnB,eAAO;AACP,iBAAS,iBAAiB;AAAA,MAC5B,WAAW,SAAS,YAAY;AAC9B,mBAAW;AACX,iBAAS;AAAA,MACX,WAAW,KAAK,WAAW,QAAQ,GAAG;AACpC,iBAAS;AACT,cAAM,YAAY,KAAK,MAAM,eAAe;AAC5C,qBAAa,uCAAY;AACzB,iBAAS,UAAU;AAAA,MACrB,WAAW,KAAK,WAAW,OAAO,GAAG;AAEnC,cAAM,cAAc,KAAK,MAAM,mBAAmB;AAClD,YAAI,aAAa;AACf,mBAAS,YAAY,CAAC,KAAK;AAAA,QAC7B;AAAA,MACF,WAAW,KAAK,WAAW,SAAS,GAAG;AAErC,cAAM,cAAc,KAAK,MAAM,4BAA4B;AAC3D,kBAAS,2CAAc,OAAM,KAAK,QAAQ,WAAW,EAAE;AAAA,MACzD;AAEA;AAAA,IACF;AAEA,UAAM,WAAwB;AAAA,MAC5B,MAAM,UAAU,CAAC,KAAK;AAAA,MACtB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,eAAe,QAAW;AAC5B,eAAS,aAAa;AAAA,IACxB;AAEA,cAAU,KAAK,QAAQ;AAAA,EACzB;AAEA,SAAO;AACT;AAKO,SAAS,WAAW,YAA6B;AACtD,QAAM,aAAa;AAAA,IACjB;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,EACF;AAEA,SAAO,WAAW,KAAK,aAAW,QAAQ,KAAK,UAAU,CAAC;AAC5D;AAKO,SAAS,gBAAgB,YAAmC;AACjE,QAAM,WAAW;AAAA,IACf;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,EACF;AAEA,aAAW,WAAW,UAAU;AAC9B,UAAM,QAAQ,WAAW,MAAM,OAAO;AACtC,QAAI,+BAAQ,IAAI;AACd,YAAM,MAAM,SAAS,MAAM,CAAC,GAAG,EAAE;AACjC,UAAI,CAAC,MAAM,GAAG,EAAG,QAAO;AAAA,IAC1B;AAAA,EACF;AAEA,SAAO;AACT;AAOO,SAAS,mBAAmB,YAAmC;AAEpE,QAAM,mBAAmB;AACzB,QAAM,WAAW,WAAW,MAAM,gBAAgB;AAClD,MAAI,qCAAW,GAAI,QAAO,SAAS,CAAC;AAGpC,QAAM,mBAAmB;AACzB,QAAM,WAAW,WAAW,MAAM,gBAAgB;AAClD,MAAI,qCAAW,GAAI,QAAO,SAAS,CAAC;AAGpC,QAAM,yBAAyB;AAC/B,QAAM,oBAAoB,WAAW,MAAM,sBAAsB;AACjE,MAAI,uDAAoB,GAAI,QAAO,kBAAkB,CAAC;AAGtD,QAAM,iBAAiB;AAAA,IACrB;AAAA;AAAA,IACA;AAAA;AAAA,EACF;AACA,aAAW,WAAW,gBAAgB;AACpC,UAAM,QAAQ,WAAW,MAAM,OAAO;AACtC,QAAI,+BAAQ,GAAI,QAAO,MAAM,CAAC;AAAA,EAChC;AAEA,SAAO;AACT;AAKO,SAAS,eAAeA,OAAuB;AACpD,QAAM,mBAAmB;AAAA,IACvB;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,EACF;AAEA,SAAO,iBAAiB,KAAK,aAAW,QAAQ,KAAKA,KAAI,CAAC;AAC5D;AAMO,SAAS,qBACd,YACA,UAAkB,QAAQ,IAAI,GAC9B,SACQ;AAER,MAAI,YAAY,WAAW,QAAQ,OAAO,GAAG;AAG7C,OAAI,mCAAS,UAAQ,mCAAS,WAAU;AACtC,gBAAY,GAAG,SAAS,OAAO,QAAQ,QAAQ;AAAA,EACjD;AAEA,QAAM,YAAY,KAAK,QAAQ,OAAO;AAGtC,MAAI;AAEJ,OAAI,mCAAS,YAAW,QAAW;AAEjC,UAAM,iBAAiB,KAAK,SAAS,OAAO;AAC5C,aAAS,iBAAiB,GAAG,cAAc,YAAY;AAAA,EACzD,WAAW,QAAQ,WAAW,IAAI;AAEhC,aAAS;AAAA,EACX,OAAO;AAEL,aAAS,QAAQ;AAGjB,UAAM,gBAAgB,OAAO,SAAS,GAAG;AAEzC,QAAI,eAAe;AAEjB,YAAM,oBAAoB,SAAS,KAAK,MAAM;AAE9C,UAAI,CAAC,mBAAmB;AAGtB,iBAAS,GAAG,MAAM;AAAA,MACpB;AAAA,IAEF,OAAO;AAEL,YAAM,oBAAoB,QAAQ,KAAK,MAAM;AAC7C,UAAI,CAAC,mBAAmB;AACtB,iBAAS,GAAG,MAAM;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAGA,MAAI,WAAW,IAAI;AACjB,WAAO,KAAK,KAAK,WAAW,SAAS;AAAA,EACvC,WAAW,OAAO,SAAS,GAAG,GAAG;AAE/B,WAAO,KAAK,KAAK,WAAW,QAAQ,SAAS;AAAA,EAC/C,WAAW,OAAO,SAAS,GAAG,GAAG;AAG/B,UAAM,iBAAiB,OAAO,YAAY,GAAG;AAC7C,UAAM,UAAU,OAAO,UAAU,GAAG,cAAc;AAClD,UAAM,sBAAsB,OAAO,UAAU,iBAAiB,CAAC;AAC/D,WAAO,KAAK,KAAK,WAAW,SAAS,GAAG,mBAAmB,GAAG,SAAS,EAAE;AAAA,EAC3E,OAAO;AAEL,WAAO,KAAK,KAAK,WAAW,GAAG,MAAM,GAAG,SAAS,EAAE;AAAA,EACrD;AACF;AAKA,eAAsB,eAAeA,OAAgC;AACnE,MAAI;AACF,UAAM,kBAAkB,CAAC,aAAa,WAAW,GAAG,EAAE,KAAKA,MAAK,CAAC;AACjE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,iBAAiBA,QAAe,QAAQ,IAAI,GAA2B;AAC3F,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB,CAAC,UAAU,gBAAgB,GAAG,EAAE,KAAKA,MAAK,CAAC;AAClF,WAAO,OAAO,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,aACpB,YACAA,QAAe,QAAQ,IAAI,GAC3B,gBAAgB,MACE;AAClB,MAAI;AAEF,UAAM,cAAc,MAAM,kBAAkB,CAAC,UAAU,UAAU,UAAU,GAAG,EAAE,KAAKA,MAAK,CAAC;AAC3F,QAAI,YAAY,KAAK,GAAG;AACtB,aAAO;AAAA,IACT;AAGA,QAAI,eAAe;AACjB,YAAM,eAAe,MAAM,kBAAkB,CAAC,UAAU,MAAM,UAAU,KAAK,UAAU,EAAE,GAAG;AAAA,QAC1F,KAAKA;AAAA,MACP,CAAC;AACD,UAAI,aAAa,KAAK,GAAG;AACvB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,gBAAgBA,QAAe,QAAQ,IAAI,GAA2B;AAC1F,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB,CAAC,aAAa,iBAAiB,GAAG,EAAE,KAAKA,MAAK,CAAC;AACtF,WAAO,OAAO,KAAK;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,YAAYA,QAAe,QAAQ,IAAI,GAA2B;AACtF,MAAI;AAEF,UAAM,eAAe,MAAM;AAAA,MACzB,CAAC,aAAa,0BAA0B,kBAAkB;AAAA,MAC1D,EAAE,KAAKA,MAAK;AAAA,IACd;AACA,UAAM,cAAc,aAAa,KAAK;AAItC,UAAM,WAAW,YACd,QAAQ,8BAA8B,EAAE,EACxC,QAAQ,YAAY,EAAE;AAEzB,WAAO;AAAA,EACT,SAAQ,OAAO;AACb,WAAO,KAAK,sDAAsDA,KAAI,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAChI,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,qBACpBA,QAAe,QAAQ,IAAI,GAC3B,SACiB;AACjB,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB,CAAC,YAAY,QAAQ,aAAa,GAAG,EAAE,KAAKA,MAAK,CAAC;AACzF,UAAM,YAAY,kBAAkB,QAAQ,mCAAS,UAAU;AAG/D,QAAI,UAAU,WAAW,GAAG;AAC1B,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAGA,QAAI,mCAAS,YAAY;AACvB,YAAM,YAAY,UAAU,KAAK,QAAM,GAAG,WAAW,QAAQ,UAAU;AACvE,UAAI,EAAC,uCAAW,OAAM;AACpB,cAAM,IAAI;AAAA,UACR,kCAAkC,QAAQ,UAAU,mDAAmD,UAAU,IAAI,QAAM,GAAG,GAAG,IAAI,KAAK,GAAG,MAAM,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,QACpK;AAAA,MACF;AACA,aAAO,UAAU;AAAA,IACnB;AAGA,UAAM,aAAa,UAAU,KAAK,QAAM,GAAG,WAAW,MAAM;AAC5D,QAAI,yCAAY,MAAM;AACpB,aAAO,WAAW;AAAA,IACpB;AAGA,UAAM,gBAAgB,UAAU,CAAC;AACjC,QAAI,EAAC,+CAAe,OAAM;AACxB,YAAM,IAAI,MAAM,2CAA2C;AAAA,IAC7D;AACA,WAAO,cAAc;AAAA,EACvB,SAAS,OAAO;AACd,QACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,+BAA+B,KACrD,MAAM,QAAQ,SAAS,oBAAoB,KAC3C,MAAM,QAAQ,SAAS,sCAAsC,IAC/D;AAEA,YAAM;AAAA,IACR;AACA,UAAM,IAAI,MAAM,iCAAiC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,EAAE;AAAA,EAC3G;AACF;AAeA,eAAsB,iCACpBA,OACA,iBACiB;AAEjB,sBAAoB,IAAI,gBAAgB;AAExC,QAAM,WAAW,MAAM,gBAAgB,aAAaA,KAAI;AACxD,QAAM,cAAc,SAAS,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI;AAChF,SAAO,qBAAqBA,OAAM,WAAW;AAC/C;AAcA,eAAsB,sBACpB,YACAA,QAAe,QAAQ,IAAI,GACV;AACjB,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB,CAAC,YAAY,QAAQ,aAAa,GAAG,EAAE,KAAKA,MAAK,CAAC;AACzF,UAAM,YAAY,kBAAkB,QAAQ,UAAU;AAGtD,QAAI,UAAU,WAAW,GAAG;AAC1B,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAGA,UAAM,iBAAiB,UAAU,KAAK,QAAM,GAAG,WAAW,UAAU;AACpE,QAAI,EAAC,iDAAgB,OAAM;AACzB,YAAM,IAAI;AAAA,QACR,kCAAkC,UAAU,uCACpB,UAAU,IAAI,QAAM,GAAG,GAAG,IAAI,KAAK,GAAG,MAAM,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,MACrF;AAAA,IACF;AACA,WAAO,eAAe;AAAA,EACxB,SAAS,OAAO;AACd,QACE,iBAAiB,UAChB,MAAM,QAAQ,SAAS,+BAA+B,KACrD,MAAM,QAAQ,SAAS,oBAAoB,IAC7C;AAEA,YAAM;AAAA,IACR;AACA,UAAM,IAAI,MAAM,uCAAuC,UAAU,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,EAAE;AAAA,EACjI;AACF;AAKA,eAAsB,sBAAsBA,QAAe,QAAQ,IAAI,GAAqB;AAC1F,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB,CAAC,UAAU,aAAa,GAAG,EAAE,KAAKA,MAAK,CAAC;AAC/E,WAAO,OAAO,KAAK,EAAE,SAAS;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,iBAAiBA,QAAe,QAAQ,IAAI,GAAoB;AACpF,MAAI;AAEF,UAAM,eAAe,MAAM,kBAAkB,CAAC,gBAAgB,0BAA0B,GAAG;AAAA,MACzF,KAAKA;AAAA,IACP,CAAC;AACD,UAAM,QAAQ,aAAa,MAAM,6BAA6B;AAC9D,QAAI,MAAO,QAAO,MAAM,CAAC,KAAK;AAG9B,UAAM,iBAAiB,CAAC,QAAQ,UAAU,SAAS;AACnD,eAAW,UAAU,gBAAgB;AACnC,UAAI,MAAM,aAAa,QAAQA,KAAI,GAAG;AACpC,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcA,eAAsB,wBACpB,aACAA,QAAe,QAAQ,IAAI,GAC3B,iBACmB;AAEnB,MAAI,CAAC,iBAAiB;AACpB,UAAM,EAAE,iBAAiB,GAAG,IAAI,MAAM,OAAO,+BAA2B;AACxE,sBAAkB,IAAI,GAAG;AAAA,EAC3B;AAGA,QAAM,oBAAoB,MAAM,gBAAgB,qBAAqBA,KAAI;AAGzE,QAAM,SAAS,MAAM,kBAAkB,CAAC,UAAU,IAAI,GAAG,EAAE,KAAKA,MAAK,CAAC;AAEtE,QAAM,WAAqB,CAAC;AAC5B,QAAM,QAAQ,OAAO,MAAM,IAAI,EAAE,OAAO,OAAO;AAE/C,aAAW,QAAQ,OAAO;AAExB,QAAI,KAAK,SAAS,qBAAqB,GAAG;AACxC;AAAA,IACF;AAIA,QAAI,cAAc,KAAK,QAAQ,WAAW,EAAE;AAG5C,kBAAc,YAAY,QAAQ,aAAa,EAAE;AAGjD,kBAAc,YAAY,QAAQ,sBAAsB,EAAE;AAG1D,kBAAc,YAAY,KAAK;AAG/B,QAAI,kBAAkB,SAAS,WAAW,GAAG;AAC3C;AAAA,IACF;AAYA,UAAM,kBAAkB,IAAI,OAAO,WAAW,WAAW,SAAS;AAClE,QAAI,CAAC,gBAAgB,KAAK,WAAW,GAAG;AACtC;AAAA,IACF;AAIA,UAAM,eAAe,YAAY,UAAU,GAAG,YAAY,QAAQ,OAAO,WAAW,CAAC,CAAC;AAEtF,QAAI,cAAc;AAEhB,YAAM,WAAW,aAAa,MAAM,sBAAsB;AAC1D,UAAI,qCAAW,IAAI;AACjB,cAAM,OAAO,SAAS,CAAC,EAAE,YAAY;AAErC,cAAM,gBAAgB;AAAA,UACpB;AAAA,UAAS;AAAA,UACT;AAAA,UAAQ;AAAA,UAAW;AAAA,UACnB;AAAA,UAAO;AAAA,UAAS;AAAA,UAAU;AAAA,UAC1B;AAAA,UAAM;AAAA,UACN;AAAA,UAAQ;AAAA,UACR;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAGA,YAAI,CAAC,cAAc,SAAS,IAAI,GAAG;AACjC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,CAAC,SAAS,SAAS,WAAW,GAAG;AACnC,eAAS,KAAK,WAAW;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO;AACT;AAOA,eAAsB,kBAAkBA,QAAe,QAAQ,IAAI,GAAqB;AACtF,MAAI;AACF,UAAM,kBAAkB,CAAC,aAAa,YAAY,MAAM,GAAG,EAAE,KAAKA,MAAK,CAAC;AACxE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,2BAA2BA,QAAe,QAAQ,IAAI,GAAkB;AAC5F,QAAM,UAAU,MAAM,kBAAkBA,KAAI;AAC5C,MAAI,SAAS;AACX,UAAM,kBAAkB,CAAC,UAAU,eAAe,iBAAiB,MAAM,gBAAgB,GAAG,EAAE,KAAKA,MAAK,CAAC;AAAA,EAC3G;AACF;AAWA,eAAsB,mBACpB,YACA,cACA,SACe;AACf,MAAI,mCAAS,QAAQ;AAEnB;AAAA,EACF;AAEA,MAAI;AAGF,UAAM,kBAAkB,CAAC,QAAQ,UAAU,UAAU,GAAG;AAAA,MACtD,KAAK;AAAA,MACL,SAAS;AAAA;AAAA,IACX,CAAC;AAAA,EACH,SAAS,OAAO;AAEd,UAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAG1E,QAAI,aAAa,SAAS,gBAAgB,KAAK,aAAa,SAAS,UAAU,GAAG;AAChF,YAAM,IAAI;AAAA,QACR,oCAAoC,UAAU;AAAA;AAAA,gBAC7B,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oCAMQ,UAAU;AAAA,MACjD;AAAA,IACF;AAEA,QAAI,aAAa,SAAS,wBAAwB,KAAK,aAAa,SAAS,SAAS,GAAG;AACvF,YAAM,IAAI;AAAA,QACR,oCAAoC,UAAU;AAAA;AAAA,gBAC7B,YAAY;AAAA;AAAA;AAAA,MAE/B;AAAA,IACF;AAEA,QAAI,aAAa,SAAS,gBAAgB,GAAG;AAC3C,YAAM,IAAI;AAAA,QACR;AAAA;AAAA,gBACiB,YAAY;AAAA;AAAA;AAAA,MAE/B;AAAA,IACF;AAGA,UAAM,IAAI,MAAM,6BAA6B,YAAY,EAAE;AAAA,EAC7D;AACF;AASA,eAAsB,mBACpB,UACA,MAAc,QAAQ,IAAI,GACR;AAClB,MAAI;AACF,UAAM,SAAS,MAAM;AAAA,MACnB,CAAC,YAAY,mBAAmB,QAAQ;AAAA,MACxC,EAAE,IAAI;AAAA,IACR;AACA,WAAO,OAAO,KAAK,EAAE,SAAS;AAAA,EAChC,SAAS,OAAO;AAEd,UAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,QAAI,aAAa,SAAS,UAAU,KAAK,aAAa,SAAS,eAAe,GAAG;AAC/E,aAAO;AAAA,IACT;AAEA,UAAM;AAAA,EACR;AACF;AAUA,eAAsB,iBACpB,UACA,MAAc,QAAQ,IAAI,GACR;AAClB,MAAI;AACF,UAAM,kBAAkB,CAAC,gBAAgB,MAAM,QAAQ,GAAG,EAAE,IAAI,CAAC;AACjE,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAkBA,eAAsB,uBACpB,YACA,aAAqB,QACrB,MAAc,QAAQ,IAAI,GACR;AAClB,MAAI;AAEF,UAAM,kBAAkB,CAAC,cAAc,iBAAiB,YAAY,UAAU,GAAG,EAAE,IAAI,CAAC;AACxF,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUA,eAAsB,uBACpB,YACA,KACkB;AAClB,MAAI;AAEF,UAAM,eAAe,MAAM,kBAAkB,CAAC,aAAa,WAAW,UAAU,UAAU,GAAG,EAAE,IAAI,CAAC;AAEpG,QAAI,aAAa,KAAK,EAAE,WAAW,GAAG;AAEpC,aAAO;AAAA,IACT;AAGA,UAAM,eAAe,aAAa,KAAK,EAAE,MAAM,GAAI,EAAE,CAAC;AAGtD,UAAM,cAAc,MAAM,kBAAkB,CAAC,aAAa,UAAU,GAAG,EAAE,IAAI,CAAC;AAG9E,WAAO,YAAY,KAAK,MAAM;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAqCA,eAAsB,wBACpB,YACA,KAC6B;AAC7B,MAAI;AAGF,QAAI;AACF,YAAM,kBAAkB,CAAC,SAAS,UAAU,UAAU,GAAG,EAAE,KAAK,SAAS,IAAM,CAAC;AAAA,IAClF,SAAS,YAAY;AAGnB,YAAM,oBAAoB,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU;AAG9F,UAAI,kBAAkB,SAAS,wBAAwB,KACnD,kBAAkB,SAAS,kBAAkB,KAC7C,kBAAkB,SAAS,SAAS,KACpC,kBAAkB,SAAS,oBAAoB,KAC/C,kBAAkB,SAAS,sBAAsB,GAAG;AACtD,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,YAAY;AAAA,UACZ,cAAc;AAAA,UACd,cAAc;AAAA,QAChB;AAAA,MACF;AAAA,IAEF;AAGA,UAAM,eAAe,MAAM,kBAAkB,CAAC,aAAa,WAAW,UAAU,UAAU,GAAG,EAAE,IAAI,CAAC;AAEpG,QAAI,aAAa,KAAK,EAAE,WAAW,GAAG;AAEpC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF;AAIA,UAAM,eAAe,aAAa,KAAK,EAAE,MAAM,GAAI,EAAE,CAAC;AAGtD,QAAI,CAAC,cAAc;AACjB,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF;AAGA,UAAM,cAAc,MAAM,kBAAkB,CAAC,aAAa,UAAU,GAAG,EAAE,IAAI,CAAC;AAC9E,UAAM,qBAAqB,YAAY,KAAK;AAE5C,QAAI,iBAAiB,oBAAoB;AAEvC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF;AAIA,QAAI;AAEF,YAAM,kBAAkB,CAAC,cAAc,iBAAiB,oBAAoB,YAAY,GAAG,EAAE,IAAI,CAAC;AAGlG,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF,QAAQ;AAIN,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,cAAc;AAAA,MAChB;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AAEd,UAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAE1E,QAAI,aAAa,SAAS,wBAAwB,KAC9C,aAAa,SAAS,kBAAkB,KACxC,aAAa,SAAS,SAAS,KAC/B,aAAa,SAAS,oBAAoB,KAC1C,aAAa,SAAS,sBAAsB,GAAG;AACjD,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,cAAc;AAAA,QACd;AAAA,MACF;AAAA,IACF;AAGA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,YAAY;AAAA,MACZ,cAAc;AAAA,IAChB;AAAA,EACF;AACF;AAaA,eAAsB,qBACpB,eAAuB,QAAQ,IAAI,GACnC,SAIiB;AAxgCnB;AAygCE,QAAM,mBAAkB,mCAAS,oBAAmB,IAAI,gBAAgB;AACxE,QAAM,mBAAkB,mCAAS,oBAAmB,IAAI,gBAAgB;AAGxE,SAAO,MAAM,yCAAyC,YAAY,EAAE;AACpE,QAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;AAChE,OAAI,0CAAU,eAAV,mBAAsB,YAAY;AACpC,WAAO,MAAM,wCAAwC,SAAS,WAAW,UAAU,EAAE;AACrF,WAAO,SAAS,WAAW;AAAA,EAC7B;AACA,SAAO,MAAM,yDAAyD;AAGtE,QAAM,WAAW,MAAM,gBAAgB,aAAa,YAAY;AAChE,QAAM,aAAa,SAAS,cAAc;AAC1C,SAAO,MAAM,iDAAiD,UAAU,EAAE;AAC1E,SAAO;AACT;AAOO,IAAM,4BAA4B;AAOzC,eAAsB,oBAAoB,MAAc,QAAQ,IAAI,GAAqB;AACvF,MAAI;AACF,UAAM,UAAU,MAAM,kBAAkB,CAAC,OAAO,MAAM,eAAe,MAAM,GAAG,EAAE,IAAI,CAAC;AACrF,WAAO,QAAQ,KAAK,EAAE,WAAW,yBAAyB;AAAA,EAC5D,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,yBAAyB,cAA8C;AAC3F,MAAI;AAGF,UAAM,MAAM,MAAM;AAAA,MAChB,CAAC,OAAO,eAAe,mBAAmB,UAAU,2BAA2B,MAAM,GAAG;AAAA,MACxF,EAAE,KAAK,aAAa;AAAA,IACtB;AACA,UAAM,MAAM,IAAI,KAAK;AACrB,QAAI,IAAI,WAAW,GAAG;AACpB,aAAO;AAAA,IACT;AAIA,UAAM,UAAU,MAAM;AAAA,MACpB,CAAC,OAAO,MAAM,eAAe,GAAG;AAAA,MAChC,EAAE,KAAK,aAAa;AAAA,IACtB;AACA,QAAI,CAAC,QAAQ,KAAK,EAAE,WAAW,yBAAyB,GAAG;AACzD,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,gCAAgC,cAAwC;AAC5F,MAAI,CAAC,MAAM,oBAAoB,YAAY,GAAG;AAC5C,WAAO;AAAA,EACT;AACA,QAAM,kBAAkB,CAAC,SAAS,UAAU,QAAQ,GAAG,EAAE,KAAK,aAAa,CAAC;AAC5E,SAAO;AACT;AASA,eAAsB,mCACpB,cACA,gBACe;AAEf,QAAM,YAAY,MAAM;AAAA,IACtB,CAAC,aAAa,GAAG,cAAc,GAAG;AAAA,IAClC,EAAE,KAAK,aAAa;AAAA,EACtB;AAGA,QAAM;AAAA,IACJ,CAAC,UAAU,UAAU,UAAU,KAAK,GAAG,cAAc;AAAA,IACrD,EAAE,KAAK,aAAa;AAAA,EACtB;AACF;","names":["path"]}
@@ -123,9 +123,9 @@ var IloomSettingsSchema = z.object({
123
123
  }).optional()
124
124
  }).optional().describe("Issue management configuration"),
125
125
  mergeBehavior: z.object({
126
- mode: z.enum(["local", "github-pr"]).default("local"),
126
+ mode: z.enum(["local", "github-pr", "github-draft-pr"]).default("local"),
127
127
  remote: z.string().optional()
128
- }).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)"),
128
+ }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
129
129
  ide: z.object({
130
130
  type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf", "antigravity"]).default("vscode").describe(
131
131
  "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor), antigravity (Antigravity IDE)."
@@ -138,7 +138,10 @@ var IloomSettingsSchema = z.object({
138
138
  vscode: z.boolean().default(false).describe(
139
139
  "Apply VSCode/Cursor title bar colors based on branch name. Note: This modifies .vscode/settings.json which may be in source control. Default is false for safety; enable via init or explicitly if .vscode is gitignored."
140
140
  )
141
- }).optional().describe("Color synchronization settings for workspace identification")
141
+ }).optional().describe("Color synchronization settings for workspace identification"),
142
+ attribution: z.enum(["off", "upstreamOnly", "on"]).default("upstreamOnly").describe(
143
+ 'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
144
+ )
142
145
  });
143
146
  var IloomSettingsSchemaNoDefaults = z.object({
144
147
  mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
@@ -191,9 +194,9 @@ var IloomSettingsSchemaNoDefaults = z.object({
191
194
  }).optional()
192
195
  }).optional().describe("Issue management configuration"),
193
196
  mergeBehavior: z.object({
194
- mode: z.enum(["local", "github-pr"]).optional(),
197
+ mode: z.enum(["local", "github-pr", "github-draft-pr"]).optional(),
195
198
  remote: z.string().optional()
196
- }).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)"),
199
+ }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
197
200
  ide: z.object({
198
201
  type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf", "antigravity"]).optional().describe(
199
202
  "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor), antigravity (Antigravity IDE)."
@@ -206,7 +209,10 @@ var IloomSettingsSchemaNoDefaults = z.object({
206
209
  vscode: z.boolean().optional().describe(
207
210
  "Apply VSCode/Cursor title bar colors based on branch name. Note: This modifies .vscode/settings.json which may be in source control."
208
211
  )
209
- }).optional().describe("Color synchronization settings for workspace identification")
212
+ }).optional().describe("Color synchronization settings for workspace identification"),
213
+ attribution: z.enum(["off", "upstreamOnly", "on"]).optional().describe(
214
+ 'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
215
+ )
210
216
  });
211
217
  var SettingsManager = class {
212
218
  /**
@@ -449,4 +455,4 @@ export {
449
455
  IloomSettingsSchemaNoDefaults,
450
456
  SettingsManager
451
457
  };
452
- //# sourceMappingURL=chunk-VWNS6DH5.js.map
458
+ //# sourceMappingURL=chunk-OOU3DKNT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/SettingsManager.ts"],"sourcesContent":["import { readFile } from 'fs/promises'\nimport path from 'path'\nimport os from 'os'\nimport { z } from 'zod'\nimport deepmerge from 'deepmerge'\nimport { logger } from '../utils/logger.js'\n\n/**\n * Zod schema for agent settings\n */\nexport const AgentSettingsSchema = z.object({\n\tmodel: z\n\t\t.enum(['sonnet', 'opus', 'haiku'])\n\t\t.optional()\n\t\t.describe('Claude model shorthand: sonnet, opus, or haiku'),\n\t// Future: could add other per-agent overrides\n})\n\n/**\n * Zod schema for spin agent settings with default model\n * Used for the spin orchestrator configuration\n */\nexport const SpinAgentSettingsSchema = z.object({\n\tmodel: z\n\t\t.enum(['sonnet', 'opus', 'haiku'])\n\t\t.default('opus')\n\t\t.describe('Claude model shorthand for spin orchestrator'),\n})\n\n/**\n * Zod schema for summary settings with default model\n * Used for session summary generation configuration\n */\nexport const SummarySettingsSchema = z.object({\n\tmodel: z\n\t\t.enum(['sonnet', 'opus', 'haiku'])\n\t\t.default('sonnet')\n\t\t.describe('Claude model shorthand for session summary generation'),\n})\n\n/**\n * Zod schema for workflow permission configuration\n */\nexport const WorkflowPermissionSchema = z.object({\n\tpermissionMode: z\n\t\t.enum(['plan', 'acceptEdits', 'bypassPermissions', 'default'])\n\t\t.optional()\n\t\t.describe('Permission mode for Claude CLI in this workflow type'),\n\tnoVerify: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe('Skip pre-commit hooks (--no-verify) when committing during finish workflow'),\n\tstartIde: z\n\t\t.boolean()\n\t\t.default(true)\n\t\t.describe('Launch IDE (code) when starting this workflow type'),\n\tstartDevServer: z\n\t\t.boolean()\n\t\t.default(true)\n\t\t.describe('Launch development server when starting this workflow type'),\n\tstartAiAgent: z\n\t\t.boolean()\n\t\t.default(true)\n\t\t.describe('Launch Claude Code agent when starting this workflow type'),\n\tstartTerminal: z\n\t\t.boolean()\n\t\t.default(false)\n\t\t.describe('Launch terminal window without dev server when starting this workflow type'),\n\tgenerateSummary: z\n\t\t.boolean()\n\t\t.default(true)\n\t\t.describe('Generate and post Claude session summary when finishing this workflow type'),\n})\n\n/**\n * Non-defaulting variant for pre-merge validation\n * This prevents Zod from polluting partial settings with default values before merge\n */\nexport const WorkflowPermissionSchemaNoDefaults = z.object({\n\tpermissionMode: z\n\t\t.enum(['plan', 'acceptEdits', 'bypassPermissions', 'default'])\n\t\t.optional()\n\t\t.describe('Permission mode for Claude CLI in this workflow type'),\n\tnoVerify: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe('Skip pre-commit hooks (--no-verify) when committing during finish workflow'),\n\tstartIde: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe('Launch IDE (code) when starting this workflow type'),\n\tstartDevServer: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe('Launch development server when starting this workflow type'),\n\tstartAiAgent: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe('Launch Claude Code agent when starting this workflow type'),\n\tstartTerminal: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe('Launch terminal window without dev server when starting this workflow type'),\n\tgenerateSummary: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe('Generate and post Claude session summary when finishing this workflow type'),\n})\n\n/**\n * Zod schema for workflows settings\n */\nexport const WorkflowsSettingsSchema = z\n\t.object({\n\t\tissue: WorkflowPermissionSchema.optional(),\n\t\tpr: WorkflowPermissionSchema.optional(),\n\t\tregular: WorkflowPermissionSchema.optional(),\n\t})\n\t.optional()\n\n/**\n * Non-defaulting variant for pre-merge validation\n */\nexport const WorkflowsSettingsSchemaNoDefaults = z\n\t.object({\n\t\tissue: WorkflowPermissionSchemaNoDefaults.optional(),\n\t\tpr: WorkflowPermissionSchemaNoDefaults.optional(),\n\t\tregular: WorkflowPermissionSchemaNoDefaults.optional(),\n\t})\n\t.optional()\n\n/**\n * Zod schema for capabilities settings\n */\nexport const CapabilitiesSettingsSchema = z\n\t.object({\n\t\tweb: z\n\t\t\t.object({\n\t\t\t\tbasePort: z\n\t\t\t\t\t.number()\n\t\t\t\t\t.min(1, 'Base port must be >= 1')\n\t\t\t\t\t.max(65535, 'Base port must be <= 65535')\n\t\t\t\t\t.optional()\n\t\t\t\t\t.describe('Base port for web workspace port calculations (default: 3000)'),\n\t\t\t})\n\t\t\t.optional(),\n\t\tdatabase: z\n\t\t\t.object({\n\t\t\t\tdatabaseUrlEnvVarName: z\n\t\t\t\t\t.string()\n\t\t\t\t\t.min(1, 'Database URL variable name cannot be empty')\n\t\t\t\t\t.regex(/^[A-Z_][A-Z0-9_]*$/, 'Must be valid env var name (uppercase, underscores)')\n\t\t\t\t\t.optional()\n\t\t\t\t\t.default('DATABASE_URL')\n\t\t\t\t\t.describe('Name of environment variable for database connection URL'),\n\t\t\t})\n\t\t\t.optional(),\n\t})\n\t.optional()\n\n/**\n * Non-defaulting variant for pre-merge validation\n */\nexport const CapabilitiesSettingsSchemaNoDefaults = z\n\t.object({\n\t\tweb: z\n\t\t\t.object({\n\t\t\t\tbasePort: z\n\t\t\t\t\t.number()\n\t\t\t\t\t.min(1, 'Base port must be >= 1')\n\t\t\t\t\t.max(65535, 'Base port must be <= 65535')\n\t\t\t\t\t.optional()\n\t\t\t\t\t.describe('Base port for web workspace port calculations (default: 3000)'),\n\t\t\t})\n\t\t\t.optional(),\n\t\tdatabase: z\n\t\t\t.object({\n\t\t\t\tdatabaseUrlEnvVarName: z\n\t\t\t\t\t.string()\n\t\t\t\t\t.min(1, 'Database URL variable name cannot be empty')\n\t\t\t\t\t.regex(/^[A-Z_][A-Z0-9_]*$/, 'Must be valid env var name (uppercase, underscores)')\n\t\t\t\t\t.optional()\n\t\t\t\t\t.describe('Name of environment variable for database connection URL'),\n\t\t\t})\n\t\t\t.optional(),\n\t})\n\t.optional()\n\n/**\n * Zod schema for Neon database provider settings\n */\nexport const NeonSettingsSchema = z.object({\n\tprojectId: z\n\t\t.string()\n\t\t.min(1)\n\t\t.regex(/^[a-zA-Z0-9-]+$/, 'Neon project ID must contain only letters, numbers, and hyphens')\n\t\t.describe('Neon project ID found in your project URL (e.g., \"fantastic-fox-3566354\")'),\n\tparentBranch: z\n\t\t.string()\n\t\t.min(1)\n\t\t.describe('Branch from which new database branches are created'),\n})\n\n/**\n * Zod schema for database provider settings\n */\nexport const DatabaseProvidersSettingsSchema = z\n\t.object({\n\t\tneon: NeonSettingsSchema.optional().describe(\n\t\t\t'Neon database configuration. Requires Neon CLI installed and authenticated for database branching.',\n\t\t),\n\t})\n\t.optional()\n\n/**\n * Zod schema for iloom settings\n */\nexport const IloomSettingsSchema = z.object({\n\tmainBranch: z\n\t\t.string()\n\t\t.min(1, \"Settings 'mainBranch' cannot be empty\")\n\t\t.optional()\n\t\t.describe('Name of the main/primary branch for the repository'),\n\tsourceEnvOnStart: z\n\t\t.boolean()\n\t\t.default(false)\n\t\t.describe(\n\t\t\t'Source all dotenv-flow files (.env, .env.local, .env.development, .env.development.local) when launching terminal processes (Claude, dev server, terminal). ' +\n\t\t\t\t'Files are sourced in precedence order so later files override earlier ones. ' +\n\t\t\t\t'NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env file compatibility. ' +\n\t\t\t\t'WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. ' +\n\t\t\t\t'Before enabling, verify ALL your .env.* files do not contain unquoted special characters ' +\n\t\t\t\t'(e.g., database URLs with ?, &, or other shell metacharacters). ' +\n\t\t\t\t'Shell compatibility issues may cause processes to fail or behave unexpectedly.',\n\t\t),\n\tworktreePrefix: z\n\t\t.string()\n\t\t.optional()\n\t\t.refine(\n\t\t\t(val) => {\n\t\t\t\tif (val === undefined) return true // undefined = use default calculation\n\t\t\t\tif (val === '') return true // empty string = no prefix mode\n\n\t\t\t\t// Allowlist: only alphanumeric, hyphens, underscores, and forward slashes\n\t\t\t\tconst allowedChars = /^[a-zA-Z0-9\\-_/]+$/\n\t\t\t\tif (!allowedChars.test(val)) return false\n\n\t\t\t\t// Reject if only special characters (no alphanumeric content)\n\t\t\t\tif (/^[-_/]+$/.test(val)) return false\n\n\t\t\t\t// Check each segment (split by /) contains at least one alphanumeric character\n\t\t\t\tconst segments = val.split('/')\n\t\t\t\tfor (const segment of segments) {\n\t\t\t\t\tif (segment && /^[-_]+$/.test(segment)) {\n\t\t\t\t\t\t// Segment exists but contains only hyphens/underscores\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn true\n\t\t\t},\n\t\t\t{\n\t\t\t\tmessage:\n\t\t\t\t\t\"worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories.\",\n\t\t\t},\n\t\t)\n\t\t.describe(\n\t\t\t'Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set.',\n\t\t),\n\tprotectedBranches: z\n\t\t.array(z.string().min(1, 'Protected branch name cannot be empty'))\n\t\t.optional()\n\t\t.describe('List of branches that cannot be deleted (defaults to [mainBranch, \"main\", \"master\", \"develop\"])'),\n\tworkflows: WorkflowsSettingsSchema.describe('Per-workflow-type permission configurations'),\n\tagents: z\n\t\t.record(z.string(), AgentSettingsSchema)\n\t\t.optional()\n\t\t.nullable()\n\t\t.describe(\n\t\t\t'Per-agent configuration overrides. Available agents: ' +\n\t\t\t\t'iloom-issue-analyzer (analyzes issues), ' +\n\t\t\t\t'iloom-issue-planner (creates implementation plans), ' +\n\t\t\t\t'iloom-issue-analyze-and-plan (combined analysis and planning), ' +\n\t\t\t\t'iloom-issue-complexity-evaluator (evaluates complexity), ' +\n\t\t\t\t'iloom-issue-enhancer (enhances issue descriptions), ' +\n\t\t\t\t'iloom-issue-implementer (implements code changes), ' +\n\t\t\t\t'iloom-issue-reviewer (reviews code changes against requirements)',\n\t\t),\n\tspin: SpinAgentSettingsSchema.optional().describe(\n\t\t'Spin orchestrator configuration. Model defaults to opus when not configured.',\n\t),\n\tsummary: SummarySettingsSchema.optional().describe(\n\t\t'Session summary generation configuration. Model defaults to sonnet when not configured.',\n\t),\n\tcapabilities: CapabilitiesSettingsSchema.describe('Project capability configurations'),\n\tdatabaseProviders: DatabaseProvidersSettingsSchema.describe('Database provider configurations'),\n\tissueManagement: z\n\t\t.object({\n\t\t\tprovider: z.enum(['github', 'linear']).optional().default('github').describe('Issue tracker provider (github, linear)'),\n\t\t\tgithub: z\n\t\t\t\t.object({\n\t\t\t\t\tremote: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.min(1, 'Remote name cannot be empty')\n\t\t\t\t\t\t.describe('Git remote name to use for GitHub operations'),\n\t\t\t\t})\n\t\t\t\t.optional(),\n\t\t\tlinear: z\n\t\t\t\t.object({\n\t\t\t\t\tteamId: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.min(1, 'Team ID cannot be empty')\n\t\t\t\t\t\t.describe('Linear team identifier (e.g., \"ENG\", \"PLAT\")'),\n\t\t\t\t\tbranchFormat: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.optional()\n\t\t\t\t\t\t.describe('Branch naming template for Linear issues'),\n\t\t\t\t\tapiToken: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.optional()\n\t\t\t\t\t\t.describe('Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.'),\n\t\t\t\t})\n\t\t\t\t.optional(),\n\t\t})\n\t\t.optional()\n\t\t.describe('Issue management configuration'),\n\tmergeBehavior: z\n\t\t.object({\n\t\t\tmode: z.enum(['local', 'github-pr', 'github-draft-pr']).default('local'),\n\t\t\tremote: z.string().optional(),\n\t\t})\n\t\t.optional()\n\t\t.describe('Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)'),\n\tide: z\n\t\t.object({\n\t\t\ttype: z\n\t\t\t\t.enum(['vscode', 'cursor', 'webstorm', 'sublime', 'intellij', 'windsurf', 'antigravity'])\n\t\t\t\t.default('vscode')\n\t\t\t\t.describe(\n\t\t\t\t\t'IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), ' +\n\t\t\t\t\t\t'webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), ' +\n\t\t\t\t\t\t'windsurf (Windsurf editor), antigravity (Antigravity IDE).'\n\t\t\t\t),\n\t\t})\n\t\t.optional()\n\t\t.describe(\n\t\t\t'IDE configuration for workspace launches. Controls which editor opens when you start a loom. ' +\n\t\t\t\t'Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, Windsurf, and Antigravity. ' +\n\t\t\t\t'Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf, antigravity).'\n\t\t),\n\tcolors: z\n\t\t.object({\n\t\t\tterminal: z\n\t\t\t\t.boolean()\n\t\t\t\t.default(true)\n\t\t\t\t.describe('Apply terminal background colors based on branch name (macOS only)'),\n\t\t\tvscode: z\n\t\t\t\t.boolean()\n\t\t\t\t.default(false)\n\t\t\t\t.describe(\n\t\t\t\t\t'Apply VSCode/Cursor title bar colors based on branch name. ' +\n\t\t\t\t\t\t'Note: This modifies .vscode/settings.json which may be in source control. ' +\n\t\t\t\t\t\t'Default is false for safety; enable via init or explicitly if .vscode is gitignored.'\n\t\t\t\t),\n\t\t})\n\t\t.optional()\n\t\t.describe('Color synchronization settings for workspace identification'),\n\tattribution: z\n\t\t.enum(['off', 'upstreamOnly', 'on'])\n\t\t.default('upstreamOnly')\n\t\t.describe(\n\t\t\t'Controls when iloom attribution appears in session summaries. ' +\n\t\t\t\t'\"off\" - never show attribution. ' +\n\t\t\t\t'\"upstreamOnly\" - only show for contributions to external repositories (e.g., open source). ' +\n\t\t\t\t'\"on\" - always show attribution.'\n\t\t),\n})\n\n/**\n * Non-defaulting variant for pre-merge validation\n * This prevents Zod from polluting partial settings with default values before merge\n */\nexport const IloomSettingsSchemaNoDefaults = z.object({\n\tmainBranch: z\n\t\t.string()\n\t\t.min(1, \"Settings 'mainBranch' cannot be empty\")\n\t\t.optional()\n\t\t.describe('Name of the main/primary branch for the repository'),\n\tsourceEnvOnStart: z\n\t\t.boolean()\n\t\t.optional()\n\t\t.describe(\n\t\t\t'Source all dotenv-flow files (.env, .env.local, .env.development, .env.development.local) when launching terminal processes (Claude, dev server, terminal). ' +\n\t\t\t\t'Files are sourced in precedence order so later files override earlier ones. ' +\n\t\t\t\t'NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env compatibility. ' +\n\t\t\t\t'WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. ' +\n\t\t\t\t'Before enabling, verify ALL your .env.* files do not contain unquoted special characters ' +\n\t\t\t\t'(e.g., database URLs with ?, &, or other shell metacharacters). ' +\n\t\t\t\t'Shell compatibility issues may cause processes to fail or behave unexpectedly.',\n\t\t),\n\tworktreePrefix: z\n\t\t.string()\n\t\t.optional()\n\t\t.refine(\n\t\t\t(val) => {\n\t\t\t\tif (val === undefined) return true // undefined = use default calculation\n\t\t\t\tif (val === '') return true // empty string = no prefix mode\n\n\t\t\t\t// Allowlist: only alphanumeric, hyphens, underscores, and forward slashes\n\t\t\t\tconst allowedChars = /^[a-zA-Z0-9\\-_/]+$/\n\t\t\t\tif (!allowedChars.test(val)) return false\n\n\t\t\t\t// Reject if only special characters (no alphanumeric content)\n\t\t\t\tif (/^[-_/]+$/.test(val)) return false\n\n\t\t\t\t// Check each segment (split by /) contains at least one alphanumeric character\n\t\t\t\tconst segments = val.split('/')\n\t\t\t\tfor (const segment of segments) {\n\t\t\t\t\tif (segment && /^[-_]+$/.test(segment)) {\n\t\t\t\t\t\t// Segment exists but contains only hyphens/underscores\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn true\n\t\t\t},\n\t\t\t{\n\t\t\t\tmessage:\n\t\t\t\t\t\"worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories.\",\n\t\t\t},\n\t\t)\n\t\t.describe(\n\t\t\t'Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set.',\n\t\t),\n\tprotectedBranches: z\n\t\t.array(z.string().min(1, 'Protected branch name cannot be empty'))\n\t\t.optional()\n\t\t.describe('List of branches that cannot be deleted (defaults to [mainBranch, \"main\", \"master\", \"develop\"])'),\n\tworkflows: WorkflowsSettingsSchemaNoDefaults.describe('Per-workflow-type permission configurations'),\n\tagents: z\n\t\t.record(z.string(), AgentSettingsSchema)\n\t\t.optional()\n\t\t.nullable()\n\t\t.describe(\n\t\t\t'Per-agent configuration overrides. Available agents: ' +\n\t\t\t\t'iloom-issue-analyzer (analyzes issues), ' +\n\t\t\t\t'iloom-issue-planner (creates implementation plans), ' +\n\t\t\t\t'iloom-issue-analyze-and-plan (combined analysis and planning), ' +\n\t\t\t\t'iloom-issue-complexity-evaluator (evaluates complexity), ' +\n\t\t\t\t'iloom-issue-enhancer (enhances issue descriptions), ' +\n\t\t\t\t'iloom-issue-implementer (implements code changes), ' +\n\t\t\t\t'iloom-issue-reviewer (reviews code changes against requirements)',\n\t\t),\n\tspin: z\n\t\t.object({\n\t\t\tmodel: z.enum(['sonnet', 'opus', 'haiku']).optional(),\n\t\t})\n\t\t.optional()\n\t\t.describe('Spin orchestrator configuration'),\n\tsummary: z\n\t\t.object({\n\t\t\tmodel: z.enum(['sonnet', 'opus', 'haiku']).optional(),\n\t\t})\n\t\t.optional()\n\t\t.describe('Session summary generation configuration'),\n\tcapabilities: CapabilitiesSettingsSchemaNoDefaults.describe('Project capability configurations'),\n\tdatabaseProviders: DatabaseProvidersSettingsSchema.describe('Database provider configurations'),\n\tissueManagement: z\n\t\t.object({\n\t\t\tprovider: z.enum(['github', 'linear']).optional().describe('Issue tracker provider (github, linear)'),\n\t\t\tgithub: z\n\t\t\t\t.object({\n\t\t\t\t\tremote: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.min(1, 'Remote name cannot be empty')\n\t\t\t\t\t\t.describe('Git remote name to use for GitHub operations'),\n\t\t\t\t})\n\t\t\t\t.optional(),\n\t\t\tlinear: z\n\t\t\t\t.object({\n\t\t\t\t\tteamId: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.min(1, 'Team ID cannot be empty')\n\t\t\t\t\t\t.describe('Linear team identifier (e.g., \"ENG\", \"PLAT\")'),\n\t\t\t\t\tbranchFormat: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.optional()\n\t\t\t\t\t\t.describe('Branch naming template for Linear issues'),\n\t\t\t\t\tapiToken: z\n\t\t\t\t\t\t.string()\n\t\t\t\t\t\t.optional()\n\t\t\t\t\t\t.describe('Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.'),\n\t\t\t\t})\n\t\t\t\t.optional(),\n\t\t})\n\t\t.optional()\n\t\t.describe('Issue management configuration'),\n\tmergeBehavior: z\n\t\t.object({\n\t\t\tmode: z.enum(['local', 'github-pr', 'github-draft-pr']).optional(),\n\t\t\tremote: z.string().optional(),\n\t\t})\n\t\t.optional()\n\t\t.describe('Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)'),\n\tide: z\n\t\t.object({\n\t\t\ttype: z\n\t\t\t\t.enum(['vscode', 'cursor', 'webstorm', 'sublime', 'intellij', 'windsurf', 'antigravity'])\n\t\t\t\t.optional()\n\t\t\t\t.describe(\n\t\t\t\t\t'IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), ' +\n\t\t\t\t\t\t'webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), ' +\n\t\t\t\t\t\t'windsurf (Windsurf editor), antigravity (Antigravity IDE).'\n\t\t\t\t),\n\t\t})\n\t\t.optional()\n\t\t.describe(\n\t\t\t'IDE configuration for workspace launches. Controls which editor opens when you start a loom. ' +\n\t\t\t\t'Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, Windsurf, and Antigravity. ' +\n\t\t\t\t'Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf, antigravity).'\n\t\t),\n\tcolors: z\n\t\t.object({\n\t\t\tterminal: z\n\t\t\t\t.boolean()\n\t\t\t\t.optional()\n\t\t\t\t.describe('Apply terminal background colors based on branch name (macOS only)'),\n\t\t\tvscode: z\n\t\t\t\t.boolean()\n\t\t\t\t.optional()\n\t\t\t\t.describe(\n\t\t\t\t\t'Apply VSCode/Cursor title bar colors based on branch name. ' +\n\t\t\t\t\t\t'Note: This modifies .vscode/settings.json which may be in source control.'\n\t\t\t\t),\n\t\t})\n\t\t.optional()\n\t\t.describe('Color synchronization settings for workspace identification'),\n\tattribution: z\n\t\t.enum(['off', 'upstreamOnly', 'on'])\n\t\t.optional()\n\t\t.describe(\n\t\t\t'Controls when iloom attribution appears in session summaries. ' +\n\t\t\t\t'\"off\" - never show attribution. ' +\n\t\t\t\t'\"upstreamOnly\" - only show for contributions to external repositories (e.g., open source). ' +\n\t\t\t\t'\"on\" - always show attribution.'\n\t\t),\n})\n\n/**\n * TypeScript type for Neon settings derived from Zod schema\n */\nexport type NeonSettings = z.infer<typeof NeonSettingsSchema>\n\n/**\n * TypeScript type for database providers settings derived from Zod schema\n */\nexport type DatabaseProvidersSettings = z.infer<typeof DatabaseProvidersSettingsSchema>\n\n/**\n * TypeScript type for agent settings derived from Zod schema\n */\nexport type AgentSettings = z.infer<typeof AgentSettingsSchema>\n\n/**\n * TypeScript type for spin agent settings derived from Zod schema\n */\nexport type SpinAgentSettings = z.infer<typeof SpinAgentSettingsSchema>\n\n/**\n * TypeScript type for summary settings derived from Zod schema\n */\nexport type SummarySettings = z.infer<typeof SummarySettingsSchema>\n\n/**\n * TypeScript type for workflow permission configuration derived from Zod schema\n */\nexport type WorkflowPermission = z.infer<typeof WorkflowPermissionSchema>\n\n/**\n * TypeScript type for workflows settings derived from Zod schema\n */\nexport type WorkflowsSettings = z.infer<typeof WorkflowsSettingsSchema>\n\n/**\n * TypeScript type for capabilities settings derived from Zod schema\n */\nexport type CapabilitiesSettings = z.infer<typeof CapabilitiesSettingsSchema>\n\n/**\n * TypeScript type for IDE settings derived from Zod schema\n */\nexport type IdeSettings = z.infer<typeof IloomSettingsSchema>['ide']\n\n/**\n * TypeScript type for iloom settings derived from Zod schema\n */\nexport type IloomSettings = z.infer<typeof IloomSettingsSchema>\n\n/**\n * Manages project-level settings from .iloom/settings.json\n */\nexport class SettingsManager {\n\t/**\n\t * Load settings from global, project, and local sources with proper precedence\n\t * Merge hierarchy (lowest to highest priority):\n\t * 1. Global settings (~/.config/iloom-ai/settings.json)\n\t * 2. Project settings (<PROJECT_ROOT>/.iloom/settings.json)\n\t * 3. Local settings (<PROJECT_ROOT>/.iloom/settings.local.json)\n\t * 4. CLI overrides (--set flags)\n\t * Returns empty object if all files don't exist (not an error)\n\t */\n\tasync loadSettings(\n\t\tprojectRoot?: string,\n\t\tcliOverrides?: Partial<IloomSettings>,\n\t): Promise<IloomSettings> {\n\t\tconst root = this.getProjectRoot(projectRoot)\n\n\t\t// Load global settings (lowest priority)\n\t\tconst globalSettings = await this.loadGlobalSettingsFile()\n\t\tconst globalSettingsPath = this.getGlobalSettingsPath()\n\t\tlogger.debug(`🌍 Global settings from ${globalSettingsPath}:`, JSON.stringify(globalSettings, null, 2))\n\n\t\t// Load base settings from settings.json\n\t\tconst baseSettings = await this.loadSettingsFile(root, 'settings.json')\n\t\tconst baseSettingsPath = path.join(root, '.iloom', 'settings.json')\n\t\tlogger.debug(`📄 Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2))\n\n\t\t// Load local overrides from settings.local.json\n\t\tconst localSettings = await this.loadSettingsFile(root, 'settings.local.json')\n\t\tconst localSettingsPath = path.join(root, '.iloom', 'settings.local.json')\n\t\tlogger.debug(`📄 Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2))\n\n\t\t// Deep merge with priority: cliOverrides > localSettings > baseSettings > globalSettings\n\t\tlet merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings)\n\t\tlogger.debug('🔄 After merging global + base + local settings:', JSON.stringify(merged, null, 2))\n\n\t\tif (cliOverrides && Object.keys(cliOverrides).length > 0) {\n\t\t\tlogger.debug('⚙️ CLI overrides to apply:', JSON.stringify(cliOverrides, null, 2))\n\t\t\tmerged = this.mergeSettings(merged, cliOverrides)\n\t\t\tlogger.debug('🔄 After applying CLI overrides:', JSON.stringify(merged, null, 2))\n\t\t}\n\n\t\t// Validate merged result\n\t\ttry {\n\t\t\tconst finalSettings = IloomSettingsSchema.parse(merged)\n\n\t\t\t// Debug: Log final merged configuration\n\t\t\tthis.logFinalConfiguration(finalSettings)\n\n\t\t\treturn finalSettings\n\t\t} catch (error) {\n\t\t\t// Show all Zod validation errors\n\t\t\tif (error instanceof z.ZodError) {\n\t\t\t\tconst errorMsg = this.formatAllZodErrors(error, '<merged settings>')\n\t\t\t\t// Enhance error message if CLI overrides were applied\n\t\t\t\tif (cliOverrides && Object.keys(cliOverrides).length > 0) {\n\t\t\t\t\tthrow new Error(`${errorMsg.message}\\n\\nNote: CLI overrides were applied. Check your --set arguments.`)\n\t\t\t\t}\n\t\t\t\tthrow errorMsg\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t/**\n\t * Log the final merged configuration for debugging\n\t */\n\tprivate logFinalConfiguration(settings: IloomSettings): void {\n\t\tlogger.debug('📋 Final merged configuration:', JSON.stringify(settings, null, 2))\n\t}\n\n\t/**\n\t * Load and parse a single settings file\n\t * Returns empty object if file doesn't exist (not an error)\n\t * Uses non-defaulting schema to prevent polluting partial settings with defaults before merge\n\t */\n\tprivate async loadSettingsFile(\n\t\tprojectRoot: string,\n\t\tfilename: string,\n\t): Promise<z.infer<typeof IloomSettingsSchemaNoDefaults>> {\n\t\tconst settingsPath = path.join(projectRoot, '.iloom', filename)\n\n\t\ttry {\n\t\t\tconst content = await readFile(settingsPath, 'utf-8')\n\t\t\tlet parsed: unknown\n\n\t\t\ttry {\n\t\t\t\tparsed = JSON.parse(content)\n\t\t\t} catch (error) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\t// Basic type checking - ensure it's an object, but don't validate schema completeness\n\t\t\t// Individual files may be incomplete (e.g., Linear config split between files)\n\t\t\t// Final validation will happen on the merged result in loadSettings()\n\t\t\tif (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Settings validation failed at ${filename}:\\n - root: Expected object, received ${typeof parsed}`\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn parsed as z.infer<typeof IloomSettingsSchemaNoDefaults>\n\t\t} catch (error) {\n\t\t\t// File not found is not an error - return empty settings\n\t\t\tif ((error as { code?: string }).code === 'ENOENT') {\n\t\t\t\tlogger.debug(`No settings file found at ${settingsPath}, using defaults`)\n\t\t\t\treturn {}\n\t\t\t}\n\n\t\t\t// Re-throw parsing errors\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t/**\n\t * Deep merge two settings objects with priority to override\n\t * Uses deepmerge library with array replacement strategy\n\t */\n\tprivate mergeSettings(\n\t\tbase: Partial<IloomSettings> | z.infer<typeof IloomSettingsSchemaNoDefaults>,\n\t\toverride: Partial<IloomSettings> | z.infer<typeof IloomSettingsSchemaNoDefaults>,\n\t): IloomSettings {\n\t\t// Use deepmerge with array replacement (not concatenation)\n\t\t// Type assertion is safe because the merged result will be validated with IloomSettingsSchema\n\t\t// which applies all the defaults after merging\n\t\treturn deepmerge(base as Record<string, unknown>, override as Record<string, unknown>, {\n\t\t\t// Replace arrays instead of concatenating them\n\t\t\tarrayMerge: (_destinationArray, sourceArray) => sourceArray,\n\t\t}) as IloomSettings\n\t}\n\n\t/**\n\t * Format all Zod validation errors into a single error message\n\t */\n\tprivate formatAllZodErrors(error: z.ZodError, settingsPath: string): Error {\n\t\tconst errorMessages = error.issues.map(issue => {\n\t\t\tconst path = issue.path.length > 0 ? issue.path.join('.') : 'root'\n\t\t\treturn ` - ${path}: ${issue.message}`\n\t\t})\n\n\t\treturn new Error(\n\t\t\t`Settings validation failed at ${settingsPath}:\\n${errorMessages.join('\\n')}`,\n\t\t)\n\t}\n\n\t/**\n\t * Validate settings structure and model names using Zod schema\n\t * This method is kept for testing purposes but uses Zod internally\n\t * @internal - Only used in tests via bracket notation\n\t */\n\t// @ts-expect-error - Used in tests via bracket notation, TypeScript can't detect this usage\n\tprivate validateSettings(settings: IloomSettings): void {\n\t\ttry {\n\t\t\tIloomSettingsSchema.parse(settings)\n\t\t} catch (error) {\n\t\t\tif (error instanceof z.ZodError) {\n\t\t\t\tthrow this.formatAllZodErrors(error, '<validation>')\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t/**\n\t * Get project root (defaults to process.cwd())\n\t */\n\tprivate getProjectRoot(projectRoot?: string): string {\n\t\treturn projectRoot ?? process.cwd()\n\t}\n\n\t/**\n\t * Get global config directory path (~/.config/iloom-ai)\n\t */\n\tprivate getGlobalConfigDir(): string {\n\t\treturn path.join(os.homedir(), '.config', 'iloom-ai')\n\t}\n\n\t/**\n\t * Get global settings file path (~/.config/iloom-ai/settings.json)\n\t */\n\tprivate getGlobalSettingsPath(): string {\n\t\treturn path.join(this.getGlobalConfigDir(), 'settings.json')\n\t}\n\n\t/**\n\t * Load and parse global settings file\n\t * Returns empty object if file doesn't exist (not an error)\n\t * Warns but returns empty object on validation/parse errors (graceful degradation)\n\t */\n\tprivate async loadGlobalSettingsFile(): Promise<z.infer<typeof IloomSettingsSchemaNoDefaults>> {\n\t\tconst settingsPath = this.getGlobalSettingsPath()\n\n\t\ttry {\n\t\t\tconst content = await readFile(settingsPath, 'utf-8')\n\t\t\tlet parsed: unknown\n\n\t\t\ttry {\n\t\t\t\tparsed = JSON.parse(content)\n\t\t\t} catch (error) {\n\t\t\t\tlogger.warn(\n\t\t\t\t\t`Failed to parse global settings file at ${settingsPath}: ${error instanceof Error ? error.message : 'Invalid JSON'}. Ignoring global settings.`,\n\t\t\t\t)\n\t\t\t\treturn {}\n\t\t\t}\n\n\t\t\t// Validate with non-defaulting schema\n\t\t\ttry {\n\t\t\t\tconst validated = IloomSettingsSchemaNoDefaults.strict().parse(parsed)\n\t\t\t\treturn validated\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof z.ZodError) {\n\t\t\t\t\tconst errorMsg = this.formatAllZodErrors(error, 'global settings')\n\t\t\t\t\tlogger.warn(`${errorMsg.message}. Ignoring global settings.`)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.warn(`Validation error in global settings: ${error instanceof Error ? error.message : 'Unknown error'}. Ignoring global settings.`)\n\t\t\t\t}\n\t\t\t\treturn {}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\t// File not found is not an error - return empty settings\n\t\t\tif ((error as { code?: string }).code === 'ENOENT') {\n\t\t\t\tlogger.debug(`No global settings file found at ${settingsPath}`)\n\t\t\t\treturn {}\n\t\t\t}\n\n\t\t\t// Other file system errors - warn and continue\n\t\t\tlogger.warn(`Error reading global settings file at ${settingsPath}: ${error instanceof Error ? error.message : 'Unknown error'}. Ignoring global settings.`)\n\t\t\treturn {}\n\t\t}\n\t}\n\n\t/**\n\t * Get effective protected branches list with mainBranch always included\n\t *\n\t * This method provides a single source of truth for protected branches logic:\n\t * 1. Use configured protectedBranches if provided\n\t * 2. Otherwise use defaults: [mainBranch, 'main', 'master', 'develop']\n\t * 3. ALWAYS ensure mainBranch is included even if user configured custom list\n\t *\n\t * @param projectRoot - Optional project root directory (defaults to process.cwd())\n\t * @returns Array of protected branch names with mainBranch guaranteed to be included\n\t */\n\tasync getProtectedBranches(projectRoot?: string): Promise<string[]> {\n\t\tconst settings = await this.loadSettings(projectRoot)\n\t\tconst mainBranch = settings.mainBranch ?? 'main'\n\n\t\t// Build protected branches list:\n\t\t// 1. Use configured protectedBranches if provided\n\t\t// 2. Otherwise use defaults: [mainBranch, 'main', 'master', 'develop']\n\t\t// 3. ALWAYS ensure mainBranch is included even if user configured custom list\n\t\tlet protectedBranches: string[]\n\t\tif (settings.protectedBranches) {\n\t\t\t// Use configured list but ensure mainBranch is always included\n\t\t\tprotectedBranches = settings.protectedBranches.includes(mainBranch)\n\t\t\t\t? settings.protectedBranches\n\t\t\t\t: [mainBranch, ...settings.protectedBranches]\n\t\t} else {\n\t\t\t// Use defaults with current mainBranch\n\t\t\tprotectedBranches = [mainBranch, 'main', 'master', 'develop']\n\t\t}\n\n\t\treturn protectedBranches\n\t}\n\n\t/**\n\t * Get the spin orchestrator model with default applied\n\t * Default is defined in SpinAgentSettingsSchema\n\t *\n\t * @param settings - Pre-loaded settings object\n\t * @returns Model shorthand ('opus', 'sonnet', or 'haiku')\n\t */\n\tgetSpinModel(settings?: IloomSettings): 'sonnet' | 'opus' | 'haiku' {\n\t\treturn settings?.spin?.model ?? SpinAgentSettingsSchema.parse({}).model\n\t}\n\n\t/**\n\t * Get the session summary model with default applied\n\t * Default is defined in SummarySettingsSchema\n\t *\n\t * @param settings - Pre-loaded settings object\n\t * @returns Model shorthand ('opus', 'sonnet', or 'haiku')\n\t */\n\tgetSummaryModel(settings?: IloomSettings): 'sonnet' | 'opus' | 'haiku' {\n\t\treturn settings?.summary?.model ?? SummarySettingsSchema.parse({}).model\n\t}\n}\n"],"mappings":"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","names":["path"]}