@gitgov/core 2.8.0 → 2.9.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.
package/dist/src/fs.js CHANGED
@@ -1,18 +1,18 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import { readdir, readFile } from 'fs/promises';
3
- import * as path9 from 'path';
4
- import path9__default, { join, dirname, basename, relative } from 'path';
3
+ import * as path6 from 'path';
4
+ import path6__default, { join, dirname, basename, relative } from 'path';
5
5
  import * as fs8 from 'fs';
6
- import { promises, existsSync, readFileSync, writeFileSync } from 'fs';
6
+ import { promises, existsSync, realpathSync } from 'fs';
7
7
  import fg from 'fast-glob';
8
- import { generateKeyPair, randomUUID, createHash } from 'crypto';
8
+ import { generateKeyPair, createHash, randomUUID } from 'crypto';
9
9
  import Ajv from 'ajv';
10
10
  import addFormats from 'ajv-formats';
11
11
  import { promisify } from 'util';
12
- import { exec, execSync } from 'child_process';
12
+ import { execSync } from 'child_process';
13
13
  import { createRequire } from 'module';
14
14
  import { fileURLToPath } from 'url';
15
- import os from 'os';
15
+ import * as os from 'os';
16
16
  import chokidar from 'chokidar';
17
17
 
18
18
  // src/record_store/fs/fs_record_store.ts
@@ -52,7 +52,7 @@ var FsRecordStore = class {
52
52
  getFilePath(id) {
53
53
  validateId(id);
54
54
  const fileId = this.idEncoder ? this.idEncoder.encode(id) : id;
55
- return path9.join(this.basePath, `${fileId}${this.extension}`);
55
+ return path6.join(this.basePath, `${fileId}${this.extension}`);
56
56
  }
57
57
  async get(id) {
58
58
  const filePath = this.getFilePath(id);
@@ -69,7 +69,7 @@ var FsRecordStore = class {
69
69
  async put(id, value) {
70
70
  const filePath = this.getFilePath(id);
71
71
  if (this.createIfMissing) {
72
- await fs.mkdir(path9.dirname(filePath), { recursive: true });
72
+ await fs.mkdir(path6.dirname(filePath), { recursive: true });
73
73
  }
74
74
  const content = this.serializer.stringify(value);
75
75
  await fs.writeFile(filePath, content, "utf-8");
@@ -219,7 +219,7 @@ var ConfigManager = class {
219
219
  var FsConfigStore = class {
220
220
  configPath;
221
221
  constructor(projectRootPath) {
222
- this.configPath = path9.join(projectRootPath, ".gitgov", "config.json");
222
+ this.configPath = path6.join(projectRootPath, ".gitgov", "config.json");
223
223
  }
224
224
  /**
225
225
  * Load project configuration from .gitgov/config.json
@@ -384,8 +384,8 @@ var FsSessionStore = class {
384
384
  sessionPath;
385
385
  keysPath;
386
386
  constructor(projectRootPath) {
387
- this.sessionPath = path9.join(projectRootPath, ".gitgov", ".session.json");
388
- this.keysPath = path9.join(projectRootPath, ".gitgov", "keys");
387
+ this.sessionPath = path6.join(projectRootPath, ".gitgov", ".session.json");
388
+ this.keysPath = path6.join(projectRootPath, ".gitgov", "keys");
389
389
  }
390
390
  /**
391
391
  * Load local session from .gitgov/.session.json
@@ -547,7 +547,7 @@ var FsKeyProvider = class {
547
547
  */
548
548
  getKeyPath(actorId) {
549
549
  const sanitized = this.sanitizeActorId(actorId);
550
- return path9.join(this.keysDir, `${sanitized}${this.extension}`);
550
+ return path6.join(this.keysDir, `${sanitized}${this.extension}`);
551
551
  }
552
552
  /**
553
553
  * [EARS-FKP04] Sanitizes actorId to prevent directory traversal.
@@ -605,7 +605,7 @@ var FsFileLister = class {
605
605
  pattern
606
606
  );
607
607
  }
608
- if (path9.isAbsolute(pattern)) {
608
+ if (path6.isAbsolute(pattern)) {
609
609
  throw new FileListerError(
610
610
  `Invalid pattern: absolute paths not allowed: ${pattern}`,
611
611
  "INVALID_PATH",
@@ -613,6 +613,7 @@ var FsFileLister = class {
613
613
  );
614
614
  }
615
615
  }
616
+ const normalized = patterns.map((p) => p.endsWith("/") ? `${p}**` : p);
616
617
  const fgOptions = {
617
618
  cwd: this.cwd,
618
619
  ignore: options?.ignore ?? [],
@@ -623,7 +624,7 @@ var FsFileLister = class {
623
624
  if (options?.maxDepth !== void 0) {
624
625
  fgOptions.deep = options.maxDepth;
625
626
  }
626
- return fg(patterns, fgOptions);
627
+ return fg(normalized, fgOptions);
627
628
  }
628
629
  /**
629
630
  * [EARS-FL02] Checks if a file exists.
@@ -631,7 +632,7 @@ var FsFileLister = class {
631
632
  async exists(filePath) {
632
633
  this.validatePath(filePath);
633
634
  try {
634
- const fullPath = path9.join(this.cwd, filePath);
635
+ const fullPath = path6.join(this.cwd, filePath);
635
636
  await fs.access(fullPath);
636
637
  return true;
637
638
  } catch {
@@ -644,7 +645,7 @@ var FsFileLister = class {
644
645
  */
645
646
  async read(filePath) {
646
647
  this.validatePath(filePath);
647
- const fullPath = path9.join(this.cwd, filePath);
648
+ const fullPath = path6.join(this.cwd, filePath);
648
649
  try {
649
650
  return await fs.readFile(fullPath, "utf-8");
650
651
  } catch (err) {
@@ -676,7 +677,7 @@ var FsFileLister = class {
676
677
  */
677
678
  async stat(filePath) {
678
679
  this.validatePath(filePath);
679
- const fullPath = path9.join(this.cwd, filePath);
680
+ const fullPath = path6.join(this.cwd, filePath);
680
681
  try {
681
682
  const stats = await fs.stat(fullPath);
682
683
  return {
@@ -712,7 +713,7 @@ var FsFileLister = class {
712
713
  filePath
713
714
  );
714
715
  }
715
- if (path9.isAbsolute(filePath)) {
716
+ if (path6.isAbsolute(filePath)) {
716
717
  throw new FileListerError(
717
718
  `Invalid path: absolute paths not allowed: ${filePath}`,
718
719
  "INVALID_PATH",
@@ -730,7 +731,7 @@ function isCyclePayload(payload) {
730
731
  return "title" in payload && "status" in payload && !("priority" in payload);
731
732
  }
732
733
 
733
- // src/utils/id_parser.ts
734
+ // src/record_types/common.types.ts
734
735
  var DIR_TO_TYPE = {
735
736
  "tasks": "task",
736
737
  "cycles": "cycle",
@@ -742,6 +743,15 @@ var DIR_TO_TYPE = {
742
743
  Object.fromEntries(
743
744
  Object.entries(DIR_TO_TYPE).map(([dir, type]) => [type, dir])
744
745
  );
746
+ var GitGovError = class extends Error {
747
+ constructor(message, code) {
748
+ super(message);
749
+ this.code = code;
750
+ this.name = this.constructor.name;
751
+ }
752
+ };
753
+
754
+ // src/utils/id_parser.ts
745
755
  var VALID_DIRS = Object.keys(DIR_TO_TYPE);
746
756
  function extractRecordIdFromPath(filePath) {
747
757
  const parts = filePath.split("/");
@@ -857,15 +867,6 @@ function calculatePayloadChecksum(payload) {
857
867
  return createHash("sha256").update(jsonString, "utf8").digest("hex");
858
868
  }
859
869
 
860
- // src/record_types/common.types.ts
861
- var GitGovError = class extends Error {
862
- constructor(message, code) {
863
- super(message);
864
- this.code = code;
865
- this.name = this.constructor.name;
866
- }
867
- };
868
-
869
870
  // src/record_schemas/errors.ts
870
871
  var DetailedValidationError = class extends GitGovError {
871
872
  constructor(recordType, errors) {
@@ -1731,6 +1732,7 @@ var embedded_metadata_schema_default = {
1731
1732
  if: {
1732
1733
  properties: {
1733
1734
  header: {
1735
+ type: "object",
1734
1736
  properties: {
1735
1737
  type: {
1736
1738
  const: "custom"
@@ -1755,6 +1757,7 @@ var embedded_metadata_schema_default = {
1755
1757
  if: {
1756
1758
  properties: {
1757
1759
  header: {
1760
+ type: "object",
1758
1761
  properties: {
1759
1762
  type: {
1760
1763
  const: "actor"
@@ -1775,6 +1778,7 @@ var embedded_metadata_schema_default = {
1775
1778
  if: {
1776
1779
  properties: {
1777
1780
  header: {
1781
+ type: "object",
1778
1782
  properties: {
1779
1783
  type: {
1780
1784
  const: "agent"
@@ -1795,6 +1799,7 @@ var embedded_metadata_schema_default = {
1795
1799
  if: {
1796
1800
  properties: {
1797
1801
  header: {
1802
+ type: "object",
1798
1803
  properties: {
1799
1804
  type: {
1800
1805
  const: "task"
@@ -1815,6 +1820,7 @@ var embedded_metadata_schema_default = {
1815
1820
  if: {
1816
1821
  properties: {
1817
1822
  header: {
1823
+ type: "object",
1818
1824
  properties: {
1819
1825
  type: {
1820
1826
  const: "execution"
@@ -1835,6 +1841,7 @@ var embedded_metadata_schema_default = {
1835
1841
  if: {
1836
1842
  properties: {
1837
1843
  header: {
1844
+ type: "object",
1838
1845
  properties: {
1839
1846
  type: {
1840
1847
  const: "feedback"
@@ -1855,6 +1862,7 @@ var embedded_metadata_schema_default = {
1855
1862
  if: {
1856
1863
  properties: {
1857
1864
  header: {
1865
+ type: "object",
1858
1866
  properties: {
1859
1867
  type: {
1860
1868
  const: "cycle"
@@ -1875,6 +1883,7 @@ var embedded_metadata_schema_default = {
1875
1883
  if: {
1876
1884
  properties: {
1877
1885
  header: {
1886
+ type: "object",
1878
1887
  properties: {
1879
1888
  type: {
1880
1889
  const: "workflow"
@@ -3117,25 +3126,25 @@ var FsLintModule = class {
3117
3126
  this.projectRoot = dependencies.projectRoot;
3118
3127
  this.lintModule = dependencies.lintModule;
3119
3128
  this.fileSystem = dependencies.fileSystem ?? {
3120
- readFile: async (path14, encoding) => {
3121
- return promises.readFile(path14, encoding);
3129
+ readFile: async (path13, encoding) => {
3130
+ return promises.readFile(path13, encoding);
3122
3131
  },
3123
- writeFile: async (path14, content) => {
3124
- await promises.writeFile(path14, content, "utf-8");
3132
+ writeFile: async (path13, content) => {
3133
+ await promises.writeFile(path13, content, "utf-8");
3125
3134
  },
3126
- exists: async (path14) => {
3135
+ exists: async (path13) => {
3127
3136
  try {
3128
- await promises.access(path14);
3137
+ await promises.access(path13);
3129
3138
  return true;
3130
3139
  } catch {
3131
3140
  return false;
3132
3141
  }
3133
3142
  },
3134
- unlink: async (path14) => {
3135
- await promises.unlink(path14);
3143
+ unlink: async (path13) => {
3144
+ await promises.unlink(path13);
3136
3145
  },
3137
- readdir: async (path14) => {
3138
- return readdir(path14);
3146
+ readdir: async (path13) => {
3147
+ return readdir(path13);
3139
3148
  }
3140
3149
  };
3141
3150
  }
@@ -3764,17 +3773,17 @@ var FsProjectInitializer = class {
3764
3773
  * Creates the .gitgov/ directory structure.
3765
3774
  */
3766
3775
  async createProjectStructure() {
3767
- const gitgovPath = path9.join(this.projectRoot, ".gitgov");
3776
+ const gitgovPath = path6.join(this.projectRoot, ".gitgov");
3768
3777
  await promises.mkdir(gitgovPath, { recursive: true });
3769
3778
  for (const dir of GITGOV_DIRECTORIES) {
3770
- await promises.mkdir(path9.join(gitgovPath, dir), { recursive: true });
3779
+ await promises.mkdir(path6.join(gitgovPath, dir), { recursive: true });
3771
3780
  }
3772
3781
  }
3773
3782
  /**
3774
3783
  * Checks if .gitgov/config.json exists.
3775
3784
  */
3776
3785
  async isInitialized() {
3777
- const configPath = path9.join(this.projectRoot, ".gitgov", "config.json");
3786
+ const configPath = path6.join(this.projectRoot, ".gitgov", "config.json");
3778
3787
  try {
3779
3788
  await promises.access(configPath);
3780
3789
  return true;
@@ -3786,14 +3795,14 @@ var FsProjectInitializer = class {
3786
3795
  * Writes config.json to .gitgov/
3787
3796
  */
3788
3797
  async writeConfig(config) {
3789
- const configPath = path9.join(this.projectRoot, ".gitgov", "config.json");
3798
+ const configPath = path6.join(this.projectRoot, ".gitgov", "config.json");
3790
3799
  await promises.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
3791
3800
  }
3792
3801
  /**
3793
3802
  * Creates .session.json with initial actor state.
3794
3803
  */
3795
3804
  async initializeSession(actorId) {
3796
- const sessionPath = path9.join(this.projectRoot, ".gitgov", ".session.json");
3805
+ const sessionPath = path6.join(this.projectRoot, ".gitgov", ".session.json");
3797
3806
  const session = {
3798
3807
  lastSession: {
3799
3808
  actorId,
@@ -3820,7 +3829,7 @@ var FsProjectInitializer = class {
3820
3829
  * Gets the path for an actor record.
3821
3830
  */
3822
3831
  getActorPath(actorId) {
3823
- return path9.join(this.projectRoot, ".gitgov", "actors", `${actorId}.json`);
3832
+ return path6.join(this.projectRoot, ".gitgov", "actors", `${actorId}.json`);
3824
3833
  }
3825
3834
  /**
3826
3835
  * Validates environment for GitGovernance initialization.
@@ -3830,7 +3839,7 @@ var FsProjectInitializer = class {
3830
3839
  const warnings = [];
3831
3840
  const suggestions = [];
3832
3841
  try {
3833
- const gitPath = path9.join(this.projectRoot, ".git");
3842
+ const gitPath = path6.join(this.projectRoot, ".git");
3834
3843
  const isGitRepo = existsSync(gitPath);
3835
3844
  if (!isGitRepo) {
3836
3845
  warnings.push(`Not a Git repository in directory: ${this.projectRoot}`);
@@ -3838,7 +3847,7 @@ var FsProjectInitializer = class {
3838
3847
  }
3839
3848
  let hasWritePermissions = false;
3840
3849
  try {
3841
- const testFile = path9.join(this.projectRoot, ".gitgov-test");
3850
+ const testFile = path6.join(this.projectRoot, ".gitgov-test");
3842
3851
  await promises.writeFile(testFile, "test");
3843
3852
  await promises.unlink(testFile);
3844
3853
  hasWritePermissions = true;
@@ -3896,7 +3905,7 @@ var FsProjectInitializer = class {
3896
3905
  currentBranch
3897
3906
  };
3898
3907
  if (isAlreadyInitialized) {
3899
- result.gitgovPath = path9.join(this.projectRoot, ".gitgov");
3908
+ result.gitgovPath = path6.join(this.projectRoot, ".gitgov");
3900
3909
  }
3901
3910
  return result;
3902
3911
  } catch (error) {
@@ -3916,18 +3925,18 @@ var FsProjectInitializer = class {
3916
3925
  * Copies the @gitgov agent prompt to project root for IDE access.
3917
3926
  */
3918
3927
  async copyAgentPrompt() {
3919
- const targetPrompt = path9.join(this.repoRoot, "gitgov");
3928
+ const targetPrompt = path6.join(this.repoRoot, "gitgov");
3920
3929
  const potentialSources = [];
3921
3930
  potentialSources.push(
3922
- path9.join(process.cwd(), "src/docs/generated/gitgov_agent.md")
3931
+ path6.join(process.cwd(), "src/docs/generated/gitgov_agent.md")
3923
3932
  );
3924
3933
  try {
3925
3934
  const metaUrl = getImportMetaUrl();
3926
3935
  if (metaUrl) {
3927
3936
  const require2 = createRequire(metaUrl);
3928
3937
  const pkgJsonPath = require2.resolve("@gitgov/core/package.json");
3929
- const pkgRoot = path9.dirname(pkgJsonPath);
3930
- potentialSources.push(path9.join(pkgRoot, "dist/src/docs/generated/gitgov_agent.md"));
3938
+ const pkgRoot = path6.dirname(pkgJsonPath);
3939
+ potentialSources.push(path6.join(pkgRoot, "dist/src/docs/generated/gitgov_agent.md"));
3931
3940
  }
3932
3941
  } catch {
3933
3942
  }
@@ -3935,8 +3944,8 @@ var FsProjectInitializer = class {
3935
3944
  const metaUrl = getImportMetaUrl();
3936
3945
  if (metaUrl) {
3937
3946
  const __filename = fileURLToPath(metaUrl);
3938
- const __dirname = path9.dirname(__filename);
3939
- potentialSources.push(path9.resolve(__dirname, "../../docs/generated/gitgov_agent.md"));
3947
+ const __dirname = path6.dirname(__filename);
3948
+ potentialSources.push(path6.resolve(__dirname, "../../docs/generated/gitgov_agent.md"));
3940
3949
  }
3941
3950
  } catch {
3942
3951
  }
@@ -3959,7 +3968,7 @@ var FsProjectInitializer = class {
3959
3968
  * Sets up .gitignore for GitGovernance files.
3960
3969
  */
3961
3970
  async setupGitIntegration() {
3962
- const gitignorePath = path9.join(this.repoRoot, ".gitignore");
3971
+ const gitignorePath = path6.join(this.repoRoot, ".gitignore");
3963
3972
  const gitignoreContent = `
3964
3973
  # GitGovernance
3965
3974
  # Ignore entire .gitgov/ directory (state lives in gitgov-state branch)
@@ -3987,7 +3996,7 @@ gitgov
3987
3996
  * Removes .gitgov/ directory (for rollback on failed init).
3988
3997
  */
3989
3998
  async rollback() {
3990
- const gitgovPath = path9.join(this.projectRoot, ".gitgov");
3999
+ const gitgovPath = path6.join(this.projectRoot, ".gitgov");
3991
4000
  try {
3992
4001
  await promises.access(gitgovPath);
3993
4002
  await promises.rm(gitgovPath, { recursive: true, force: true });
@@ -4222,7 +4231,7 @@ var LocalGitModule = class {
4222
4231
  if (result.exitCode !== 0) {
4223
4232
  try {
4224
4233
  const repoRoot = await this.ensureRepoRoot();
4225
- const headPath = path9.join(repoRoot, ".git", "HEAD");
4234
+ const headPath = path6.join(repoRoot, ".git", "HEAD");
4226
4235
  const headContent = fs8.readFileSync(headPath, "utf-8").trim();
4227
4236
  if (headContent.startsWith("ref: refs/heads/")) {
4228
4237
  return headContent.replace("ref: refs/heads/", "");
@@ -4562,8 +4571,8 @@ var LocalGitModule = class {
4562
4571
  */
4563
4572
  async isRebaseInProgress() {
4564
4573
  const repoRoot = await this.ensureRepoRoot();
4565
- const rebaseMergePath = path9.join(repoRoot, ".git", "rebase-merge");
4566
- const rebaseApplyPath = path9.join(repoRoot, ".git", "rebase-apply");
4574
+ const rebaseMergePath = path6.join(repoRoot, ".git", "rebase-merge");
4575
+ const rebaseApplyPath = path6.join(repoRoot, ".git", "rebase-apply");
4567
4576
  return fs8.existsSync(rebaseMergePath) || fs8.existsSync(rebaseApplyPath);
4568
4577
  }
4569
4578
  /**
@@ -5111,10 +5120,6 @@ var LocalGitModule = class {
5111
5120
  var projectRootCache = null;
5112
5121
  var lastSearchPath = null;
5113
5122
  function findProjectRoot(startPath = process.cwd()) {
5114
- if (typeof global.projectRoot !== "undefined" && global.projectRoot === null) {
5115
- projectRootCache = null;
5116
- lastSearchPath = null;
5117
- }
5118
5123
  if (lastSearchPath && lastSearchPath !== startPath) {
5119
5124
  projectRootCache = null;
5120
5125
  lastSearchPath = null;
@@ -5124,61 +5129,67 @@ function findProjectRoot(startPath = process.cwd()) {
5124
5129
  }
5125
5130
  lastSearchPath = startPath;
5126
5131
  let currentPath = startPath;
5127
- while (currentPath !== path9.parse(currentPath).root) {
5128
- if (existsSync(path9.join(currentPath, ".git"))) {
5132
+ while (currentPath !== path6.parse(currentPath).root) {
5133
+ if (existsSync(path6.join(currentPath, ".git"))) {
5129
5134
  projectRootCache = currentPath;
5130
5135
  return projectRootCache;
5131
5136
  }
5132
- currentPath = path9.dirname(currentPath);
5137
+ currentPath = path6.dirname(currentPath);
5133
5138
  }
5134
- if (existsSync(path9.join(currentPath, ".git"))) {
5139
+ if (existsSync(path6.join(currentPath, ".git"))) {
5135
5140
  projectRootCache = currentPath;
5136
5141
  return projectRootCache;
5137
5142
  }
5138
5143
  return null;
5139
5144
  }
5140
- function findGitgovRoot(startPath = process.cwd()) {
5141
- let currentPath = startPath;
5142
- while (currentPath !== path9.parse(currentPath).root) {
5143
- if (existsSync(path9.join(currentPath, ".gitgov"))) {
5144
- return currentPath;
5145
- }
5146
- currentPath = path9.dirname(currentPath);
5147
- }
5148
- if (existsSync(path9.join(currentPath, ".gitgov"))) {
5149
- return currentPath;
5150
- }
5151
- currentPath = startPath;
5152
- while (currentPath !== path9.parse(currentPath).root) {
5153
- if (existsSync(path9.join(currentPath, ".git"))) {
5154
- return currentPath;
5155
- }
5156
- currentPath = path9.dirname(currentPath);
5157
- }
5158
- if (existsSync(path9.join(currentPath, ".git"))) {
5159
- return currentPath;
5160
- }
5161
- return null;
5162
- }
5163
- function getGitgovPath() {
5164
- const root = findGitgovRoot();
5165
- if (!root) {
5166
- throw new Error("Could not find project root. Make sure you are inside a GitGovernance repository.");
5167
- }
5168
- return path9.join(root, ".gitgov");
5169
- }
5170
- function isGitgovProject() {
5171
- try {
5172
- const gitgovPath = getGitgovPath();
5173
- return existsSync(gitgovPath);
5174
- } catch {
5175
- return false;
5176
- }
5177
- }
5178
5145
  function resetDiscoveryCache() {
5179
5146
  projectRootCache = null;
5180
5147
  lastSearchPath = null;
5181
5148
  }
5149
+ function getWorktreeBasePath(repoRoot) {
5150
+ const resolvedPath = realpathSync(repoRoot);
5151
+ const hash = createHash("sha256").update(resolvedPath).digest("hex").slice(0, 12);
5152
+ return path6.join(os.homedir(), ".gitgov", "worktrees", hash);
5153
+ }
5154
+
5155
+ // src/sync_state/sync_state.types.ts
5156
+ var SYNC_DIRECTORIES = [
5157
+ "tasks",
5158
+ "cycles",
5159
+ "actors",
5160
+ "agents",
5161
+ "feedbacks",
5162
+ "executions",
5163
+ "workflows"
5164
+ ];
5165
+ var SYNC_ROOT_FILES = [
5166
+ "config.json"
5167
+ ];
5168
+ var SYNC_ALLOWED_EXTENSIONS = [".json"];
5169
+ var SYNC_EXCLUDED_PATTERNS = [
5170
+ /\.key$/,
5171
+ // Private keys (e.g., keys/*.key)
5172
+ /\.backup$/,
5173
+ // Backup files from lint
5174
+ /\.backup-\d+$/,
5175
+ // Numbered backup files
5176
+ /\.tmp$/,
5177
+ // Temporary files
5178
+ /\.bak$/
5179
+ // Backup files
5180
+ ];
5181
+ var LOCAL_ONLY_FILES = [
5182
+ "index.json",
5183
+ // Generated index, rebuilt on each machine
5184
+ ".session.json",
5185
+ // Local session state for current user/agent
5186
+ "gitgov"
5187
+ // Local binary/script
5188
+ ];
5189
+
5190
+ // src/sync_state/fs_worktree/fs_worktree_sync_state.types.ts
5191
+ var WORKTREE_DIR_NAME = ".gitgov-worktree";
5192
+ var DEFAULT_STATE_BRANCH = "gitgov-state";
5182
5193
 
5183
5194
  // src/sync_state/sync_state.errors.ts
5184
5195
  var SyncStateError = class _SyncStateError extends Error {
@@ -5188,17 +5199,6 @@ var SyncStateError = class _SyncStateError extends Error {
5188
5199
  Object.setPrototypeOf(this, _SyncStateError.prototype);
5189
5200
  }
5190
5201
  };
5191
- var PushFromStateBranchError = class _PushFromStateBranchError extends SyncStateError {
5192
- branch;
5193
- constructor(branchName) {
5194
- super(
5195
- `Cannot push from ${branchName} branch. Please switch to a working branch before pushing state.`
5196
- );
5197
- this.name = "PushFromStateBranchError";
5198
- this.branch = branchName;
5199
- Object.setPrototypeOf(this, _PushFromStateBranchError.prototype);
5200
- }
5201
- };
5202
5202
  var ConflictMarkersPresentError = class _ConflictMarkersPresentError extends SyncStateError {
5203
5203
  constructor(filesWithMarkers) {
5204
5204
  super(
@@ -5259,59 +5259,10 @@ var RebaseAlreadyInProgressError = class _RebaseAlreadyInProgressError extends S
5259
5259
  Object.setPrototypeOf(this, _RebaseAlreadyInProgressError.prototype);
5260
5260
  }
5261
5261
  };
5262
- var UncommittedChangesError = class _UncommittedChangesError extends SyncStateError {
5263
- branch;
5264
- constructor(branchName) {
5265
- super(
5266
- `Uncommitted changes detected in ${branchName}. Please commit or stash changes before synchronizing.`
5267
- );
5268
- this.name = "UncommittedChangesError";
5269
- this.branch = branchName;
5270
- Object.setPrototypeOf(this, _UncommittedChangesError.prototype);
5271
- }
5272
- };
5273
-
5274
- // src/sync_state/sync_state.types.ts
5275
- var SYNC_DIRECTORIES = [
5276
- "tasks",
5277
- "cycles",
5278
- "actors",
5279
- "agents",
5280
- "feedbacks",
5281
- "executions",
5282
- "workflows"
5283
- ];
5284
- var SYNC_ROOT_FILES = [
5285
- "config.json"
5286
- ];
5287
- var SYNC_ALLOWED_EXTENSIONS = [".json"];
5288
- var SYNC_EXCLUDED_PATTERNS = [
5289
- /\.key$/,
5290
- // Private keys (e.g., keys/*.key)
5291
- /\.backup$/,
5292
- // Backup files from lint
5293
- /\.backup-\d+$/,
5294
- // Numbered backup files
5295
- /\.tmp$/,
5296
- // Temporary files
5297
- /\.bak$/
5298
- // Backup files
5299
- ];
5300
- var LOCAL_ONLY_FILES = [
5301
- "index.json",
5302
- // Generated index, rebuilt on each machine
5303
- ".session.json",
5304
- // Local session state for current user/agent
5305
- "gitgov"
5306
- // Local binary/script
5307
- ];
5308
-
5309
- // src/sync_state/fs/fs_sync_state.ts
5310
- var execAsync = promisify(exec);
5311
- var logger6 = createLogger("[SyncStateModule] ");
5262
+ var logger6 = createLogger("[WorktreeSyncState] ");
5312
5263
  function shouldSyncFile(filePath) {
5313
- const fileName = path9__default.basename(filePath);
5314
- const ext = path9__default.extname(filePath);
5264
+ const fileName = path6__default.basename(filePath);
5265
+ const ext = path6__default.extname(filePath);
5315
5266
  if (!SYNC_ALLOWED_EXTENSIONS.includes(ext)) {
5316
5267
  return false;
5317
5268
  }
@@ -5349,1987 +5300,88 @@ function shouldSyncFile(filePath) {
5349
5300
  }
5350
5301
  return false;
5351
5302
  }
5352
- async function getAllFiles(dir, baseDir = dir) {
5353
- const files = [];
5354
- try {
5355
- const entries = await promises.readdir(dir, { withFileTypes: true });
5356
- for (const entry of entries) {
5357
- const fullPath = path9__default.join(dir, entry.name);
5358
- if (entry.isDirectory()) {
5359
- const subFiles = await getAllFiles(fullPath, baseDir);
5360
- files.push(...subFiles);
5361
- } else if (entry.isFile()) {
5362
- files.push(path9__default.relative(baseDir, fullPath));
5363
- }
5364
- }
5365
- } catch {
5303
+ var FsWorktreeSyncStateModule = class {
5304
+ deps;
5305
+ repoRoot;
5306
+ stateBranchName;
5307
+ worktreePath;
5308
+ gitgovPath;
5309
+ constructor(deps, config) {
5310
+ if (!deps.git) throw new Error("GitModule is required for FsWorktreeSyncStateModule");
5311
+ if (!deps.config) throw new Error("ConfigManager is required for FsWorktreeSyncStateModule");
5312
+ if (!deps.identity) throw new Error("IdentityAdapter is required for FsWorktreeSyncStateModule");
5313
+ if (!deps.lint) throw new Error("LintModule is required for FsWorktreeSyncStateModule");
5314
+ if (!deps.indexer) throw new Error("IndexerAdapter is required for FsWorktreeSyncStateModule");
5315
+ if (!config.repoRoot) throw new Error("repoRoot is required");
5316
+ this.deps = deps;
5317
+ this.repoRoot = config.repoRoot;
5318
+ this.stateBranchName = config.stateBranchName ?? DEFAULT_STATE_BRANCH;
5319
+ this.worktreePath = config.worktreePath ?? path6__default.join(this.repoRoot, WORKTREE_DIR_NAME);
5320
+ this.gitgovPath = path6__default.join(this.worktreePath, ".gitgov");
5366
5321
  }
5367
- return files;
5368
- }
5369
- async function copySyncableFiles(sourceDir, destDir, log, excludeFiles = /* @__PURE__ */ new Set()) {
5370
- let copiedCount = 0;
5371
- for (const dirName of SYNC_DIRECTORIES) {
5372
- const sourcePath = path9__default.join(sourceDir, dirName);
5373
- const destPath = path9__default.join(destDir, dirName);
5374
- try {
5375
- const stat2 = await promises.stat(sourcePath);
5376
- if (!stat2.isDirectory()) continue;
5377
- const allFiles = await getAllFiles(sourcePath);
5378
- for (const relativePath of allFiles) {
5379
- const fullSourcePath = path9__default.join(sourcePath, relativePath);
5380
- const fullDestPath = path9__default.join(destPath, relativePath);
5381
- const gitgovRelativePath = `.gitgov/${dirName}/${relativePath}`;
5382
- if (excludeFiles.has(gitgovRelativePath)) {
5383
- log(`[EARS-B23] Skipped (remote-only change preserved): ${gitgovRelativePath}`);
5384
- continue;
5385
- }
5386
- if (shouldSyncFile(fullSourcePath)) {
5387
- await promises.mkdir(path9__default.dirname(fullDestPath), { recursive: true });
5388
- await promises.copyFile(fullSourcePath, fullDestPath);
5389
- log(`Copied: ${dirName}/${relativePath}`);
5390
- copiedCount++;
5391
- } else {
5392
- log(`Skipped (not syncable): ${dirName}/${relativePath}`);
5393
- }
5394
- }
5395
- } catch (error) {
5396
- const errCode = error.code;
5397
- if (errCode !== "ENOENT") {
5398
- log(`Error processing ${dirName}: ${error}`);
5399
- }
5400
- }
5322
+ // ═══════════════════════════════════════════════
5323
+ // Section A: Worktree Management (WTSYNC-A1..A7)
5324
+ // ═══════════════════════════════════════════════
5325
+ /** [WTSYNC-A4] Returns the worktree path */
5326
+ getWorktreePath() {
5327
+ return this.worktreePath;
5401
5328
  }
5402
- for (const fileName of SYNC_ROOT_FILES) {
5403
- const sourcePath = path9__default.join(sourceDir, fileName);
5404
- const destPath = path9__default.join(destDir, fileName);
5405
- const gitgovRelativePath = `.gitgov/${fileName}`;
5406
- if (excludeFiles.has(gitgovRelativePath)) {
5407
- log(`[EARS-B23] Skipped (remote-only change preserved): ${gitgovRelativePath}`);
5408
- continue;
5329
+ /** [WTSYNC-A1..A6] Ensures worktree exists and is healthy */
5330
+ async ensureWorktree() {
5331
+ const health = await this.checkWorktreeHealth();
5332
+ if (health.healthy) {
5333
+ logger6.debug("Worktree is healthy");
5334
+ await this.removeLegacyGitignore();
5335
+ return;
5336
+ }
5337
+ if (health.exists && !health.healthy) {
5338
+ logger6.warn(`Worktree corrupted: ${health.error}. Recreating...`);
5339
+ await this.removeWorktree();
5409
5340
  }
5341
+ await this.ensureStateBranch();
5410
5342
  try {
5411
- await promises.copyFile(sourcePath, destPath);
5412
- log(`Copied root file: ${fileName}`);
5413
- copiedCount++;
5343
+ logger6.info(`Creating worktree at ${this.worktreePath}`);
5344
+ await this.execGit(["worktree", "add", this.worktreePath, this.stateBranchName]);
5414
5345
  } catch (error) {
5415
- const errCode = error.code;
5416
- if (errCode !== "ENOENT") {
5417
- log(`Error copying ${fileName}: ${error}`);
5418
- }
5419
- }
5420
- }
5421
- return copiedCount;
5422
- }
5423
- var FsSyncStateModule = class {
5424
- git;
5425
- config;
5426
- identity;
5427
- lint;
5428
- indexer;
5429
- /**
5430
- * Constructor with dependency injection
5431
- */
5432
- constructor(dependencies) {
5433
- if (!dependencies.git) {
5434
- throw new Error("IGitModule is required for SyncStateModule");
5435
- }
5436
- if (!dependencies.config) {
5437
- throw new Error("ConfigManager is required for SyncStateModule");
5438
- }
5439
- if (!dependencies.identity) {
5440
- throw new Error("IdentityAdapter is required for SyncStateModule");
5441
- }
5442
- if (!dependencies.lint) {
5443
- throw new Error("LintModule is required for SyncStateModule");
5444
- }
5445
- if (!dependencies.indexer) {
5446
- throw new Error("RecordProjector is required for SyncStateModule");
5346
+ throw new WorktreeSetupError(
5347
+ "Failed to create worktree",
5348
+ this.worktreePath,
5349
+ error instanceof Error ? error : void 0
5350
+ );
5447
5351
  }
5448
- this.git = dependencies.git;
5449
- this.config = dependencies.config;
5450
- this.identity = dependencies.identity;
5451
- this.lint = dependencies.lint;
5452
- this.indexer = dependencies.indexer;
5352
+ await this.removeLegacyGitignore();
5453
5353
  }
5454
5354
  /**
5455
- * Static method to bootstrap .gitgov/ from gitgov-state branch.
5456
- * Used when cloning a repo that has gitgov-state but .gitgov/ is not in the work branch.
5457
- *
5458
- * This method only requires GitModule and can be called before full SyncStateModule initialization.
5459
- *
5460
- * @param gitModule - GitModule instance for git operations
5461
- * @param stateBranch - Name of the state branch (default: "gitgov-state")
5462
- * @returns Promise<{ success: boolean; error?: string }>
5355
+ * [WTSYNC-A7] Remove .gitignore from state branch if it exists.
5356
+ * The worktree module filters files in code (shouldSyncFile()), not via .gitignore.
5357
+ * Legacy state branches initialized by FsSyncState may have a .gitignore — remove it.
5463
5358
  */
5464
- static async bootstrapFromStateBranch(gitModule, stateBranch = "gitgov-state") {
5359
+ async removeLegacyGitignore() {
5360
+ const gitignorePath = path6__default.join(this.worktreePath, ".gitignore");
5361
+ if (!existsSync(gitignorePath)) return;
5362
+ logger6.info("Removing legacy .gitignore from state branch");
5465
5363
  try {
5466
- const repoRoot = await gitModule.getRepoRoot();
5467
- const hasLocalBranch = await gitModule.branchExists(stateBranch);
5468
- let hasRemoteBranch = false;
5469
- try {
5470
- const remoteBranches = await gitModule.listRemoteBranches("origin");
5471
- hasRemoteBranch = remoteBranches.includes(stateBranch);
5472
- } catch {
5473
- }
5474
- if (!hasLocalBranch && !hasRemoteBranch) {
5475
- return {
5476
- success: false,
5477
- error: `State branch '${stateBranch}' does not exist locally or remotely`
5478
- };
5479
- }
5480
- if (!hasLocalBranch && hasRemoteBranch) {
5481
- try {
5482
- const currentBranch = await gitModule.getCurrentBranch();
5483
- await gitModule.fetch("origin");
5484
- await execAsync(`git checkout -b ${stateBranch} origin/${stateBranch}`, { cwd: repoRoot });
5485
- if (currentBranch && currentBranch !== stateBranch) {
5486
- await gitModule.checkoutBranch(currentBranch);
5487
- }
5488
- } catch (error) {
5489
- return {
5490
- success: false,
5491
- error: `Failed to fetch state branch: ${error.message}`
5492
- };
5493
- }
5494
- }
5364
+ await this.execInWorktree(["rm", ".gitignore"]);
5365
+ await this.execInWorktree(["commit", "-m", "gitgov: remove legacy .gitignore (filtering is in code)"]);
5366
+ } catch {
5495
5367
  try {
5496
- const { stdout } = await execAsync(`git ls-tree -r ${stateBranch} --name-only .gitgov/`, { cwd: repoRoot });
5497
- if (!stdout.trim()) {
5498
- return {
5499
- success: false,
5500
- error: `No .gitgov/ directory found in '${stateBranch}' branch`
5501
- };
5502
- }
5368
+ await promises.unlink(gitignorePath);
5503
5369
  } catch {
5504
- return {
5505
- success: false,
5506
- error: `Failed to check .gitgov/ in '${stateBranch}' branch`
5507
- };
5508
- }
5509
- try {
5510
- await execAsync(`git checkout ${stateBranch} -- .gitgov/`, { cwd: repoRoot });
5511
- await execAsync("git reset HEAD .gitgov/", { cwd: repoRoot });
5512
- logger6.info(`[bootstrapFromStateBranch] Successfully restored .gitgov/ from ${stateBranch}`);
5513
- } catch (error) {
5514
- return {
5515
- success: false,
5516
- error: `Failed to copy .gitgov/ from state branch: ${error.message}`
5517
- };
5518
5370
  }
5519
- return { success: true };
5520
- } catch (error) {
5371
+ }
5372
+ }
5373
+ /** Check worktree health */
5374
+ async checkWorktreeHealth() {
5375
+ if (!existsSync(this.worktreePath)) {
5376
+ return { exists: false, healthy: false, path: this.worktreePath };
5377
+ }
5378
+ const gitFile = path6__default.join(this.worktreePath, ".git");
5379
+ if (!existsSync(gitFile)) {
5521
5380
  return {
5522
- success: false,
5523
- error: `Bootstrap failed: ${error.message}`
5524
- };
5525
- }
5526
- }
5527
- /**
5528
- * Gets the state branch name from configuration.
5529
- * Default: "gitgov-state"
5530
- *
5531
- * [EARS-A4]
5532
- */
5533
- async getStateBranchName() {
5534
- try {
5535
- const config = await this.config.loadConfig();
5536
- return config?.state?.branch ?? "gitgov-state";
5537
- } catch {
5538
- return "gitgov-state";
5539
- }
5540
- }
5541
- /**
5542
- * Ensures that the gitgov-state branch exists both locally and remotely.
5543
- * If it doesn't exist, creates it as an orphan branch.
5544
- *
5545
- * Use cases (4 edge cases):
5546
- * 1. Doesn't exist locally or remotely → Create orphan branch + initial commit + push
5547
- * 2. Exists remotely, not locally → Fetch + create local + set tracking
5548
- * 3. Exists locally, not remotely → Push + set tracking
5549
- * 4. Exists both → Verify tracking
5550
- *
5551
- * [EARS-A1, EARS-A2, EARS-A3]
5552
- */
5553
- async ensureStateBranch() {
5554
- const stateBranch = await this.getStateBranchName();
5555
- const remoteName = "origin";
5556
- try {
5557
- const existsLocal = await this.git.branchExists(stateBranch);
5558
- try {
5559
- await this.git.fetch(remoteName);
5560
- } catch {
5561
- }
5562
- const remoteBranches = await this.git.listRemoteBranches(remoteName);
5563
- const existsRemote = remoteBranches.includes(stateBranch);
5564
- if (!existsLocal && !existsRemote) {
5565
- await this.createOrphanStateBranch(stateBranch, remoteName);
5566
- return;
5567
- }
5568
- if (!existsLocal && existsRemote) {
5569
- const currentBranch = await this.git.getCurrentBranch();
5570
- const repoRoot = await this.git.getRepoRoot();
5571
- try {
5572
- await execAsync(`git checkout -b ${stateBranch} ${remoteName}/${stateBranch}`, { cwd: repoRoot });
5573
- if (currentBranch !== stateBranch) {
5574
- await this.git.checkoutBranch(currentBranch);
5575
- }
5576
- } catch (checkoutError) {
5577
- try {
5578
- await this.git.checkoutBranch(currentBranch);
5579
- } catch {
5580
- }
5581
- throw checkoutError;
5582
- }
5583
- return;
5584
- }
5585
- if (existsLocal && !existsRemote) {
5586
- const currentBranch = await this.git.getCurrentBranch();
5587
- if (currentBranch !== stateBranch) {
5588
- await this.git.checkoutBranch(stateBranch);
5589
- }
5590
- try {
5591
- await this.git.pushWithUpstream(remoteName, stateBranch);
5592
- } catch {
5593
- }
5594
- if (currentBranch !== stateBranch) {
5595
- await this.git.checkoutBranch(currentBranch);
5596
- }
5597
- return;
5598
- }
5599
- if (existsLocal && existsRemote) {
5600
- const upstreamBranch = await this.git.getBranchRemote(stateBranch);
5601
- if (!upstreamBranch || upstreamBranch !== `${remoteName}/${stateBranch}`) {
5602
- await this.git.setUpstream(stateBranch, remoteName, stateBranch);
5603
- }
5604
- return;
5605
- }
5606
- } catch (error) {
5607
- const errorMessage = error instanceof Error ? error.message : String(error);
5608
- throw new StateBranchSetupError(
5609
- `Failed to ensure state branch ${stateBranch}: ${errorMessage}`,
5610
- error
5611
- );
5612
- }
5613
- }
5614
- /**
5615
- * Creates the gitgov-state orphan branch with an empty initial commit.
5616
- * Used by ensureStateBranch when the branch doesn't exist locally or remotely.
5617
- *
5618
- * [EARS-A1]
5619
- */
5620
- async createOrphanStateBranch(stateBranch, remoteName) {
5621
- const currentBranch = await this.git.getCurrentBranch();
5622
- const repoRoot = await this.git.getRepoRoot();
5623
- const currentBranchHasCommits = await this.git.branchExists(currentBranch);
5624
- if (!currentBranchHasCommits) {
5625
- throw new Error(
5626
- `Cannot initialize GitGovernance: branch '${currentBranch}' has no commits. Please create an initial commit first (e.g., 'git commit --allow-empty -m "Initial commit"').`
5627
- );
5628
- }
5629
- try {
5630
- await this.git.checkoutOrphanBranch(stateBranch);
5631
- try {
5632
- await execAsync("git rm -rf . 2>/dev/null || true", { cwd: repoRoot });
5633
- const gitignoreContent = `# GitGovernance State Branch .gitignore
5634
- # This file is auto-generated during gitgov init
5635
- # These files are machine-specific and should NOT be synced
5636
-
5637
- # Local-only files (regenerated/machine-specific)
5638
- index.json
5639
- .session.json
5640
- gitgov
5641
-
5642
- # Private keys (never synced for security)
5643
- *.key
5644
-
5645
- # Backup and temporary files
5646
- *.backup
5647
- *.backup-*
5648
- *.tmp
5649
- *.bak
5650
- `;
5651
- const gitignorePath = path9__default.join(repoRoot, ".gitignore");
5652
- await promises.writeFile(gitignorePath, gitignoreContent, "utf-8");
5653
- await execAsync("git add .gitignore", { cwd: repoRoot });
5654
- await execAsync('git commit -m "Initialize state branch with .gitignore"', { cwd: repoRoot });
5655
- } catch (commitError) {
5656
- const error = commitError;
5657
- throw new Error(`Failed to create initial commit on orphan branch: ${error.stderr || error.message}`);
5658
- }
5659
- const hasRemote = await this.git.isRemoteConfigured(remoteName);
5660
- if (hasRemote) {
5661
- try {
5662
- await this.git.pushWithUpstream(remoteName, stateBranch);
5663
- } catch (pushError) {
5664
- const pushErrorMsg = pushError instanceof Error ? pushError.message : String(pushError);
5665
- const isRemoteError = pushErrorMsg.includes("does not appear to be") || pushErrorMsg.includes("Could not read from remote") || pushErrorMsg.includes("repository not found");
5666
- if (!isRemoteError) {
5667
- throw new Error(`Failed to push state branch to remote: ${pushErrorMsg}`);
5668
- }
5669
- logger6.info(`Remote '${remoteName}' not reachable, gitgov-state branch created locally only`);
5670
- }
5671
- } else {
5672
- logger6.info(`No remote '${remoteName}' configured, gitgov-state branch created locally only`);
5673
- }
5674
- await this.git.checkoutBranch(currentBranch);
5675
- } catch (error) {
5676
- try {
5677
- await this.git.checkoutBranch(currentBranch);
5678
- } catch {
5679
- }
5680
- throw error;
5681
- }
5682
- }
5683
- /** Returns pending local changes not yet synced (delegates to calculateStateDelta) */
5684
- async getPendingChanges() {
5685
- try {
5686
- return await this.calculateStateDelta("HEAD");
5687
- } catch {
5688
- return [];
5689
- }
5690
- }
5691
- /**
5692
- * Calculates the file delta in .gitgov/ between the current branch and gitgov-state.
5693
- *
5694
- * [EARS-A5]
5695
- */
5696
- async calculateStateDelta(sourceBranch) {
5697
- const stateBranch = await this.getStateBranchName();
5698
- if (!stateBranch) {
5699
- throw new SyncStateError("Failed to get state branch name");
5700
- }
5701
- try {
5702
- const changedFiles = await this.git.getChangedFiles(
5703
- stateBranch,
5704
- sourceBranch,
5705
- ".gitgov/"
5706
- );
5707
- return changedFiles.map((file) => ({
5708
- status: file.status,
5709
- file: file.file
5710
- }));
5711
- } catch (error) {
5712
- throw new SyncStateError(
5713
- `Failed to calculate state delta: ${error.message}`
5714
- );
5715
- }
5716
- }
5717
- /**
5718
- * [EARS-B23] Detect file-level conflicts and identify remote-only changes.
5719
- *
5720
- * A conflict exists when:
5721
- * 1. A file was modified by the remote during implicit pull
5722
- * 2. AND the LOCAL USER also modified that same file (content in tempDir differs from what was in git before pull)
5723
- *
5724
- * This catches conflicts that git rebase can't detect because we copy files AFTER the pull.
5725
- *
5726
- * @param tempDir - Directory containing local .gitgov/ files (preserved before checkout)
5727
- * @param repoRoot - Repository root path
5728
- /**
5729
- * Checks if a rebase is in progress.
5730
- *
5731
- * [EARS-D6]
5732
- */
5733
- async isRebaseInProgress() {
5734
- return await this.git.isRebaseInProgress();
5735
- }
5736
- /**
5737
- * Checks for absence of conflict markers in specified files.
5738
- * Returns list of files that still have markers.
5739
- *
5740
- * [EARS-D7]
5741
- */
5742
- async checkConflictMarkers(filePaths) {
5743
- const repoRoot = await this.git.getRepoRoot();
5744
- const filesWithMarkers = [];
5745
- for (const filePath of filePaths) {
5746
- try {
5747
- const fullPath = join(repoRoot, filePath);
5748
- if (!existsSync(fullPath)) {
5749
- continue;
5750
- }
5751
- const content = readFileSync(fullPath, "utf-8");
5752
- const hasMarkers = content.includes("<<<<<<<") || content.includes("=======") || content.includes(">>>>>>>");
5753
- if (hasMarkers) {
5754
- filesWithMarkers.push(filePath);
5755
- }
5756
- } catch {
5757
- continue;
5758
- }
5759
- }
5760
- return filesWithMarkers;
5761
- }
5762
- /**
5763
- * Gets the diff of conflicted files for manual analysis.
5764
- * Useful so the actor can analyze conflicted changes before resolving.
5765
- *
5766
- * [EARS-E8]
5767
- */
5768
- async getConflictDiff(filePaths) {
5769
- try {
5770
- let conflictedFiles;
5771
- if (filePaths && filePaths.length > 0) {
5772
- conflictedFiles = filePaths;
5773
- } else {
5774
- conflictedFiles = await this.git.getConflictedFiles();
5775
- }
5776
- if (conflictedFiles.length === 0) {
5777
- return {
5778
- files: [],
5779
- message: "No conflicted files found",
5780
- resolutionSteps: []
5781
- };
5782
- }
5783
- const repoRoot = await this.git.getRepoRoot();
5784
- const files = [];
5785
- for (const filePath of conflictedFiles) {
5786
- const fullPath = join(repoRoot, filePath);
5787
- try {
5788
- const localContent = existsSync(fullPath) ? readFileSync(fullPath, "utf-8") : "";
5789
- const remoteContent = "";
5790
- const baseContent = null;
5791
- const conflictMarkers = [];
5792
- const lines = localContent.split("\n");
5793
- lines.forEach((line, index) => {
5794
- if (line.startsWith("<<<<<<<")) {
5795
- conflictMarkers.push({ line: index + 1, marker: "<<<<<<" });
5796
- } else if (line.startsWith("=======")) {
5797
- conflictMarkers.push({ line: index + 1, marker: "=======" });
5798
- } else if (line.startsWith(">>>>>>>")) {
5799
- conflictMarkers.push({ line: index + 1, marker: ">>>>>>>" });
5800
- }
5801
- });
5802
- const fileDiff = {
5803
- filePath,
5804
- localContent,
5805
- remoteContent,
5806
- baseContent
5807
- };
5808
- if (conflictMarkers.length > 0) {
5809
- fileDiff.conflictMarkers = conflictMarkers;
5810
- }
5811
- files.push(fileDiff);
5812
- } catch {
5813
- continue;
5814
- }
5815
- }
5816
- return {
5817
- files,
5818
- message: `${files.length} file(s) in conflict`,
5819
- resolutionSteps: [
5820
- "1. Review the conflict diff for each file",
5821
- "2. Manually edit conflicted files to resolve conflicts",
5822
- "3. Remove all conflict markers (<<<<<<<, =======, >>>>>>>)",
5823
- "4. Run 'gitgov sync resolve' to complete the resolution"
5824
- ]
5825
- };
5826
- } catch (error) {
5827
- throw new SyncStateError(
5828
- `Failed to get conflict diff: ${error.message}`
5829
- );
5830
- }
5831
- }
5832
- /**
5833
- * Verifies integrity of previous resolutions in gitgov-state history.
5834
- * Returns list of violations if any exist.
5835
- *
5836
- * [EARS-E1, EARS-E2, EARS-E3]
5837
- */
5838
- async verifyResolutionIntegrity() {
5839
- const stateBranch = await this.getStateBranchName();
5840
- if (!stateBranch) {
5841
- throw new SyncStateError("Failed to get state branch name");
5842
- }
5843
- const violations = [];
5844
- try {
5845
- const branchExists = await this.git.branchExists(stateBranch);
5846
- if (!branchExists) {
5847
- return violations;
5848
- }
5849
- let commits;
5850
- try {
5851
- commits = await this.git.getCommitHistory(stateBranch, {
5852
- maxCount: 1e3
5853
- // Analyze last 1000 commits
5854
- });
5855
- } catch (error) {
5856
- return violations;
5857
- }
5858
- for (let i = 0; i < commits.length; i++) {
5859
- const commit = commits[i];
5860
- if (!commit) continue;
5861
- const message = commit.message.toLowerCase();
5862
- if (message.startsWith("resolution:")) {
5863
- continue;
5864
- }
5865
- if (message.startsWith("sync:")) {
5866
- continue;
5867
- }
5868
- const isExplicitRebaseCommit = message.includes("rebase") || message.includes("pick ");
5869
- if (isExplicitRebaseCommit) {
5870
- const nextCommit = commits[i + 1];
5871
- const isResolutionNext = nextCommit && nextCommit.message.toLowerCase().startsWith("resolution:");
5872
- if (!isResolutionNext) {
5873
- violations.push({
5874
- rebaseCommitHash: commit.hash,
5875
- commitMessage: commit.message,
5876
- timestamp: commit.date,
5877
- author: commit.author
5878
- });
5879
- }
5880
- }
5881
- }
5882
- return violations;
5883
- } catch (error) {
5884
- return violations;
5885
- }
5886
- }
5887
- /**
5888
- * Complete audit of gitgov-state status.
5889
- * Verifies integrity of resolutions, signatures in Records, checksums and expected files.
5890
- *
5891
- * [EARS-E4, EARS-E5, EARS-E6, EARS-E7]
5892
- */
5893
- async auditState(options = {}) {
5894
- const scope = options.scope ?? "all";
5895
- const verifySignatures2 = options.verifySignatures ?? true;
5896
- const verifyChecksums = options.verifyChecksums ?? true;
5897
- const report = {
5898
- passed: true,
5899
- scope,
5900
- totalCommits: 0,
5901
- rebaseCommits: 0,
5902
- resolutionCommits: 0,
5903
- integrityViolations: [],
5904
- summary: ""
5905
- };
5906
- try {
5907
- const integrityViolations = await this.verifyResolutionIntegrity();
5908
- report.integrityViolations = integrityViolations;
5909
- if (integrityViolations.length > 0) {
5910
- report.passed = false;
5911
- }
5912
- const stateBranch = await this.getStateBranchName();
5913
- const branchExists = await this.git.branchExists(stateBranch);
5914
- if (branchExists) {
5915
- try {
5916
- const commits = await this.git.getCommitHistory(stateBranch, {
5917
- maxCount: 1e3
5918
- });
5919
- report.totalCommits = commits.length;
5920
- report.rebaseCommits = commits.filter(
5921
- (c) => c.message.toLowerCase().includes("rebase")
5922
- ).length;
5923
- report.resolutionCommits = commits.filter(
5924
- (c) => c.message.toLowerCase().startsWith("resolution:")
5925
- ).length;
5926
- } catch {
5927
- report.totalCommits = 0;
5928
- report.rebaseCommits = 0;
5929
- report.resolutionCommits = 0;
5930
- }
5931
- }
5932
- if (verifySignatures2 || verifyChecksums) {
5933
- const lintReport = await this.lint.lint({
5934
- validateChecksums: verifyChecksums,
5935
- validateSignatures: verifySignatures2,
5936
- validateReferences: false,
5937
- concurrent: true
5938
- });
5939
- report.lintReport = lintReport;
5940
- if (lintReport.summary.errors > 0) {
5941
- report.passed = false;
5942
- }
5943
- }
5944
- const lintErrorCount = report.lintReport?.summary.errors || 0;
5945
- const violationCount = report.integrityViolations.length + lintErrorCount;
5946
- report.summary = report.passed ? `Audit passed. No violations found (scope: ${scope}).` : `Audit failed. Found ${violationCount} violation(s): ${report.integrityViolations.length} integrity + ${lintErrorCount} structural (scope: ${scope}).`;
5947
- return report;
5948
- } catch (error) {
5949
- throw new SyncStateError(
5950
- `Failed to audit state: ${error.message}`
5951
- );
5952
- }
5953
- }
5954
- /**
5955
- * Publishes local state changes to gitgov-state.
5956
- * Implements 3 phases: verification, reconciliation, publication.
5957
- *
5958
- * [EARS-B1 through EARS-B7]
5959
- */
5960
- async pushState(options) {
5961
- const { actorId, dryRun = false } = options;
5962
- const stateBranch = await this.getStateBranchName();
5963
- if (!stateBranch) {
5964
- throw new SyncStateError("Failed to get state branch name");
5965
- }
5966
- let sourceBranch = options.sourceBranch;
5967
- const log = (msg) => logger6.debug(`[pushState] ${msg}`);
5968
- const result = {
5969
- success: false,
5970
- filesSynced: 0,
5971
- sourceBranch: "",
5972
- commitHash: null,
5973
- commitMessage: null,
5974
- conflictDetected: false
5975
- };
5976
- let stashHash = null;
5977
- let savedBranch = sourceBranch || "";
5978
- try {
5979
- log("=== STARTING pushState ===");
5980
- if (!sourceBranch) {
5981
- sourceBranch = await this.git.getCurrentBranch();
5982
- log(`Got current branch: ${sourceBranch}`);
5983
- }
5984
- result.sourceBranch = sourceBranch;
5985
- if (sourceBranch === stateBranch) {
5986
- log(`ERROR: Attempting to push from state branch ${stateBranch}`);
5987
- throw new PushFromStateBranchError(stateBranch);
5988
- }
5989
- log(`Pre-check passed: pushing from ${sourceBranch} to ${stateBranch}`);
5990
- const currentActor = await this.identity.getCurrentActor();
5991
- if (currentActor.id !== actorId) {
5992
- log(`ERROR: Actor identity mismatch: requested '${actorId}' but authenticated as '${currentActor.id}'`);
5993
- throw new ActorIdentityMismatchError(actorId, currentActor.id);
5994
- }
5995
- log(`Pre-check passed: actorId '${actorId}' matches authenticated identity`);
5996
- const remoteName = "origin";
5997
- const hasRemote = await this.git.isRemoteConfigured(remoteName);
5998
- if (!hasRemote) {
5999
- log(`ERROR: No remote '${remoteName}' configured`);
6000
- throw new SyncStateError(
6001
- `No remote repository configured. State sync requires a remote for multi-machine collaboration.
6002
- Add a remote with: git remote add origin <url>
6003
- Then push your changes: git push -u origin ${sourceBranch}`
6004
- );
6005
- }
6006
- log(`Pre-check passed: remote '${remoteName}' configured`);
6007
- const hasCommits = await this.git.branchExists(sourceBranch);
6008
- if (!hasCommits) {
6009
- log(`ERROR: Branch '${sourceBranch}' has no commits`);
6010
- throw new SyncStateError(
6011
- `Cannot sync: branch '${sourceBranch}' has no commits. Please create an initial commit first (e.g., 'git commit --allow-empty -m "Initial commit"').`
6012
- );
6013
- }
6014
- log(`Pre-check passed: branch '${sourceBranch}' has commits`);
6015
- log("Phase 0: Starting audit...");
6016
- const auditReport = await this.auditState({ scope: "current" });
6017
- log(`Audit result: ${auditReport.passed ? "PASSED" : "FAILED"}`);
6018
- if (!auditReport.passed) {
6019
- log(`Audit violations: ${auditReport.summary}`);
6020
- const affectedFiles = [];
6021
- const detailedErrors = [];
6022
- if (auditReport.lintReport?.results) {
6023
- for (const r of auditReport.lintReport.results) {
6024
- if (r.level === "error") {
6025
- if (!affectedFiles.includes(r.filePath)) {
6026
- affectedFiles.push(r.filePath);
6027
- }
6028
- detailedErrors.push(` \u2022 ${r.filePath}: [${r.validator}] ${r.message}`);
6029
- }
6030
- }
6031
- }
6032
- for (const v of auditReport.integrityViolations) {
6033
- detailedErrors.push(` \u2022 Commit ${v.rebaseCommitHash.slice(0, 8)}: ${v.commitMessage} (by ${v.author})`);
6034
- }
6035
- const detailSection = detailedErrors.length > 0 ? `
6036
-
6037
- Details:
6038
- ${detailedErrors.join("\n")}` : "";
6039
- result.conflictDetected = true;
6040
- result.conflictInfo = {
6041
- type: "integrity_violation",
6042
- affectedFiles,
6043
- message: auditReport.summary + detailSection,
6044
- resolutionSteps: [
6045
- "Run 'gitgov lint --fix' to auto-fix signature/checksum issues",
6046
- "If issues persist, manually review the affected files",
6047
- "Then retry: gitgov sync push"
6048
- ]
6049
- };
6050
- result.error = "Integrity violations detected. Cannot push.";
6051
- return result;
6052
- }
6053
- log("Ensuring state branch exists...");
6054
- await this.ensureStateBranch();
6055
- log("State branch confirmed");
6056
- log("=== Phase 1: Reconciliation ===");
6057
- savedBranch = sourceBranch;
6058
- log(`Saved branch: ${savedBranch}`);
6059
- const isCurrentBranch = sourceBranch === await this.git.getCurrentBranch();
6060
- let hasUntrackedGitgovFiles = false;
6061
- let tempDir = null;
6062
- if (isCurrentBranch) {
6063
- const repoRoot2 = await this.git.getRepoRoot();
6064
- const gitgovPath = path9__default.join(repoRoot2, ".gitgov");
6065
- hasUntrackedGitgovFiles = existsSync(gitgovPath);
6066
- log(`[EARS-B10] .gitgov/ exists on filesystem: ${hasUntrackedGitgovFiles}`);
6067
- if (hasUntrackedGitgovFiles) {
6068
- log("[EARS-B10] Copying ENTIRE .gitgov/ to temp directory for preservation...");
6069
- tempDir = await promises.mkdtemp(path9__default.join(os.tmpdir(), "gitgov-sync-"));
6070
- log(`[EARS-B10] Created temp directory: ${tempDir}`);
6071
- await promises.cp(gitgovPath, tempDir, { recursive: true });
6072
- log("[EARS-B10] Entire .gitgov/ copied to temp");
6073
- }
6074
- }
6075
- log("[EARS-B10] Checking for uncommitted changes before checkout...");
6076
- const hasUncommittedBeforeCheckout = await this.git.hasUncommittedChanges();
6077
- if (hasUncommittedBeforeCheckout) {
6078
- log("[EARS-B10] Uncommitted changes detected, stashing before checkout...");
6079
- stashHash = await this.git.stash("gitgov-sync-temp-stash");
6080
- log(`[EARS-B10] Changes stashed: ${stashHash || "none"}`);
6081
- }
6082
- const restoreStashAndReturn = async (returnResult) => {
6083
- await this.git.checkoutBranch(savedBranch);
6084
- if (tempDir) {
6085
- try {
6086
- const repoRoot2 = await this.git.getRepoRoot();
6087
- const gitgovDir = path9__default.join(repoRoot2, ".gitgov");
6088
- if (returnResult.implicitPull?.hasChanges) {
6089
- log("[EARS-B21] Implicit pull detected in early return - preserving new files from gitgov-state...");
6090
- try {
6091
- await this.git.checkoutFilesFromBranch(stateBranch, [".gitgov/"]);
6092
- await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot2 });
6093
- log("[EARS-B21] Synced files copied from gitgov-state (unstaged)");
6094
- } catch (checkoutError) {
6095
- log(`[EARS-B21] Warning: Failed to checkout from gitgov-state: ${checkoutError}`);
6096
- }
6097
- for (const fileName of LOCAL_ONLY_FILES) {
6098
- const tempFilePath = path9__default.join(tempDir, fileName);
6099
- const destFilePath = path9__default.join(gitgovDir, fileName);
6100
- try {
6101
- await promises.access(tempFilePath);
6102
- await promises.cp(tempFilePath, destFilePath, { force: true });
6103
- log(`[EARS-B21] Restored LOCAL_ONLY_FILE: ${fileName}`);
6104
- } catch {
6105
- }
6106
- }
6107
- const restoreExcludedFilesEarly = async (dir, destDir) => {
6108
- try {
6109
- const entries = await promises.readdir(dir, { withFileTypes: true });
6110
- for (const entry of entries) {
6111
- const srcPath = path9__default.join(dir, entry.name);
6112
- const dstPath = path9__default.join(destDir, entry.name);
6113
- if (entry.isDirectory()) {
6114
- await restoreExcludedFilesEarly(srcPath, dstPath);
6115
- } else {
6116
- const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
6117
- if (isExcluded) {
6118
- await promises.mkdir(path9__default.dirname(dstPath), { recursive: true });
6119
- await promises.copyFile(srcPath, dstPath);
6120
- log(`[EARS-B22] Restored excluded file (early return): ${entry.name}`);
6121
- }
6122
- }
6123
- }
6124
- } catch {
6125
- }
6126
- };
6127
- await restoreExcludedFilesEarly(tempDir, gitgovDir);
6128
- } else {
6129
- log("[EARS-B14] Restoring .gitgov/ from temp directory (early return)...");
6130
- await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
6131
- log("[EARS-B14] .gitgov/ restored from temp (early return)");
6132
- }
6133
- await promises.rm(tempDir, { recursive: true, force: true });
6134
- log("[EARS-B14] Temp directory cleaned up (early return)");
6135
- } catch (tempRestoreError) {
6136
- log(`[EARS-B14] WARNING: Failed to restore tempDir: ${tempRestoreError}`);
6137
- returnResult.error = returnResult.error ? `${returnResult.error}. Failed to restore .gitgov/ from temp.` : `Failed to restore .gitgov/ from temp. Check /tmp for gitgov-sync-* directory.`;
6138
- }
6139
- }
6140
- if (stashHash) {
6141
- try {
6142
- await this.git.stashPop();
6143
- log("[EARS-B10] Stashed changes restored");
6144
- } catch (stashError) {
6145
- log(`[EARS-B10] Failed to restore stash: ${stashError}`);
6146
- returnResult.error = returnResult.error ? `${returnResult.error}. Failed to restore stashed changes.` : "Failed to restore stashed changes. Run 'git stash pop' manually.";
6147
- }
6148
- }
6149
- if (returnResult.implicitPull?.hasChanges) {
6150
- log("[EARS-B21] Regenerating index after implicit pull (early return)...");
6151
- try {
6152
- await this.indexer.generateIndex();
6153
- returnResult.implicitPull.reindexed = true;
6154
- log("[EARS-B21] Index regenerated successfully");
6155
- } catch (indexError) {
6156
- log(`[EARS-B21] Warning: Failed to regenerate index: ${indexError}`);
6157
- returnResult.implicitPull.reindexed = false;
6158
- }
6159
- }
6160
- return returnResult;
6161
- };
6162
- log(`Checking out to ${stateBranch}...`);
6163
- await this.git.checkoutBranch(stateBranch);
6164
- log(`Now on branch: ${await this.git.getCurrentBranch()}`);
6165
- let filesBeforeChanges = /* @__PURE__ */ new Set();
6166
- try {
6167
- const repoRoot2 = await this.git.getRepoRoot();
6168
- const { stdout: filesOutput } = await execAsync(
6169
- `git ls-files ".gitgov" 2>/dev/null || true`,
6170
- { cwd: repoRoot2 }
6171
- );
6172
- filesBeforeChanges = new Set(filesOutput.trim().split("\n").filter((f) => f && shouldSyncFile(f)));
6173
- log(`[EARS-B19] Files in gitgov-state before changes: ${filesBeforeChanges.size}`);
6174
- } catch {
6175
- }
6176
- log("=== Phase 2: Publication ===");
6177
- log("Checking if .gitgov/ exists in gitgov-state...");
6178
- let isFirstPush = false;
6179
- try {
6180
- const repoRoot2 = await this.git.getRepoRoot();
6181
- const { stdout } = await execAsync(
6182
- `git ls-tree -d ${stateBranch} .gitgov`,
6183
- { cwd: repoRoot2 }
6184
- );
6185
- isFirstPush = !stdout.trim();
6186
- log(`First push detected: ${isFirstPush}`);
6187
- } catch (error) {
6188
- log("Error checking .gitgov/ existence, assuming first push");
6189
- isFirstPush = true;
6190
- }
6191
- let delta = [];
6192
- if (!isFirstPush) {
6193
- log("Calculating state delta...");
6194
- delta = await this.calculateStateDelta(sourceBranch);
6195
- log(`Delta: ${delta.length} file(s) changed`);
6196
- if (delta.length === 0) {
6197
- log("No changes detected, returning without commit");
6198
- result.success = true;
6199
- result.filesSynced = 0;
6200
- return await restoreStashAndReturn(result);
6201
- }
6202
- } else {
6203
- log("First push: will copy all whitelisted files");
6204
- }
6205
- log(`Copying syncable .gitgov/ files from ${sourceBranch}...`);
6206
- log(`Sync directories: ${SYNC_DIRECTORIES.join(", ")}`);
6207
- log(`Sync root files: ${SYNC_ROOT_FILES.join(", ")}`);
6208
- const repoRoot = await this.git.getRepoRoot();
6209
- if (tempDir) {
6210
- log("[EARS-B10] Copying ONLY syncable files from temp directory...");
6211
- const gitgovDir = path9__default.join(repoRoot, ".gitgov");
6212
- await promises.mkdir(gitgovDir, { recursive: true });
6213
- log(`[EARS-B10] Ensured .gitgov/ directory exists: ${gitgovDir}`);
6214
- const copiedCount = await copySyncableFiles(tempDir, gitgovDir, log);
6215
- log(`[EARS-B10] Syncable files copy complete: ${copiedCount} files copied`);
6216
- } else {
6217
- log("Copying syncable files from git...");
6218
- const existingPaths = [];
6219
- for (const dirName of SYNC_DIRECTORIES) {
6220
- const fullPath = `.gitgov/${dirName}`;
6221
- try {
6222
- const { stdout } = await execAsync(
6223
- `git ls-tree -r ${sourceBranch} -- ${fullPath}`,
6224
- { cwd: repoRoot }
6225
- );
6226
- const lines = stdout.trim().split("\n").filter((l) => l);
6227
- for (const line of lines) {
6228
- const parts = line.split(" ");
6229
- const filePath = parts[1];
6230
- if (filePath && shouldSyncFile(filePath)) {
6231
- existingPaths.push(filePath);
6232
- } else if (filePath) {
6233
- log(`Skipped (not syncable): ${filePath}`);
6234
- }
6235
- }
6236
- } catch {
6237
- log(`Directory ${dirName} does not exist in ${sourceBranch}, skipping`);
6238
- }
6239
- }
6240
- for (const fileName of SYNC_ROOT_FILES) {
6241
- const fullPath = `.gitgov/${fileName}`;
6242
- try {
6243
- const { stdout } = await execAsync(
6244
- `git ls-tree ${sourceBranch} -- ${fullPath}`,
6245
- { cwd: repoRoot }
6246
- );
6247
- if (stdout.trim()) {
6248
- existingPaths.push(fullPath);
6249
- }
6250
- } catch {
6251
- log(`File ${fileName} does not exist in ${sourceBranch}, skipping`);
6252
- }
6253
- }
6254
- log(`Syncable paths found: ${existingPaths.length}`);
6255
- if (existingPaths.length === 0) {
6256
- log("No syncable files to sync, aborting");
6257
- result.success = true;
6258
- result.filesSynced = 0;
6259
- return await restoreStashAndReturn(result);
6260
- }
6261
- await this.git.checkoutFilesFromBranch(sourceBranch, existingPaths);
6262
- log("Syncable files checked out successfully");
6263
- }
6264
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
6265
- if (isFirstPush) {
6266
- await this.git.add([".gitgov"], { force: true });
6267
- const repoRoot2 = await this.git.getRepoRoot();
6268
- try {
6269
- const { stdout } = await execAsync(
6270
- "git diff --cached --name-status",
6271
- { cwd: repoRoot2 }
6272
- );
6273
- const lines = stdout.trim().split("\n").filter((l) => l);
6274
- delta = lines.map((line) => {
6275
- const [status, file] = line.split(" ");
6276
- if (!file) return null;
6277
- return {
6278
- status,
6279
- file
6280
- };
6281
- }).filter((item) => item !== null);
6282
- log(`First push delta calculated: ${delta.length} file(s)`);
6283
- } catch (error) {
6284
- log("Error calculating first push delta, using empty delta");
6285
- delta = [];
6286
- }
6287
- }
6288
- const commitMessage = `sync: ${isFirstPush ? "Initial state" : "Publish state"} from ${sourceBranch}
6289
-
6290
- Actor: ${actorId}
6291
- Timestamp: ${timestamp}
6292
- Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
6293
-
6294
- ` + delta.map((d) => `${d.status} ${d.file}`).join("\n");
6295
- result.commitMessage = commitMessage;
6296
- if (!dryRun) {
6297
- if (!isFirstPush) {
6298
- log("Staging all .gitgov/ files before cleanup...");
6299
- await this.git.add([".gitgov"], { force: true });
6300
- log("All files staged, proceeding to cleanup");
6301
- }
6302
- log("[EARS-B14] Scanning for non-syncable files in gitgov-state...");
6303
- try {
6304
- const { stdout: trackedFiles } = await execAsync(
6305
- `git ls-files ".gitgov" 2>/dev/null || true`,
6306
- { cwd: repoRoot }
6307
- );
6308
- const allTrackedFiles = trackedFiles.trim().split("\n").filter((f) => f);
6309
- log(`[EARS-B14] Found ${allTrackedFiles.length} staged/tracked files in .gitgov/`);
6310
- for (const trackedFile of allTrackedFiles) {
6311
- if (!shouldSyncFile(trackedFile)) {
6312
- try {
6313
- await execAsync(`git rm -f "${trackedFile}"`, { cwd: repoRoot });
6314
- log(`[EARS-B14] Removed non-syncable file: ${trackedFile}`);
6315
- } catch {
6316
- }
6317
- }
6318
- }
6319
- } catch {
6320
- }
6321
- log("[EARS-B14] Non-syncable files cleanup complete");
6322
- log("[EARS-B19] Checking for deleted files to sync...");
6323
- try {
6324
- const sourceFiles = /* @__PURE__ */ new Set();
6325
- const findSourceFiles = async (dir, prefix = ".gitgov") => {
6326
- try {
6327
- const entries = await promises.readdir(dir, { withFileTypes: true });
6328
- for (const entry of entries) {
6329
- const fullPath = path9__default.join(dir, entry.name);
6330
- const relativePath = `${prefix}/${entry.name}`;
6331
- if (entry.isDirectory()) {
6332
- await findSourceFiles(fullPath, relativePath);
6333
- } else if (shouldSyncFile(relativePath)) {
6334
- sourceFiles.add(relativePath);
6335
- }
6336
- }
6337
- } catch {
6338
- }
6339
- };
6340
- const sourceDir = tempDir || path9__default.join(repoRoot, ".gitgov");
6341
- await findSourceFiles(sourceDir);
6342
- log(`[EARS-B19] Found ${sourceFiles.size} syncable files in source (user's local state)`);
6343
- log(`[EARS-B19] Files that existed before changes: ${filesBeforeChanges.size}`);
6344
- let deletedCount = 0;
6345
- for (const fileBeforeChange of filesBeforeChanges) {
6346
- if (!sourceFiles.has(fileBeforeChange)) {
6347
- try {
6348
- await execAsync(`git rm -f "${fileBeforeChange}"`, { cwd: repoRoot });
6349
- log(`[EARS-B19] Deleted (user removed): ${fileBeforeChange}`);
6350
- deletedCount++;
6351
- } catch {
6352
- }
6353
- }
6354
- }
6355
- log(`[EARS-B19] Deleted ${deletedCount} files that user removed locally`);
6356
- } catch (err) {
6357
- log(`[EARS-B19] Warning: Failed to sync deleted files: ${err}`);
6358
- }
6359
- const hasStaged = await this.git.hasUncommittedChanges();
6360
- log(`Has staged changes: ${hasStaged}`);
6361
- if (!hasStaged) {
6362
- log("No staged changes detected, returning without commit");
6363
- result.success = true;
6364
- result.filesSynced = 0;
6365
- return await restoreStashAndReturn(result);
6366
- }
6367
- log("Creating local commit...");
6368
- try {
6369
- const commitHash = await this.git.commit(commitMessage);
6370
- log(`Local commit created: ${commitHash}`);
6371
- result.commitHash = commitHash;
6372
- } catch (commitError) {
6373
- const errorMsg = commitError instanceof Error ? commitError.message : String(commitError);
6374
- const stdout = commitError.stdout || "";
6375
- const stderr = commitError.stderr || "";
6376
- log(`Commit attempt output - stdout: ${stdout}, stderr: ${stderr}`);
6377
- const isNothingToCommit = stdout.includes("nothing to commit") || stderr.includes("nothing to commit") || stdout.includes("nothing added to commit") || stderr.includes("nothing added to commit");
6378
- if (isNothingToCommit) {
6379
- log("Nothing to commit - files are identical to gitgov-state HEAD");
6380
- result.success = true;
6381
- result.filesSynced = 0;
6382
- return await restoreStashAndReturn(result);
6383
- }
6384
- log(`ERROR: Commit failed: ${errorMsg}`);
6385
- log(`ERROR: Git stderr: ${stderr}`);
6386
- throw new Error(`Failed to create commit: ${errorMsg} | stderr: ${stderr}`);
6387
- }
6388
- log("=== Phase 3: Reconcile with Remote (Git-Native) ===");
6389
- let hashBeforePull = null;
6390
- try {
6391
- const { stdout: beforeHash } = await execAsync(`git rev-parse HEAD`, { cwd: repoRoot });
6392
- hashBeforePull = beforeHash.trim();
6393
- log(`Hash before pull: ${hashBeforePull}`);
6394
- } catch {
6395
- }
6396
- log("Attempting git pull --rebase origin gitgov-state...");
6397
- try {
6398
- await this.git.pullRebase("origin", stateBranch);
6399
- log("Pull rebase successful - no conflicts");
6400
- if (hashBeforePull) {
6401
- try {
6402
- const { stdout: afterHash } = await execAsync(`git rev-parse HEAD`, { cwd: repoRoot });
6403
- const hashAfterPull = afterHash.trim();
6404
- if (hashAfterPull !== hashBeforePull) {
6405
- const pulledChangedFiles = await this.git.getChangedFiles(hashBeforePull, hashAfterPull, ".gitgov/");
6406
- result.implicitPull = {
6407
- hasChanges: true,
6408
- filesUpdated: pulledChangedFiles.length,
6409
- reindexed: false
6410
- // Will be set to true after actual reindex
6411
- };
6412
- log(`[EARS-B16] Implicit pull: ${pulledChangedFiles.length} files from remote were rebased`);
6413
- }
6414
- } catch (e) {
6415
- log(`[EARS-B16] Could not capture implicit pull details: ${e}`);
6416
- }
6417
- }
6418
- } catch (pullError) {
6419
- const errorMsg = pullError instanceof Error ? pullError.message : String(pullError);
6420
- log(`Pull rebase result: ${errorMsg}`);
6421
- const isAlreadyUpToDate = errorMsg.includes("up to date") || errorMsg.includes("up-to-date");
6422
- const isNoRemote = errorMsg.includes("does not appear to be") || errorMsg.includes("Could not read from remote");
6423
- const isNoUpstream = errorMsg.includes("no tracking information") || errorMsg.includes("There is no tracking information");
6424
- if (isAlreadyUpToDate || isNoRemote || isNoUpstream) {
6425
- log("Pull not needed or no remote - continuing to push");
6426
- } else {
6427
- const isRebaseInProgress = await this.isRebaseInProgress();
6428
- const conflictedFiles = await this.git.getConflictedFiles();
6429
- if (isRebaseInProgress || conflictedFiles.length > 0) {
6430
- log(`[GIT-NATIVE] Conflict detected! Files: ${conflictedFiles.join(", ")}`);
6431
- result.conflictDetected = true;
6432
- const fileWord = conflictedFiles.length === 1 ? "file" : "files";
6433
- const stageCommand = conflictedFiles.length === 1 ? `git add ${conflictedFiles[0]}` : "git add .gitgov/";
6434
- result.conflictInfo = {
6435
- type: "rebase_conflict",
6436
- affectedFiles: conflictedFiles,
6437
- message: "Conflict detected during sync - Git has paused the rebase for manual resolution",
6438
- resolutionSteps: [
6439
- `1. Edit the conflicted ${fileWord} to resolve conflicts (remove <<<<<<, ======, >>>>>> markers)`,
6440
- `2. Stage resolved ${fileWord}: ${stageCommand}`,
6441
- "3. Complete sync: gitgov sync resolve --reason 'your reason'",
6442
- "(This will continue the rebase, re-sign the record, and return you to your original branch)"
6443
- ]
6444
- };
6445
- result.error = `Conflict detected: ${conflictedFiles.length} file(s) need manual resolution. Use 'git status' to see details.`;
6446
- if (stashHash) {
6447
- try {
6448
- await this.git.checkoutBranch(sourceBranch);
6449
- await execAsync("git stash pop", { cwd: repoRoot });
6450
- await this.git.checkoutBranch(stateBranch);
6451
- log("Restored stash to original branch during conflict");
6452
- } catch (stashErr) {
6453
- log(`Warning: Could not restore stash: ${stashErr}`);
6454
- }
6455
- }
6456
- if (tempDir) {
6457
- log("Restoring local files (.key, .session.json, etc.) for conflict resolution...");
6458
- const gitgovInState = path9__default.join(repoRoot, ".gitgov");
6459
- for (const fileName of LOCAL_ONLY_FILES) {
6460
- const srcPath = path9__default.join(tempDir, fileName);
6461
- const destPath = path9__default.join(gitgovInState, fileName);
6462
- try {
6463
- await promises.access(srcPath);
6464
- await promises.cp(srcPath, destPath, { force: true });
6465
- log(`Restored LOCAL_ONLY_FILE for conflict resolution: ${fileName}`);
6466
- } catch {
6467
- }
6468
- }
6469
- const restoreExcluded = async (srcDir, destDir) => {
6470
- try {
6471
- const entries = await promises.readdir(srcDir, { withFileTypes: true });
6472
- for (const entry of entries) {
6473
- const srcPath = path9__default.join(srcDir, entry.name);
6474
- const dstPath = path9__default.join(destDir, entry.name);
6475
- if (entry.isDirectory()) {
6476
- await restoreExcluded(srcPath, dstPath);
6477
- } else {
6478
- const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
6479
- if (isExcluded) {
6480
- await promises.mkdir(path9__default.dirname(dstPath), { recursive: true });
6481
- await promises.copyFile(srcPath, dstPath);
6482
- log(`Restored EXCLUDED file for conflict resolution: ${entry.name}`);
6483
- }
6484
- }
6485
- }
6486
- } catch {
6487
- }
6488
- };
6489
- await restoreExcluded(tempDir, gitgovInState);
6490
- log("Local files restored for conflict resolution");
6491
- }
6492
- return result;
6493
- }
6494
- throw pullError;
6495
- }
6496
- }
6497
- log("=== Phase 4: Push to Remote ===");
6498
- log("Pushing to remote...");
6499
- try {
6500
- await this.git.push("origin", stateBranch);
6501
- log("Push successful");
6502
- } catch (pushError) {
6503
- const pushErrorMsg = pushError instanceof Error ? pushError.message : String(pushError);
6504
- log(`Push failed: ${pushErrorMsg}`);
6505
- const isNoRemote = pushErrorMsg.includes("does not appear to be") || pushErrorMsg.includes("Could not read from remote");
6506
- if (!isNoRemote) {
6507
- log("ERROR: Push failed with non-remote error");
6508
- throw pushError;
6509
- }
6510
- log("Push failed due to no remote, continuing (local commit succeeded)");
6511
- }
6512
- }
6513
- log(`Returning to ${savedBranch}...`);
6514
- await this.git.checkoutBranch(savedBranch);
6515
- log(`Back on ${await this.git.getCurrentBranch()}`);
6516
- if (stashHash) {
6517
- log("[EARS-B10] Restoring stashed changes...");
6518
- try {
6519
- await this.git.stashPop();
6520
- log("[EARS-B10] Stashed changes restored successfully");
6521
- } catch (stashError) {
6522
- log(`[EARS-B10] Warning: Failed to restore stashed changes: ${stashError}`);
6523
- result.error = `Push succeeded but failed to restore stashed changes. Run 'git stash pop' manually. Error: ${stashError}`;
6524
- }
6525
- }
6526
- if (tempDir) {
6527
- const repoRoot2 = await this.git.getRepoRoot();
6528
- const gitgovDir = path9__default.join(repoRoot2, ".gitgov");
6529
- if (result.implicitPull?.hasChanges) {
6530
- log("[EARS-B18] Implicit pull detected - copying synced files from gitgov-state first...");
6531
- try {
6532
- await this.git.checkoutFilesFromBranch(stateBranch, [".gitgov/"]);
6533
- await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot2 });
6534
- log("[EARS-B18] Synced files copied from gitgov-state to work branch (unstaged)");
6535
- } catch (checkoutError) {
6536
- log(`[EARS-B18] Warning: Failed to checkout from gitgov-state: ${checkoutError}`);
6537
- await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
6538
- log("[EARS-B18] Fallback: Entire .gitgov/ restored from temp");
6539
- }
6540
- log("[EARS-B18] Restoring local-only files from temp directory...");
6541
- for (const fileName of LOCAL_ONLY_FILES) {
6542
- const tempFilePath = path9__default.join(tempDir, fileName);
6543
- const destFilePath = path9__default.join(gitgovDir, fileName);
6544
- try {
6545
- await promises.access(tempFilePath);
6546
- await promises.cp(tempFilePath, destFilePath, { force: true });
6547
- log(`[EARS-B18] Restored LOCAL_ONLY_FILE: ${fileName}`);
6548
- } catch {
6549
- }
6550
- }
6551
- const restoreExcludedFiles = async (dir, destDir) => {
6552
- try {
6553
- const entries = await promises.readdir(dir, { withFileTypes: true });
6554
- for (const entry of entries) {
6555
- const srcPath = path9__default.join(dir, entry.name);
6556
- const dstPath = path9__default.join(destDir, entry.name);
6557
- if (entry.isDirectory()) {
6558
- await restoreExcludedFiles(srcPath, dstPath);
6559
- } else {
6560
- const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
6561
- if (isExcluded) {
6562
- await promises.mkdir(path9__default.dirname(dstPath), { recursive: true });
6563
- await promises.copyFile(srcPath, dstPath);
6564
- log(`[EARS-B22] Restored excluded file: ${entry.name}`);
6565
- }
6566
- }
6567
- }
6568
- } catch {
6569
- }
6570
- };
6571
- await restoreExcludedFiles(tempDir, gitgovDir);
6572
- log("[EARS-B22] Local-only and excluded files restored from temp");
6573
- } else {
6574
- log("[EARS-B10] Restoring ENTIRE .gitgov/ from temp directory to working tree...");
6575
- await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
6576
- log("[EARS-B10] Entire .gitgov/ restored from temp");
6577
- }
6578
- log("[EARS-B10] Cleaning up temp directory...");
6579
- try {
6580
- await promises.rm(tempDir, { recursive: true, force: true });
6581
- log("[EARS-B10] Temp directory cleaned up");
6582
- } catch (cleanupError) {
6583
- log(`[EARS-B10] Warning: Failed to cleanup temp directory: ${cleanupError}`);
6584
- }
6585
- }
6586
- if (result.implicitPull?.hasChanges) {
6587
- log("[EARS-B16] Regenerating index after implicit pull...");
6588
- try {
6589
- await this.indexer.generateIndex();
6590
- result.implicitPull.reindexed = true;
6591
- log("[EARS-B16] Index regenerated successfully after implicit pull");
6592
- } catch (indexError) {
6593
- log(`[EARS-B16] Warning: Failed to regenerate index after implicit pull: ${indexError}`);
6594
- result.implicitPull.reindexed = false;
6595
- }
6596
- }
6597
- result.success = true;
6598
- result.filesSynced = delta.length;
6599
- log(`=== pushState COMPLETED SUCCESSFULLY: ${delta.length} files synced ===`);
6600
- return result;
6601
- } catch (error) {
6602
- log(`=== pushState FAILED: ${error.message} ===`);
6603
- try {
6604
- const currentBranch = await this.git.getCurrentBranch();
6605
- if (currentBranch !== savedBranch && savedBranch) {
6606
- log(`[EARS-B10] Restoring original branch: ${savedBranch}...`);
6607
- await this.git.checkoutBranch(savedBranch);
6608
- log(`[EARS-B10] Restored to ${savedBranch}`);
6609
- }
6610
- } catch (branchError) {
6611
- log(`[EARS-B10] Failed to restore original branch: ${branchError}`);
6612
- }
6613
- if (stashHash) {
6614
- log("[EARS-B10] Attempting to restore stashed changes after error...");
6615
- try {
6616
- await this.git.stashPop();
6617
- log("[EARS-B10] Stashed changes restored after error");
6618
- } catch (stashError) {
6619
- log(`[EARS-B10] Failed to restore stashed changes after error: ${stashError}`);
6620
- const originalError = error.message;
6621
- error.message = `${originalError}. Additionally, failed to restore stashed changes. Run 'git stash pop' manually.`;
6622
- }
6623
- }
6624
- if (error instanceof PushFromStateBranchError || error instanceof UncommittedChangesError) {
6625
- throw error;
6626
- }
6627
- result.error = error.message;
6628
- return result;
6629
- }
6630
- }
6631
- /**
6632
- * Pulls remote changes from gitgov-state to the local environment.
6633
- * Includes automatic re-indexing if there are new changes.
6634
- *
6635
- * [EARS-C1 through EARS-C4]
6636
- * [EARS-C5] Requires remote to be configured (pull without remote makes no sense)
6637
- */
6638
- async pullState(options = {}) {
6639
- const { forceReindex = false, force = false } = options;
6640
- const stateBranch = await this.getStateBranchName();
6641
- if (!stateBranch) {
6642
- throw new SyncStateError("Failed to get state branch name");
6643
- }
6644
- const log = (msg) => logger6.debug(`[pullState] ${msg}`);
6645
- const result = {
6646
- success: false,
6647
- hasChanges: false,
6648
- filesUpdated: 0,
6649
- reindexed: false,
6650
- conflictDetected: false
6651
- };
6652
- try {
6653
- log("=== STARTING pullState ===");
6654
- log("Phase 0: Pre-flight checks...");
6655
- const remoteName = "origin";
6656
- const hasRemote = await this.git.isRemoteConfigured(remoteName);
6657
- if (!hasRemote) {
6658
- throw new SyncStateError(
6659
- `No remote '${remoteName}' configured. Pull requires a remote repository. Add a remote with: git remote add origin <url>`
6660
- );
6661
- }
6662
- await this.git.fetch(remoteName);
6663
- const remoteBranches = await this.git.listRemoteBranches(remoteName);
6664
- const existsRemote = remoteBranches.includes(stateBranch);
6665
- if (!existsRemote) {
6666
- const existsLocal = await this.git.branchExists(stateBranch);
6667
- if (!existsLocal) {
6668
- const repoRoot = await this.git.getRepoRoot();
6669
- const gitgovPath2 = path9__default.join(repoRoot, ".gitgov");
6670
- const gitgovExists2 = existsSync(gitgovPath2);
6671
- if (gitgovExists2) {
6672
- throw new SyncStateError(
6673
- `State branch '${stateBranch}' does not exist remotely yet. Run 'gitgov sync push' to publish your local state to the remote.`
6674
- );
6675
- } else {
6676
- throw new SyncStateError(
6677
- `State branch '${stateBranch}' does not exist locally or remotely. Run 'gitgov init' first to initialize GitGovernance, then 'gitgov sync push' to publish.`
6678
- );
6679
- }
6680
- }
6681
- result.success = true;
6682
- result.hasChanges = false;
6683
- result.filesUpdated = 0;
6684
- logger6.info(`[pullState] State branch exists locally but not remotely. Nothing to pull.`);
6685
- return result;
6686
- }
6687
- log("Pre-flight checks complete");
6688
- log("Phase 1: Setting up branches...");
6689
- const pullRepoRoot = await this.git.getRepoRoot();
6690
- await this.ensureStateBranch();
6691
- const savedBranch = await this.git.getCurrentBranch();
6692
- const savedLocalFiles = /* @__PURE__ */ new Map();
6693
- try {
6694
- for (const fileName of LOCAL_ONLY_FILES) {
6695
- const filePath = path9__default.join(pullRepoRoot, ".gitgov", fileName);
6696
- try {
6697
- const content = await promises.readFile(filePath, "utf-8");
6698
- savedLocalFiles.set(fileName, content);
6699
- log(`[EARS-C8] Saved local-only file: ${fileName}`);
6700
- } catch {
6701
- }
6702
- }
6703
- } catch (error) {
6704
- log(`[EARS-C8] Warning: Could not save local files: ${error.message}`);
6705
- }
6706
- const savedSyncableFiles = /* @__PURE__ */ new Map();
6707
- try {
6708
- const gitgovPath2 = path9__default.join(pullRepoRoot, ".gitgov");
6709
- const gitgovExists2 = await promises.access(gitgovPath2).then(() => true).catch(() => false);
6710
- if (gitgovExists2) {
6711
- const readSyncableFilesRecursive = async (dir, baseDir) => {
6712
- try {
6713
- const entries = await promises.readdir(dir, { withFileTypes: true });
6714
- for (const entry of entries) {
6715
- const fullPath = path9__default.join(dir, entry.name);
6716
- const relativePath = path9__default.relative(baseDir, fullPath);
6717
- const gitgovRelativePath = `.gitgov/${relativePath}`;
6718
- if (entry.isDirectory()) {
6719
- await readSyncableFilesRecursive(fullPath, baseDir);
6720
- } else if (shouldSyncFile(gitgovRelativePath)) {
6721
- try {
6722
- const content = await promises.readFile(fullPath, "utf-8");
6723
- savedSyncableFiles.set(gitgovRelativePath, content);
6724
- } catch {
6725
- }
6726
- }
6727
- }
6728
- } catch {
6729
- }
6730
- };
6731
- await readSyncableFilesRecursive(gitgovPath2, gitgovPath2);
6732
- log(`[EARS-C10] Saved ${savedSyncableFiles.size} syncable files before checkout for conflict detection`);
6733
- }
6734
- } catch (error) {
6735
- log(`[EARS-C10] Warning: Could not save syncable files: ${error.message}`);
6736
- }
6737
- try {
6738
- await this.git.checkoutBranch(stateBranch);
6739
- } catch (checkoutError) {
6740
- log(`[EARS-C8] Normal checkout failed, trying with force: ${checkoutError.message}`);
6741
- try {
6742
- await execAsync(`git checkout -f ${stateBranch}`, { cwd: pullRepoRoot });
6743
- log(`[EARS-C8] Force checkout successful`);
6744
- } catch (forceError) {
6745
- throw checkoutError;
6746
- }
6747
- }
6748
- try {
6749
- const { stdout } = await execAsync("git status --porcelain", { cwd: pullRepoRoot });
6750
- const lines = stdout.trim().split("\n").filter((l) => l);
6751
- const hasStagedOrModified = lines.some((line) => {
6752
- const status = line.substring(0, 2);
6753
- return status !== "??" && status.trim().length > 0;
6754
- });
6755
- if (hasStagedOrModified) {
6756
- await this.git.checkoutBranch(savedBranch);
6757
- throw new UncommittedChangesError(stateBranch);
6758
- }
6759
- } catch (error) {
6760
- if (error instanceof UncommittedChangesError) {
6761
- throw error;
6762
- }
6763
- }
6764
- log("Branch setup complete");
6765
- log("Phase 2: Pulling remote changes...");
6766
- const commitBefore = await this.git.getCommitHistory(stateBranch, {
6767
- maxCount: 1
6768
- });
6769
- const hashBefore = commitBefore[0]?.hash;
6770
- await this.git.fetch("origin");
6771
- log("[EARS-C10] Checking for local changes that would be overwritten...");
6772
- let remoteChangedFiles = [];
6773
- try {
6774
- const { stdout: remoteChanges } = await execAsync(
6775
- `git diff --name-only ${stateBranch} origin/${stateBranch} -- .gitgov/ 2>/dev/null || true`,
6776
- { cwd: pullRepoRoot }
6777
- );
6778
- remoteChangedFiles = remoteChanges.trim().split("\n").filter((f) => f && shouldSyncFile(f));
6779
- log(`[EARS-C10] Remote changed files: ${remoteChangedFiles.length} - ${remoteChangedFiles.join(", ")}`);
6780
- } catch {
6781
- log("[EARS-C10] Could not determine remote changes, continuing...");
6782
- }
6783
- let localModifiedFiles = [];
6784
- if (remoteChangedFiles.length > 0 && savedSyncableFiles.size > 0) {
6785
- try {
6786
- for (const remoteFile of remoteChangedFiles) {
6787
- const savedContent = savedSyncableFiles.get(remoteFile);
6788
- if (savedContent !== void 0) {
6789
- try {
6790
- const { stdout: gitStateContent } = await execAsync(
6791
- `git show HEAD:${remoteFile} 2>/dev/null`,
6792
- { cwd: pullRepoRoot }
6793
- );
6794
- if (savedContent !== gitStateContent) {
6795
- localModifiedFiles.push(remoteFile);
6796
- log(`[EARS-C10] Local file was modified since last sync: ${remoteFile}`);
6797
- }
6798
- } catch {
6799
- localModifiedFiles.push(remoteFile);
6800
- log(`[EARS-C10] Local file is new (not in gitgov-state): ${remoteFile}`);
6801
- }
6802
- }
6803
- }
6804
- log(`[EARS-C10] Local modified files that overlap with remote: ${localModifiedFiles.length}`);
6805
- } catch (error) {
6806
- log(`[EARS-C10] Warning: Could not check local modifications: ${error.message}`);
6807
- }
6808
- }
6809
- if (localModifiedFiles.length > 0) {
6810
- if (force) {
6811
- log(`[EARS-C11] Force flag set - will overwrite ${localModifiedFiles.length} local file(s)`);
6812
- logger6.warn(`[pullState] Force pull: overwriting local changes to ${localModifiedFiles.length} file(s)`);
6813
- result.forcedOverwrites = localModifiedFiles;
6814
- } else {
6815
- log(`[EARS-C10] CONFLICT: Local changes would be overwritten by pull`);
6816
- await this.git.checkoutBranch(savedBranch);
6817
- for (const [filePath, content] of savedSyncableFiles) {
6818
- const fullPath = path9__default.join(pullRepoRoot, filePath);
6819
- try {
6820
- await promises.mkdir(path9__default.dirname(fullPath), { recursive: true });
6821
- await promises.writeFile(fullPath, content, "utf-8");
6822
- log(`[EARS-C10] Restored syncable file: ${filePath}`);
6823
- } catch {
6824
- }
6825
- }
6826
- for (const [fileName, content] of savedLocalFiles) {
6827
- const filePath = path9__default.join(pullRepoRoot, ".gitgov", fileName);
6828
- try {
6829
- await promises.mkdir(path9__default.dirname(filePath), { recursive: true });
6830
- await promises.writeFile(filePath, content, "utf-8");
6831
- log(`[EARS-C10] Restored local-only file: ${fileName}`);
6832
- } catch {
6833
- }
6834
- }
6835
- result.success = false;
6836
- result.conflictDetected = true;
6837
- result.conflictInfo = {
6838
- type: "local_changes_conflict",
6839
- affectedFiles: localModifiedFiles,
6840
- message: `Your local changes to the following files would be overwritten by pull.
6841
- You have modified these files locally, and they were also modified remotely.
6842
- To avoid losing your changes, push first or use --force to overwrite.`,
6843
- resolutionSteps: [
6844
- "1. Run 'gitgov sync push' to push your local changes first",
6845
- " \u2192 This will trigger a rebase and let you resolve conflicts properly",
6846
- "2. Or run 'gitgov sync pull --force' to discard your local changes"
6847
- ]
6848
- };
6849
- result.error = "Aborting pull: local changes would be overwritten by remote changes";
6850
- logger6.warn(`[pullState] Aborting: local changes to ${localModifiedFiles.length} file(s) would be overwritten by pull`);
6851
- return result;
6852
- }
6853
- }
6854
- log("[EARS-C10] No conflicting local changes (or force enabled), proceeding with pull...");
6855
- try {
6856
- await this.git.pullRebase("origin", stateBranch);
6857
- } catch (error) {
6858
- const conflictedFiles = await this.git.getConflictedFiles();
6859
- if (conflictedFiles.length > 0) {
6860
- await this.git.checkoutBranch(savedBranch);
6861
- result.conflictDetected = true;
6862
- result.conflictInfo = {
6863
- type: "rebase_conflict",
6864
- affectedFiles: conflictedFiles,
6865
- message: "Conflict detected during pull",
6866
- resolutionSteps: [
6867
- "Review conflicted files",
6868
- "Manually resolve conflicts",
6869
- "Run 'gitgov sync resolve' to complete"
6870
- ]
6871
- };
6872
- result.error = "Conflict detected during pull";
6873
- return result;
6874
- }
6875
- throw error;
6876
- }
6877
- log("Pull rebase successful");
6878
- log("Phase 3: Checking for changes and re-indexing...");
6879
- const commitAfter = await this.git.getCommitHistory(stateBranch, {
6880
- maxCount: 1
6881
- });
6882
- const hashAfter = commitAfter[0]?.hash;
6883
- const hasNewChanges = hashBefore !== hashAfter;
6884
- result.hasChanges = hasNewChanges;
6885
- const indexPath = path9__default.join(pullRepoRoot, ".gitgov", "index.json");
6886
- const indexExists = await promises.access(indexPath).then(() => true).catch(() => false);
6887
- const shouldReindex = hasNewChanges || forceReindex || !indexExists;
6888
- if (shouldReindex) {
6889
- result.reindexed = true;
6890
- if (hasNewChanges && hashBefore && hashAfter) {
6891
- const changedFiles = await this.git.getChangedFiles(
6892
- hashBefore,
6893
- hashAfter,
6894
- ".gitgov/"
6895
- );
6896
- result.filesUpdated = changedFiles.length;
6897
- }
6898
- }
6899
- const gitgovPath = path9__default.join(pullRepoRoot, ".gitgov");
6900
- const gitgovExists = await promises.access(gitgovPath).then(() => true).catch(() => false);
6901
- if (gitgovExists && hasNewChanges) {
6902
- logger6.debug("[pullState] Copying .gitgov/ to filesystem for work branch access");
6903
- }
6904
- log("Phase 4: Restoring working branch...");
6905
- await this.git.checkoutBranch(savedBranch);
6906
- if (gitgovExists) {
6907
- try {
6908
- logger6.debug("[pullState] Restoring .gitgov/ to filesystem from gitgov-state (preserving local-only files)");
6909
- const pathsToCheckout = [];
6910
- for (const dirName of SYNC_DIRECTORIES) {
6911
- pathsToCheckout.push(`.gitgov/${dirName}`);
6912
- }
6913
- for (const fileName of SYNC_ROOT_FILES) {
6914
- pathsToCheckout.push(`.gitgov/${fileName}`);
6915
- }
6916
- for (const checkoutPath of pathsToCheckout) {
6917
- try {
6918
- await execAsync(`git checkout ${stateBranch} -- "${checkoutPath}"`, { cwd: pullRepoRoot });
6919
- logger6.debug(`[pullState] Checked out: ${checkoutPath}`);
6920
- } catch {
6921
- logger6.debug(`[pullState] Skipped (not in gitgov-state): ${checkoutPath}`);
6922
- }
6923
- }
6924
- try {
6925
- await execAsync("git reset HEAD .gitgov/", { cwd: pullRepoRoot });
6926
- } catch {
6927
- }
6928
- for (const [fileName, content] of savedLocalFiles) {
6929
- try {
6930
- const filePath = path9__default.join(pullRepoRoot, ".gitgov", fileName);
6931
- await promises.writeFile(filePath, content, "utf-8");
6932
- logger6.debug(`[EARS-C8] Restored local-only file: ${fileName}`);
6933
- } catch (writeError) {
6934
- logger6.warn(`[EARS-C8] Failed to restore ${fileName}: ${writeError.message}`);
6935
- }
6936
- }
6937
- logger6.debug("[pullState] .gitgov/ restored to filesystem successfully (local-only files preserved)");
6938
- } catch (error) {
6939
- logger6.warn(`[pullState] Failed to restore .gitgov/ to filesystem: ${error.message}`);
6940
- }
6941
- }
6942
- if (shouldReindex) {
6943
- logger6.info("Invoking RecordProjector.generateIndex() after pull...");
6944
- try {
6945
- await this.indexer.generateIndex();
6946
- logger6.info("Index regenerated successfully");
6947
- } catch (error) {
6948
- logger6.warn(`Failed to regenerate index: ${error.message}`);
6949
- }
6950
- }
6951
- result.success = true;
6952
- log(`=== pullState COMPLETED: ${hasNewChanges ? "new changes pulled" : "no changes"}, reindexed: ${result.reindexed} ===`);
6953
- return result;
6954
- } catch (error) {
6955
- log(`=== pullState FAILED: ${error.message} ===`);
6956
- if (error instanceof UncommittedChangesError) {
6957
- throw error;
6958
- }
6959
- result.error = error.message;
6960
- return result;
6961
- }
6962
- }
6963
- /**
6964
- * Resolves state conflicts in a governed manner (Git-Native).
6965
- *
6966
- * Git-Native Flow:
6967
- * 1. User resolves conflicts using standard Git tools (edit files, remove markers)
6968
- * 2. User stages resolved files: git add .gitgov/
6969
- * 3. User runs: gitgov sync resolve --reason "reason"
6970
- *
6971
- * This method:
6972
- * - Verifies that a rebase is in progress
6973
- * - Checks that no conflict markers remain in staged files
6974
- * - Updates resolved Records with new checksums and signatures
6975
- * - Continues the git rebase (git rebase --continue)
6976
- * - Creates a signed resolution commit
6977
- * - Regenerates the index
6978
- *
6979
- * [EARS-D1 through EARS-D7]
6980
- */
6981
- async resolveConflict(options) {
6982
- const { reason, actorId } = options;
6983
- const log = (msg) => logger6.debug(`[resolveConflict] ${msg}`);
6984
- log("=== STARTING resolveConflict (Git-Native) ===");
6985
- log("Phase 0: Verifying rebase in progress...");
6986
- const rebaseInProgress = await this.isRebaseInProgress();
6987
- if (!rebaseInProgress) {
6988
- throw new NoRebaseInProgressError();
6989
- }
6990
- const authenticatedActor = await this.identity.getCurrentActor();
6991
- if (authenticatedActor.id !== actorId) {
6992
- log(`ERROR: Actor identity mismatch: requested '${actorId}' but authenticated as '${authenticatedActor.id}'`);
6993
- throw new ActorIdentityMismatchError(actorId, authenticatedActor.id);
6994
- }
6995
- log(`Pre-check passed: actorId '${actorId}' matches authenticated identity`);
6996
- log("Conflict mode: rebase_conflict (Git-Native)");
6997
- console.log("[resolveConflict] Getting staged files...");
6998
- const allStagedFiles = await this.git.getStagedFiles();
6999
- console.log("[resolveConflict] All staged files:", allStagedFiles);
7000
- let resolvedRecords = allStagedFiles.filter(
7001
- (f) => f.startsWith(".gitgov/") && f.endsWith(".json")
7002
- );
7003
- console.log("[resolveConflict] Resolved Records (staged .gitgov/*.json):", resolvedRecords);
7004
- console.log("[resolveConflict] Checking for conflict markers...");
7005
- const filesWithMarkers = await this.checkConflictMarkers(resolvedRecords);
7006
- console.log("[resolveConflict] Files with markers:", filesWithMarkers);
7007
- if (filesWithMarkers.length > 0) {
7008
- throw new ConflictMarkersPresentError(filesWithMarkers);
7009
- }
7010
- let rebaseCommitHash = "";
7011
- console.log("[resolveConflict] Step 4: Calling git.rebaseContinue()...");
7012
- await this.git.rebaseContinue();
7013
- console.log("[resolveConflict] rebaseContinue completed successfully");
7014
- const currentBranch = await this.git.getCurrentBranch();
7015
- const rebaseCommit = await this.git.getCommitHistory(currentBranch, {
7016
- maxCount: 1
7017
- });
7018
- rebaseCommitHash = rebaseCommit[0]?.hash ?? "";
7019
- if (resolvedRecords.length === 0 && rebaseCommitHash) {
7020
- console.log("[resolveConflict] No staged files detected, getting files from rebase commit...");
7021
- const repoRoot2 = await this.git.getRepoRoot();
7022
- try {
7023
- const { stdout } = await execAsync(
7024
- `git diff-tree --no-commit-id --name-only -r ${rebaseCommitHash}`,
7025
- { cwd: repoRoot2 }
7026
- );
7027
- const commitFiles = stdout.trim().split("\n").filter((f) => f);
7028
- resolvedRecords = commitFiles.filter(
7029
- (f) => f.startsWith(".gitgov/") && f.endsWith(".json")
7030
- );
7031
- console.log("[resolveConflict] Files from rebase commit:", resolvedRecords);
7032
- } catch (e) {
7033
- console.log("[resolveConflict] Could not get files from rebase commit:", e);
7034
- }
7035
- }
7036
- console.log("[resolveConflict] Updating resolved Records with signatures...");
7037
- if (resolvedRecords.length > 0) {
7038
- const currentActor = await this.identity.getCurrentActor();
7039
- console.log("[resolveConflict] Current actor:", currentActor);
7040
- console.log("[resolveConflict] Processing", resolvedRecords.length, "resolved Records");
7041
- for (const filePath of resolvedRecords) {
7042
- console.log("[resolveConflict] Processing Record:", filePath);
7043
- try {
7044
- const repoRoot2 = await this.git.getRepoRoot();
7045
- const fullPath = join(repoRoot2, filePath);
7046
- const content = readFileSync(fullPath, "utf-8");
7047
- const record = JSON.parse(content);
7048
- if (!record.header || !record.payload) {
7049
- continue;
7050
- }
7051
- const signedRecord = await this.identity.signRecord(
7052
- record,
7053
- currentActor.id,
7054
- "resolver",
7055
- `Conflict resolved: ${reason}`
7056
- );
7057
- writeFileSync(fullPath, JSON.stringify(signedRecord, null, 2) + "\n", "utf-8");
7058
- logger6.info(`Updated Record: ${filePath} (new checksum + resolver signature)`);
7059
- console.log("[resolveConflict] Successfully updated Record:", filePath);
7060
- } catch (error) {
7061
- logger6.debug(`Skipping file ${filePath}: ${error.message}`);
7062
- console.log("[resolveConflict] Error updating Record:", filePath, error);
7063
- }
7064
- }
7065
- console.log("[resolveConflict] All Records updated, staging...");
7066
- }
7067
- console.log("[resolveConflict] Staging .gitgov/ with updated metadata...");
7068
- await this.git.add([".gitgov"], { force: true });
7069
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
7070
- const resolutionMessage = `resolution: conflict resolved by ${actorId}
7071
-
7072
- Actor: ${actorId}
7073
- Timestamp: ${timestamp}
7074
- Reason: ${reason}
7075
- Files: ${resolvedRecords.length} file(s) resolved
7076
-
7077
- Signed-off-by: ${actorId}`;
7078
- let resolutionCommitHash = "";
7079
- try {
7080
- resolutionCommitHash = await this.git.commit(resolutionMessage);
7081
- } catch (commitError) {
7082
- const stdout = commitError.stdout || "";
7083
- const stderr = commitError.stderr || "";
7084
- const isNothingToCommit = stdout.includes("nothing to commit") || stderr.includes("nothing to commit") || stdout.includes("nothing added to commit") || stderr.includes("nothing added to commit");
7085
- if (isNothingToCommit) {
7086
- log("No additional changes to commit (no records needed re-signing)");
7087
- resolutionCommitHash = rebaseCommitHash;
7088
- } else {
7089
- throw commitError;
7090
- }
7091
- }
7092
- log("Pushing resolved state to remote...");
7093
- try {
7094
- await this.git.push("origin", "gitgov-state");
7095
- log("Push successful");
7096
- } catch (pushError) {
7097
- const pushErrorMsg = pushError instanceof Error ? pushError.message : String(pushError);
7098
- log(`Push failed (non-fatal): ${pushErrorMsg}`);
7099
- }
7100
- log("Returning to original branch and restoring .gitgov/ files...");
7101
- const repoRoot = await this.git.getRepoRoot();
7102
- const gitgovDir = path9__default.join(repoRoot, ".gitgov");
7103
- const tempDir = path9__default.join(os.tmpdir(), `gitgov-resolve-${Date.now()}`);
7104
- await promises.mkdir(tempDir, { recursive: true });
7105
- log(`Created temp directory for local files: ${tempDir}`);
7106
- for (const fileName of LOCAL_ONLY_FILES) {
7107
- const srcPath = path9__default.join(gitgovDir, fileName);
7108
- const destPath = path9__default.join(tempDir, fileName);
7109
- try {
7110
- await promises.access(srcPath);
7111
- await promises.cp(srcPath, destPath, { force: true });
7112
- log(`Saved LOCAL_ONLY_FILE to temp: ${fileName}`);
7113
- } catch {
7114
- }
7115
- }
7116
- const saveExcludedFiles = async (srcDir, destDir) => {
7117
- try {
7118
- const entries = await promises.readdir(srcDir, { withFileTypes: true });
7119
- for (const entry of entries) {
7120
- const srcPath = path9__default.join(srcDir, entry.name);
7121
- const dstPath = path9__default.join(destDir, entry.name);
7122
- if (entry.isDirectory()) {
7123
- await promises.mkdir(dstPath, { recursive: true });
7124
- await saveExcludedFiles(srcPath, dstPath);
7125
- } else {
7126
- const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
7127
- if (isExcluded) {
7128
- await promises.mkdir(path9__default.dirname(dstPath), { recursive: true });
7129
- await promises.copyFile(srcPath, dstPath);
7130
- log(`Saved EXCLUDED file to temp: ${entry.name}`);
7131
- }
7132
- }
7133
- }
7134
- } catch {
7135
- }
7136
- };
7137
- await saveExcludedFiles(gitgovDir, tempDir);
7138
- try {
7139
- await execAsync("git checkout -", { cwd: repoRoot });
7140
- log("Returned to original branch");
7141
- } catch (checkoutError) {
7142
- log(`Warning: Could not return to original branch: ${checkoutError}`);
7143
- }
7144
- log("Restoring .gitgov/ from gitgov-state...");
7145
- try {
7146
- await this.git.checkoutFilesFromBranch("gitgov-state", [".gitgov/"]);
7147
- await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot });
7148
- log("Restored .gitgov/ from gitgov-state (unstaged)");
7149
- } catch (checkoutFilesError) {
7150
- log(`Warning: Could not restore .gitgov/ from gitgov-state: ${checkoutFilesError}`);
7151
- }
7152
- for (const fileName of LOCAL_ONLY_FILES) {
7153
- const srcPath = path9__default.join(tempDir, fileName);
7154
- const destPath = path9__default.join(gitgovDir, fileName);
7155
- try {
7156
- await promises.access(srcPath);
7157
- await promises.cp(srcPath, destPath, { force: true });
7158
- log(`Restored LOCAL_ONLY_FILE from temp: ${fileName}`);
7159
- } catch {
7160
- }
7161
- }
7162
- const restoreExcludedFiles = async (srcDir, destDir) => {
7163
- try {
7164
- const entries = await promises.readdir(srcDir, { withFileTypes: true });
7165
- for (const entry of entries) {
7166
- const srcPath = path9__default.join(srcDir, entry.name);
7167
- const dstPath = path9__default.join(destDir, entry.name);
7168
- if (entry.isDirectory()) {
7169
- await restoreExcludedFiles(srcPath, dstPath);
7170
- } else {
7171
- const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
7172
- if (isExcluded) {
7173
- await promises.mkdir(path9__default.dirname(dstPath), { recursive: true });
7174
- await promises.copyFile(srcPath, dstPath);
7175
- log(`Restored EXCLUDED file from temp: ${entry.name}`);
7176
- }
7177
- }
7178
- }
7179
- } catch {
7180
- }
7181
- };
7182
- await restoreExcludedFiles(tempDir, gitgovDir);
7183
- try {
7184
- await promises.rm(tempDir, { recursive: true, force: true });
7185
- log("Temp directory cleaned up");
7186
- } catch {
7187
- }
7188
- logger6.info("Invoking RecordProjector.generateIndex() after conflict resolution...");
7189
- try {
7190
- await this.indexer.generateIndex();
7191
- logger6.info("Index regenerated successfully after conflict resolution");
7192
- } catch (error) {
7193
- logger6.warn(`Failed to regenerate index after resolution: ${error.message}`);
7194
- }
7195
- log(`=== resolveConflict COMPLETED: ${resolvedRecords.length} conflicts resolved ===`);
7196
- return {
7197
- success: true,
7198
- rebaseCommitHash,
7199
- resolutionCommitHash,
7200
- conflictsResolved: resolvedRecords.length,
7201
- resolvedBy: actorId,
7202
- reason
7203
- };
7204
- }
7205
- };
7206
-
7207
- // src/sync_state/fs_worktree/fs_worktree_sync_state.types.ts
7208
- var WORKTREE_DIR_NAME = ".gitgov-worktree";
7209
- var DEFAULT_STATE_BRANCH = "gitgov-state";
7210
- var logger7 = createLogger("[WorktreeSyncState] ");
7211
- function shouldSyncFile2(filePath) {
7212
- const fileName = path9__default.basename(filePath);
7213
- const ext = path9__default.extname(filePath);
7214
- if (!SYNC_ALLOWED_EXTENSIONS.includes(ext)) {
7215
- return false;
7216
- }
7217
- for (const pattern of SYNC_EXCLUDED_PATTERNS) {
7218
- if (pattern.test(fileName)) {
7219
- return false;
7220
- }
7221
- }
7222
- if (LOCAL_ONLY_FILES.includes(fileName)) {
7223
- return false;
7224
- }
7225
- const normalizedPath = filePath.replace(/\\/g, "/");
7226
- const parts = normalizedPath.split("/");
7227
- const gitgovIndex = parts.findIndex((p) => p === ".gitgov");
7228
- let relativeParts;
7229
- if (gitgovIndex !== -1) {
7230
- relativeParts = parts.slice(gitgovIndex + 1);
7231
- } else {
7232
- const syncDirIndex = parts.findIndex(
7233
- (p) => SYNC_DIRECTORIES.includes(p)
7234
- );
7235
- if (syncDirIndex !== -1) {
7236
- relativeParts = parts.slice(syncDirIndex);
7237
- } else if (SYNC_ROOT_FILES.includes(fileName)) {
7238
- return true;
7239
- } else {
7240
- return false;
7241
- }
7242
- }
7243
- if (relativeParts.length === 1) {
7244
- return SYNC_ROOT_FILES.includes(relativeParts[0]);
7245
- } else if (relativeParts.length >= 2) {
7246
- const dirName = relativeParts[0];
7247
- return SYNC_DIRECTORIES.includes(dirName);
7248
- }
7249
- return false;
7250
- }
7251
- var FsWorktreeSyncStateModule = class {
7252
- deps;
7253
- repoRoot;
7254
- stateBranchName;
7255
- worktreePath;
7256
- gitgovPath;
7257
- constructor(deps, config) {
7258
- if (!deps.git) throw new Error("GitModule is required for FsWorktreeSyncStateModule");
7259
- if (!deps.config) throw new Error("ConfigManager is required for FsWorktreeSyncStateModule");
7260
- if (!deps.identity) throw new Error("IdentityAdapter is required for FsWorktreeSyncStateModule");
7261
- if (!deps.lint) throw new Error("LintModule is required for FsWorktreeSyncStateModule");
7262
- if (!deps.indexer) throw new Error("IndexerAdapter is required for FsWorktreeSyncStateModule");
7263
- if (!config.repoRoot) throw new Error("repoRoot is required");
7264
- this.deps = deps;
7265
- this.repoRoot = config.repoRoot;
7266
- this.stateBranchName = config.stateBranchName ?? DEFAULT_STATE_BRANCH;
7267
- this.worktreePath = config.worktreePath ?? path9__default.join(this.repoRoot, WORKTREE_DIR_NAME);
7268
- this.gitgovPath = path9__default.join(this.worktreePath, ".gitgov");
7269
- }
7270
- // ═══════════════════════════════════════════════
7271
- // Section A: Worktree Management (WTSYNC-A1..A7)
7272
- // ═══════════════════════════════════════════════
7273
- /** [WTSYNC-A4] Returns the worktree path */
7274
- getWorktreePath() {
7275
- return this.worktreePath;
7276
- }
7277
- /** [WTSYNC-A1..A6] Ensures worktree exists and is healthy */
7278
- async ensureWorktree() {
7279
- const health = await this.checkWorktreeHealth();
7280
- if (health.healthy) {
7281
- logger7.debug("Worktree is healthy");
7282
- await this.removeLegacyGitignore();
7283
- return;
7284
- }
7285
- if (health.exists && !health.healthy) {
7286
- logger7.warn(`Worktree corrupted: ${health.error}. Recreating...`);
7287
- await this.removeWorktree();
7288
- }
7289
- await this.ensureStateBranch();
7290
- try {
7291
- logger7.info(`Creating worktree at ${this.worktreePath}`);
7292
- await this.execGit(["worktree", "add", this.worktreePath, this.stateBranchName]);
7293
- } catch (error) {
7294
- throw new WorktreeSetupError(
7295
- "Failed to create worktree",
7296
- this.worktreePath,
7297
- error instanceof Error ? error : void 0
7298
- );
7299
- }
7300
- await this.removeLegacyGitignore();
7301
- }
7302
- /**
7303
- * [WTSYNC-A7] Remove .gitignore from state branch if it exists.
7304
- * The worktree module filters files in code (shouldSyncFile()), not via .gitignore.
7305
- * Legacy state branches initialized by FsSyncState may have a .gitignore — remove it.
7306
- */
7307
- async removeLegacyGitignore() {
7308
- const gitignorePath = path9__default.join(this.worktreePath, ".gitignore");
7309
- if (!existsSync(gitignorePath)) return;
7310
- logger7.info("Removing legacy .gitignore from state branch");
7311
- try {
7312
- await this.execInWorktree(["rm", ".gitignore"]);
7313
- await this.execInWorktree(["commit", "-m", "gitgov: remove legacy .gitignore (filtering is in code)"]);
7314
- } catch {
7315
- try {
7316
- await promises.unlink(gitignorePath);
7317
- } catch {
7318
- }
7319
- }
7320
- }
7321
- /** Check worktree health */
7322
- async checkWorktreeHealth() {
7323
- if (!existsSync(this.worktreePath)) {
7324
- return { exists: false, healthy: false, path: this.worktreePath };
7325
- }
7326
- const gitFile = path9__default.join(this.worktreePath, ".git");
7327
- if (!existsSync(gitFile)) {
7328
- return {
7329
- exists: true,
7330
- healthy: false,
7331
- path: this.worktreePath,
7332
- error: ".git file missing in worktree"
5381
+ exists: true,
5382
+ healthy: false,
5383
+ path: this.worktreePath,
5384
+ error: ".git file missing in worktree"
7333
5385
  };
7334
5386
  }
7335
5387
  try {
@@ -7367,7 +5419,7 @@ var FsWorktreeSyncStateModule = class {
7367
5419
  /** [WTSYNC-B1..B16] Push local state to remote */
7368
5420
  async pushState(options) {
7369
5421
  const { actorId, dryRun = false, force = false } = options;
7370
- const log = (msg) => logger7.debug(`[pushState] ${msg}`);
5422
+ const log = (msg) => logger6.debug(`[pushState] ${msg}`);
7371
5423
  if (await this.isRebaseInProgress()) {
7372
5424
  throw new RebaseAlreadyInProgressError();
7373
5425
  }
@@ -7389,7 +5441,7 @@ var FsWorktreeSyncStateModule = class {
7389
5441
  };
7390
5442
  }
7391
5443
  const rawDelta = await this.calculateFileDelta();
7392
- const delta = rawDelta.filter((f) => shouldSyncFile2(f.file));
5444
+ const delta = rawDelta.filter((f) => shouldSyncFile(f.file));
7393
5445
  log(`Delta: ${delta.length} syncable files (${rawDelta.length} total)`);
7394
5446
  if (delta.length === 0) {
7395
5447
  const { ahead: aheadOfRemote, remoteExists } = await this.isLocalAheadOfRemote();
@@ -7538,7 +5590,7 @@ var FsWorktreeSyncStateModule = class {
7538
5590
  /** [WTSYNC-C1..C9] Pull remote state */
7539
5591
  async pullState(options) {
7540
5592
  const { forceReindex = false, force = false } = options ?? {};
7541
- const log = (msg) => logger7.debug(`[pullState] ${msg}`);
5593
+ const log = (msg) => logger6.debug(`[pullState] ${msg}`);
7542
5594
  if (await this.isRebaseInProgress()) {
7543
5595
  throw new RebaseAlreadyInProgressError();
7544
5596
  }
@@ -7546,7 +5598,7 @@ var FsWorktreeSyncStateModule = class {
7546
5598
  if (!force) {
7547
5599
  const statusRaw = await this.execInWorktree(["status", "--porcelain", "-uall", ".gitgov/"]);
7548
5600
  const statusLines = statusRaw.split("\n").filter((line) => line.length >= 4);
7549
- const syncableChanges = statusLines.filter((l) => shouldSyncFile2(l.slice(3)));
5601
+ const syncableChanges = statusLines.filter((l) => shouldSyncFile(l.slice(3)));
7550
5602
  if (syncableChanges.length > 0) {
7551
5603
  log(`Auto-committing ${syncableChanges.length} local changes before pull`);
7552
5604
  for (const line of syncableChanges) {
@@ -7725,7 +5777,7 @@ var FsWorktreeSyncStateModule = class {
7725
5777
  async getPendingChanges() {
7726
5778
  await this.ensureWorktree();
7727
5779
  const allChanges = await this.calculateFileDelta();
7728
- return allChanges.filter((f) => shouldSyncFile2(f.file));
5780
+ return allChanges.filter((f) => shouldSyncFile(f.file));
7729
5781
  }
7730
5782
  /** Calculate delta between source and worktree state branch */
7731
5783
  async calculateStateDelta(_sourceBranch) {
@@ -7747,10 +5799,10 @@ var FsWorktreeSyncStateModule = class {
7747
5799
  /** [WTSYNC-E6] Check if rebase is in progress in worktree */
7748
5800
  async isRebaseInProgress() {
7749
5801
  try {
7750
- const gitContent = await promises.readFile(path9__default.join(this.worktreePath, ".git"), "utf8");
5802
+ const gitContent = await promises.readFile(path6__default.join(this.worktreePath, ".git"), "utf8");
7751
5803
  const gitDir = gitContent.replace("gitdir: ", "").trim();
7752
- const resolvedGitDir = path9__default.resolve(this.worktreePath, gitDir);
7753
- return existsSync(path9__default.join(resolvedGitDir, "rebase-merge")) || existsSync(path9__default.join(resolvedGitDir, "rebase-apply"));
5804
+ const resolvedGitDir = path6__default.resolve(this.worktreePath, gitDir);
5805
+ return existsSync(path6__default.join(resolvedGitDir, "rebase-merge")) || existsSync(path6__default.join(resolvedGitDir, "rebase-apply"));
7754
5806
  } catch {
7755
5807
  return false;
7756
5808
  }
@@ -7759,7 +5811,7 @@ var FsWorktreeSyncStateModule = class {
7759
5811
  async checkConflictMarkers(filePaths) {
7760
5812
  const filesWithMarkers = [];
7761
5813
  for (const filePath of filePaths) {
7762
- const fullPath = path9__default.join(this.gitgovPath, filePath);
5814
+ const fullPath = path6__default.join(this.gitgovPath, filePath);
7763
5815
  try {
7764
5816
  const content = await promises.readFile(fullPath, "utf8");
7765
5817
  if (content.includes("<<<<<<<") || content.includes(">>>>>>>")) {
@@ -7775,7 +5827,7 @@ var FsWorktreeSyncStateModule = class {
7775
5827
  const files = filePaths ?? await this.getConflictedFiles();
7776
5828
  const diffFiles = [];
7777
5829
  for (const file of files) {
7778
- const fullPath = path9__default.join(this.worktreePath, file);
5830
+ const fullPath = path6__default.join(this.worktreePath, file);
7779
5831
  try {
7780
5832
  const content = await promises.readFile(fullPath, "utf8");
7781
5833
  let localContent = "";
@@ -7873,7 +5925,7 @@ var FsWorktreeSyncStateModule = class {
7873
5925
  filePaths
7874
5926
  } = options ?? {};
7875
5927
  if (expectedFilesScope === "all-commits") {
7876
- logger7.debug('expectedFilesScope "all-commits" treated as "head" in worktree module');
5928
+ logger6.debug('expectedFilesScope "all-commits" treated as "head" in worktree module');
7877
5929
  }
7878
5930
  await this.ensureWorktree();
7879
5931
  const integrityViolations = await this.verifyResolutionIntegrity();
@@ -7889,7 +5941,7 @@ var FsWorktreeSyncStateModule = class {
7889
5941
  if (verifyExpectedFiles) {
7890
5942
  if (filePaths && filePaths.length > 0) {
7891
5943
  for (const fp of filePaths) {
7892
- if (!existsSync(path9__default.join(this.gitgovPath, fp))) {
5944
+ if (!existsSync(path6__default.join(this.gitgovPath, fp))) {
7893
5945
  integrityViolations.push({
7894
5946
  rebaseCommitHash: "",
7895
5947
  commitMessage: `Missing expected file: ${fp}`,
@@ -7901,7 +5953,7 @@ var FsWorktreeSyncStateModule = class {
7901
5953
  } else {
7902
5954
  const expectedDirs = ["tasks", "cycles", "actors"];
7903
5955
  for (const dir of expectedDirs) {
7904
- if (!existsSync(path9__default.join(this.gitgovPath, dir))) {
5956
+ if (!existsSync(path6__default.join(this.gitgovPath, dir))) {
7905
5957
  integrityViolations.push({
7906
5958
  rebaseCommitHash: "",
7907
5959
  commitMessage: `Missing expected directory: ${dir}`,
@@ -7910,7 +5962,7 @@ var FsWorktreeSyncStateModule = class {
7910
5962
  });
7911
5963
  }
7912
5964
  }
7913
- if (!existsSync(path9__default.join(this.gitgovPath, "config.json"))) {
5965
+ if (!existsSync(path6__default.join(this.gitgovPath, "config.json"))) {
7914
5966
  integrityViolations.push({
7915
5967
  rebaseCommitHash: "",
7916
5968
  commitMessage: "Missing expected file: config.json",
@@ -8007,7 +6059,7 @@ var FsWorktreeSyncStateModule = class {
8007
6059
  async stageSyncableFiles(delta, log) {
8008
6060
  let stagedCount = 0;
8009
6061
  for (const file of delta) {
8010
- if (!shouldSyncFile2(file.file)) {
6062
+ if (!shouldSyncFile(file.file)) {
8011
6063
  log(`Skipped (not syncable): ${file.file}`);
8012
6064
  continue;
8013
6065
  }
@@ -8033,7 +6085,7 @@ var FsWorktreeSyncStateModule = class {
8033
6085
  /** Re-sign records after conflict resolution */
8034
6086
  async resignResolvedRecords(filePaths, actorId, reason) {
8035
6087
  for (const filePath of filePaths) {
8036
- const fullPath = path9__default.join(this.gitgovPath, filePath);
6088
+ const fullPath = path6__default.join(this.gitgovPath, filePath);
8037
6089
  try {
8038
6090
  const content = await promises.readFile(fullPath, "utf8");
8039
6091
  const record = JSON.parse(content);
@@ -8048,7 +6100,7 @@ var FsWorktreeSyncStateModule = class {
8048
6100
  try {
8049
6101
  await this.deps.indexer.generateIndex();
8050
6102
  } catch {
8051
- logger7.warn("Re-index failed");
6103
+ logger6.warn("Re-index failed");
8052
6104
  }
8053
6105
  }
8054
6106
  /** Parse git diff --name-status output */
@@ -8346,7 +6398,7 @@ var LocalBackend = class {
8346
6398
  * Executes via entrypoint (dynamic import) and captures output.
8347
6399
  */
8348
6400
  async executeEntrypoint(engine, ctx) {
8349
- const absolutePath = path9__default.join(this.projectRoot, engine.entrypoint);
6401
+ const absolutePath = path6__default.join(this.projectRoot, engine.entrypoint);
8350
6402
  const mod = await import(absolutePath);
8351
6403
  const fnName = engine.function || "runAgent";
8352
6404
  const fn = mod[fnName];
@@ -8785,7 +6837,7 @@ var FsAgentRunner = class {
8785
6837
  throw new MissingDependencyError("ExecutionAdapter", "required");
8786
6838
  }
8787
6839
  this.projectRoot = deps.projectRoot;
8788
- this.gitgovPath = deps.gitgovPath ?? path9__default.join(this.projectRoot, ".gitgov");
6840
+ this.gitgovPath = deps.gitgovPath ?? path6__default.join(this.projectRoot, ".gitgov");
8789
6841
  this.identityAdapter = deps.identityAdapter ?? void 0;
8790
6842
  this.executionAdapter = deps.executionAdapter;
8791
6843
  this.eventBus = deps.eventBus ?? void 0;
@@ -8931,7 +6983,7 @@ var FsAgentRunner = class {
8931
6983
  */
8932
6984
  async loadAgent(agentId) {
8933
6985
  const id = agentId.startsWith("agent:") ? agentId.slice(6) : agentId;
8934
- const agentPath = path9__default.join(this.gitgovPath, "agents", `agent-${id}.json`);
6986
+ const agentPath = path6__default.join(this.gitgovPath, "agents", `agent-${id}.json`);
8935
6987
  try {
8936
6988
  const content = await promises.readFile(agentPath, "utf-8");
8937
6989
  const record = JSON.parse(content);
@@ -8963,10 +7015,10 @@ function createAgentRunner(deps) {
8963
7015
  var FsRecordProjection = class {
8964
7016
  indexPath;
8965
7017
  constructor(options) {
8966
- this.indexPath = path9.join(options.basePath, "index.json");
7018
+ this.indexPath = path6.join(options.basePath, "index.json");
8967
7019
  }
8968
7020
  async persist(data, _context) {
8969
- const dir = path9.dirname(this.indexPath);
7021
+ const dir = path6.dirname(this.indexPath);
8970
7022
  await fs.mkdir(dir, { recursive: true });
8971
7023
  const tmpPath = `${this.indexPath}.tmp`;
8972
7024
  const content = JSON.stringify(data, null, 2);
@@ -9004,6 +7056,6 @@ var FsRecordProjection = class {
9004
7056
  }
9005
7057
  };
9006
7058
 
9007
- export { DEFAULT_ID_ENCODER, FsAgentRunner, FsConfigStore, FsFileLister, FsKeyProvider, FsLintModule, FsProjectInitializer, FsRecordProjection, FsRecordStore, FsSessionStore, FsSyncStateModule, FsWatcherStateModule, FsWorktreeSyncStateModule, LocalGitModule as GitModule, LocalGitModule, createAgentRunner, createConfigManager, createSessionManager, findGitgovRoot, findProjectRoot, getGitgovPath, isGitgovProject, resetDiscoveryCache };
7059
+ export { DEFAULT_ID_ENCODER, FsAgentRunner, FsConfigStore, FsFileLister, FsKeyProvider, FsLintModule, FsProjectInitializer, FsRecordProjection, FsRecordStore, FsSessionStore, FsWatcherStateModule, FsWorktreeSyncStateModule, LocalGitModule as GitModule, LocalGitModule, createAgentRunner, createConfigManager, createSessionManager, findProjectRoot, getWorktreeBasePath, resetDiscoveryCache };
9008
7060
  //# sourceMappingURL=fs.js.map
9009
7061
  //# sourceMappingURL=fs.js.map