@gitgov/core 2.3.0 → 2.4.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
@@ -382,10 +382,10 @@ var SessionManager = class {
382
382
  // src/session_store/fs/fs_session_store.ts
383
383
  var FsSessionStore = class {
384
384
  sessionPath;
385
- actorsPath;
385
+ keysPath;
386
386
  constructor(projectRootPath) {
387
387
  this.sessionPath = path9.join(projectRootPath, ".gitgov", ".session.json");
388
- this.actorsPath = path9.join(projectRootPath, ".gitgov", "actors");
388
+ this.keysPath = path9.join(projectRootPath, ".gitgov", "keys");
389
389
  }
390
390
  /**
391
391
  * Load local session from .gitgov/.session.json
@@ -414,12 +414,12 @@ var FsSessionStore = class {
414
414
  await promises.writeFile(this.sessionPath, JSON.stringify(session, null, 2), "utf-8");
415
415
  }
416
416
  /**
417
- * Detect actor from .key files in .gitgov/actors/
417
+ * Detect actor from .key files in .gitgov/keys/
418
418
  *
419
419
  * [EARS-C1] Returns actor ID from first .key file
420
420
  * [EARS-C2] Returns first .key file alphabetically if multiple exist
421
421
  * [EARS-C3] Returns null if no .key files exist
422
- * [EARS-C4] Returns null if actors directory doesn't exist
422
+ * [EARS-C4] Returns null if keys directory doesn't exist
423
423
  * [EARS-C5] Ignores non-.key files
424
424
  * [EARS-C6] Returns null for empty directory
425
425
  *
@@ -427,7 +427,7 @@ var FsSessionStore = class {
427
427
  */
428
428
  async detectActorFromKeyFiles() {
429
429
  try {
430
- const files = await promises.readdir(this.actorsPath);
430
+ const files = await promises.readdir(this.keysPath);
431
431
  const keyFiles = files.filter((f) => f.endsWith(".key"));
432
432
  const firstKeyFile = keyFiles[0];
433
433
  if (!firstKeyFile) {
@@ -457,11 +457,11 @@ var KeyProviderError = class extends Error {
457
457
 
458
458
  // src/key_provider/fs/fs_key_provider.ts
459
459
  var FsKeyProvider = class {
460
- actorsDir;
460
+ keysDir;
461
461
  extension;
462
462
  fileMode;
463
463
  constructor(options) {
464
- this.actorsDir = options.actorsDir;
464
+ this.keysDir = options.keysDir;
465
465
  this.extension = options.extension ?? ".key";
466
466
  this.fileMode = options.fileMode ?? 384;
467
467
  }
@@ -492,14 +492,14 @@ var FsKeyProvider = class {
492
492
  }
493
493
  /**
494
494
  * [EARS-KP03] Stores a private key for an actor.
495
- * [EARS-FKP01] Creates actorsDir if not exists.
496
- * [EARS-FKP02] Writes key to {actorsDir}/{actorId}.key.
495
+ * [EARS-FKP01] Creates keysDir if not exists.
496
+ * [EARS-FKP02] Writes key to {keysDir}/{actorId}.key.
497
497
  * [EARS-FKP03] Sets secure file permissions (0600).
498
498
  */
499
499
  async setPrivateKey(actorId, privateKey) {
500
500
  const keyPath = this.getKeyPath(actorId);
501
501
  try {
502
- await fs.mkdir(this.actorsDir, { recursive: true });
502
+ await fs.mkdir(this.keysDir, { recursive: true });
503
503
  await fs.writeFile(keyPath, privateKey, "utf-8");
504
504
  await fs.chmod(keyPath, this.fileMode);
505
505
  } catch (error) {
@@ -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.actorsDir, `${sanitized}${this.extension}`);
550
+ return path9.join(this.keysDir, `${sanitized}${this.extension}`);
551
551
  }
552
552
  /**
553
553
  * [EARS-FKP04] Sanitizes actorId to prevent directory traversal.
@@ -3519,25 +3519,25 @@ var FsLintModule = class {
3519
3519
  this.projectRoot = dependencies.projectRoot;
3520
3520
  this.lintModule = dependencies.lintModule;
3521
3521
  this.fileSystem = dependencies.fileSystem ?? {
3522
- readFile: async (path13, encoding) => {
3523
- return promises.readFile(path13, encoding);
3522
+ readFile: async (path14, encoding) => {
3523
+ return promises.readFile(path14, encoding);
3524
3524
  },
3525
- writeFile: async (path13, content) => {
3526
- await promises.writeFile(path13, content, "utf-8");
3525
+ writeFile: async (path14, content) => {
3526
+ await promises.writeFile(path14, content, "utf-8");
3527
3527
  },
3528
- exists: async (path13) => {
3528
+ exists: async (path14) => {
3529
3529
  try {
3530
- await promises.access(path13);
3530
+ await promises.access(path14);
3531
3531
  return true;
3532
3532
  } catch {
3533
3533
  return false;
3534
3534
  }
3535
3535
  },
3536
- unlink: async (path13) => {
3537
- await promises.unlink(path13);
3536
+ unlink: async (path14) => {
3537
+ await promises.unlink(path14);
3538
3538
  },
3539
- readdir: async (path13) => {
3540
- return readdir(path13);
3539
+ readdir: async (path14) => {
3540
+ return readdir(path14);
3541
3541
  }
3542
3542
  };
3543
3543
  }
@@ -4141,6 +4141,7 @@ function getImportMetaUrl() {
4141
4141
  var logger4 = createLogger("[FsProjectInitializer] ");
4142
4142
  var GITGOV_DIRECTORIES = [
4143
4143
  "actors",
4144
+ "keys",
4144
4145
  "cycles",
4145
4146
  "tasks",
4146
4147
  "executions",
@@ -4149,8 +4150,10 @@ var GITGOV_DIRECTORIES = [
4149
4150
  ];
4150
4151
  var FsProjectInitializer = class {
4151
4152
  projectRoot;
4152
- constructor(projectRoot) {
4153
+ repoRoot;
4154
+ constructor(projectRoot, repoRoot) {
4153
4155
  this.projectRoot = projectRoot;
4156
+ this.repoRoot = repoRoot ?? projectRoot;
4154
4157
  }
4155
4158
  // ==================== IProjectInitializer Interface Methods ====================
4156
4159
  /**
@@ -4309,7 +4312,7 @@ var FsProjectInitializer = class {
4309
4312
  * Copies the @gitgov agent prompt to project root for IDE access.
4310
4313
  */
4311
4314
  async copyAgentPrompt() {
4312
- const targetPrompt = path9.join(this.projectRoot, "gitgov");
4315
+ const targetPrompt = path9.join(this.repoRoot, "gitgov");
4313
4316
  const potentialSources = [];
4314
4317
  potentialSources.push(
4315
4318
  path9.join(process.cwd(), "src/docs/generated/gitgov_agent.md")
@@ -4352,7 +4355,7 @@ var FsProjectInitializer = class {
4352
4355
  * Sets up .gitignore for GitGovernance files.
4353
4356
  */
4354
4357
  async setupGitIntegration() {
4355
- const gitignorePath = path9.join(this.projectRoot, ".gitignore");
4358
+ const gitignorePath = path9.join(this.repoRoot, ".gitignore");
4356
4359
  const gitignoreContent = `
4357
4360
  # GitGovernance
4358
4361
  # Ignore entire .gitgov/ directory (state lives in gitgov-state branch)
@@ -5633,6 +5636,25 @@ var ActorIdentityMismatchError = class _ActorIdentityMismatchError extends SyncS
5633
5636
  Object.setPrototypeOf(this, _ActorIdentityMismatchError.prototype);
5634
5637
  }
5635
5638
  };
5639
+ var WorktreeSetupError = class _WorktreeSetupError extends SyncStateError {
5640
+ constructor(reason, worktreePath, underlyingError) {
5641
+ super(`Failed to setup worktree at ${worktreePath}: ${reason}`);
5642
+ this.reason = reason;
5643
+ this.worktreePath = worktreePath;
5644
+ this.underlyingError = underlyingError;
5645
+ this.name = "WorktreeSetupError";
5646
+ Object.setPrototypeOf(this, _WorktreeSetupError.prototype);
5647
+ }
5648
+ };
5649
+ var RebaseAlreadyInProgressError = class _RebaseAlreadyInProgressError extends SyncStateError {
5650
+ constructor() {
5651
+ super(
5652
+ `A rebase is already in progress. Resolve the conflict with 'gitgov sync resolve --reason "..."' before pulling or pushing.`
5653
+ );
5654
+ this.name = "RebaseAlreadyInProgressError";
5655
+ Object.setPrototypeOf(this, _RebaseAlreadyInProgressError.prototype);
5656
+ }
5657
+ };
5636
5658
  var UncommittedChangesError = class _UncommittedChangesError extends SyncStateError {
5637
5659
  branch;
5638
5660
  constructor(branchName) {
@@ -5662,7 +5684,7 @@ var SYNC_ROOT_FILES = [
5662
5684
  var SYNC_ALLOWED_EXTENSIONS = [".json"];
5663
5685
  var SYNC_EXCLUDED_PATTERNS = [
5664
5686
  /\.key$/,
5665
- // Private keys (e.g., actors/*.key)
5687
+ // Private keys (e.g., keys/*.key)
5666
5688
  /\.backup$/,
5667
5689
  // Backup files from lint
5668
5690
  /\.backup-\d+$/,
@@ -6055,6 +6077,14 @@ gitgov
6055
6077
  throw error;
6056
6078
  }
6057
6079
  }
6080
+ /** Returns pending local changes not yet synced (delegates to calculateStateDelta) */
6081
+ async getPendingChanges() {
6082
+ try {
6083
+ return await this.calculateStateDelta("HEAD");
6084
+ } catch {
6085
+ return [];
6086
+ }
6087
+ }
6058
6088
  /**
6059
6089
  * Calculates the file delta in .gitgov/ between the current branch and gitgov-state.
6060
6090
  *
@@ -7571,6 +7601,790 @@ Signed-off-by: ${actorId}`;
7571
7601
  }
7572
7602
  };
7573
7603
 
7604
+ // src/sync_state/fs_worktree/fs_worktree_sync_state.types.ts
7605
+ var WORKTREE_DIR_NAME = ".gitgov-worktree";
7606
+ var DEFAULT_STATE_BRANCH = "gitgov-state";
7607
+ var logger7 = createLogger("[WorktreeSyncState] ");
7608
+ function shouldSyncFile2(filePath) {
7609
+ const fileName = path9__default.basename(filePath);
7610
+ const ext = path9__default.extname(filePath);
7611
+ if (!SYNC_ALLOWED_EXTENSIONS.includes(ext)) {
7612
+ return false;
7613
+ }
7614
+ for (const pattern of SYNC_EXCLUDED_PATTERNS) {
7615
+ if (pattern.test(fileName)) {
7616
+ return false;
7617
+ }
7618
+ }
7619
+ if (LOCAL_ONLY_FILES.includes(fileName)) {
7620
+ return false;
7621
+ }
7622
+ const normalizedPath = filePath.replace(/\\/g, "/");
7623
+ const parts = normalizedPath.split("/");
7624
+ const gitgovIndex = parts.findIndex((p) => p === ".gitgov");
7625
+ let relativeParts;
7626
+ if (gitgovIndex !== -1) {
7627
+ relativeParts = parts.slice(gitgovIndex + 1);
7628
+ } else {
7629
+ const syncDirIndex = parts.findIndex(
7630
+ (p) => SYNC_DIRECTORIES.includes(p)
7631
+ );
7632
+ if (syncDirIndex !== -1) {
7633
+ relativeParts = parts.slice(syncDirIndex);
7634
+ } else if (SYNC_ROOT_FILES.includes(fileName)) {
7635
+ return true;
7636
+ } else {
7637
+ return false;
7638
+ }
7639
+ }
7640
+ if (relativeParts.length === 1) {
7641
+ return SYNC_ROOT_FILES.includes(relativeParts[0]);
7642
+ } else if (relativeParts.length >= 2) {
7643
+ const dirName = relativeParts[0];
7644
+ return SYNC_DIRECTORIES.includes(dirName);
7645
+ }
7646
+ return false;
7647
+ }
7648
+ var FsWorktreeSyncStateModule = class {
7649
+ deps;
7650
+ repoRoot;
7651
+ stateBranchName;
7652
+ worktreePath;
7653
+ gitgovPath;
7654
+ constructor(deps, config) {
7655
+ if (!deps.git) throw new Error("GitModule is required for FsWorktreeSyncStateModule");
7656
+ if (!deps.config) throw new Error("ConfigManager is required for FsWorktreeSyncStateModule");
7657
+ if (!deps.identity) throw new Error("IdentityAdapter is required for FsWorktreeSyncStateModule");
7658
+ if (!deps.lint) throw new Error("LintModule is required for FsWorktreeSyncStateModule");
7659
+ if (!deps.indexer) throw new Error("IndexerAdapter is required for FsWorktreeSyncStateModule");
7660
+ if (!config.repoRoot) throw new Error("repoRoot is required");
7661
+ this.deps = deps;
7662
+ this.repoRoot = config.repoRoot;
7663
+ this.stateBranchName = config.stateBranchName ?? DEFAULT_STATE_BRANCH;
7664
+ this.worktreePath = config.worktreePath ?? path9__default.join(this.repoRoot, WORKTREE_DIR_NAME);
7665
+ this.gitgovPath = path9__default.join(this.worktreePath, ".gitgov");
7666
+ }
7667
+ // ═══════════════════════════════════════════════
7668
+ // Section A: Worktree Management (WTSYNC-A1..A6)
7669
+ // ═══════════════════════════════════════════════
7670
+ /** [WTSYNC-A4] Returns the worktree path */
7671
+ getWorktreePath() {
7672
+ return this.worktreePath;
7673
+ }
7674
+ /** [WTSYNC-A1..A6] Ensures worktree exists and is healthy */
7675
+ async ensureWorktree() {
7676
+ const health = await this.checkWorktreeHealth();
7677
+ if (health.healthy) {
7678
+ logger7.debug("Worktree is healthy");
7679
+ return;
7680
+ }
7681
+ if (health.exists && !health.healthy) {
7682
+ logger7.warn(`Worktree corrupted: ${health.error}. Recreating...`);
7683
+ await this.removeWorktree();
7684
+ }
7685
+ await this.ensureStateBranch();
7686
+ try {
7687
+ logger7.info(`Creating worktree at ${this.worktreePath}`);
7688
+ await this.execGit(["worktree", "add", this.worktreePath, this.stateBranchName]);
7689
+ } catch (error) {
7690
+ throw new WorktreeSetupError(
7691
+ "Failed to create worktree",
7692
+ this.worktreePath,
7693
+ error instanceof Error ? error : void 0
7694
+ );
7695
+ }
7696
+ }
7697
+ /** Check worktree health */
7698
+ async checkWorktreeHealth() {
7699
+ if (!existsSync(this.worktreePath)) {
7700
+ return { exists: false, healthy: false, path: this.worktreePath };
7701
+ }
7702
+ const gitFile = path9__default.join(this.worktreePath, ".git");
7703
+ if (!existsSync(gitFile)) {
7704
+ return {
7705
+ exists: true,
7706
+ healthy: false,
7707
+ path: this.worktreePath,
7708
+ error: ".git file missing in worktree"
7709
+ };
7710
+ }
7711
+ try {
7712
+ const branch = (await this.execGit(["-C", this.worktreePath, "rev-parse", "--abbrev-ref", "HEAD"])).trim();
7713
+ if (branch !== this.stateBranchName) {
7714
+ return {
7715
+ exists: true,
7716
+ healthy: false,
7717
+ path: this.worktreePath,
7718
+ error: `Wrong branch: ${branch}, expected ${this.stateBranchName}`
7719
+ };
7720
+ }
7721
+ } catch {
7722
+ return {
7723
+ exists: true,
7724
+ healthy: false,
7725
+ path: this.worktreePath,
7726
+ error: "Cannot determine branch"
7727
+ };
7728
+ }
7729
+ return { exists: true, healthy: true, path: this.worktreePath };
7730
+ }
7731
+ /** Remove worktree cleanly */
7732
+ async removeWorktree() {
7733
+ try {
7734
+ await this.execGit(["worktree", "remove", this.worktreePath, "--force"]);
7735
+ } catch {
7736
+ await promises.rm(this.worktreePath, { recursive: true, force: true });
7737
+ await this.execGit(["worktree", "prune"]);
7738
+ }
7739
+ }
7740
+ // ═══════════════════════════════════════════════
7741
+ // Section B: Push Operations (WTSYNC-B1..B14)
7742
+ // ═══════════════════════════════════════════════
7743
+ /** [WTSYNC-B1..B14] Push local state to remote */
7744
+ async pushState(options) {
7745
+ const { actorId, dryRun = false, force = false } = options;
7746
+ const log = (msg) => logger7.debug(`[pushState] ${msg}`);
7747
+ if (await this.isRebaseInProgress()) {
7748
+ throw new RebaseAlreadyInProgressError();
7749
+ }
7750
+ const currentActor = await this.deps.identity.getCurrentActor();
7751
+ if (currentActor.id !== actorId) {
7752
+ throw new ActorIdentityMismatchError(actorId, currentActor.id);
7753
+ }
7754
+ await this.ensureWorktree();
7755
+ const lintReport = await this.deps.lint.lint();
7756
+ if (lintReport.summary.errors > 0) {
7757
+ return {
7758
+ success: false,
7759
+ filesSynced: 0,
7760
+ sourceBranch: options.sourceBranch ?? "current",
7761
+ commitHash: null,
7762
+ commitMessage: null,
7763
+ conflictDetected: false,
7764
+ error: `Lint validation failed: ${lintReport.summary.errors} error(s)`
7765
+ };
7766
+ }
7767
+ const rawDelta = await this.calculateFileDelta();
7768
+ const delta = rawDelta.filter((f) => shouldSyncFile2(f.file));
7769
+ log(`Delta: ${delta.length} syncable files (${rawDelta.length} total)`);
7770
+ if (delta.length === 0) {
7771
+ return {
7772
+ success: true,
7773
+ filesSynced: 0,
7774
+ sourceBranch: options.sourceBranch ?? "current",
7775
+ commitHash: null,
7776
+ commitMessage: null,
7777
+ conflictDetected: false
7778
+ };
7779
+ }
7780
+ if (dryRun) {
7781
+ return {
7782
+ success: true,
7783
+ filesSynced: delta.length,
7784
+ sourceBranch: options.sourceBranch ?? "current",
7785
+ commitHash: null,
7786
+ commitMessage: "[dry-run] Would commit changes",
7787
+ conflictDetected: false
7788
+ };
7789
+ }
7790
+ const stagedCount = await this.stageSyncableFiles(delta, log);
7791
+ const commitMessage = `gitgov: sync state [actor:${actorId}]`;
7792
+ await this.execInWorktree(["commit", "-m", commitMessage]);
7793
+ const commitHash = (await this.execInWorktree(["rev-parse", "HEAD"])).trim();
7794
+ log(`Committed: ${commitHash}`);
7795
+ let implicitPull;
7796
+ let remoteBranchExists = false;
7797
+ if (!force) {
7798
+ try {
7799
+ await this.execGit(["ls-remote", "--exit-code", "origin", this.stateBranchName]);
7800
+ remoteBranchExists = true;
7801
+ } catch {
7802
+ }
7803
+ }
7804
+ if (!force && remoteBranchExists) {
7805
+ try {
7806
+ const beforeHash = commitHash;
7807
+ await this.execInWorktree(["pull", "--rebase", "origin", this.stateBranchName]);
7808
+ const afterHash = (await this.execInWorktree(["rev-parse", "HEAD"])).trim();
7809
+ if (beforeHash !== afterHash) {
7810
+ let filesUpdated = 0;
7811
+ try {
7812
+ const diffOutput = await this.execInWorktree([
7813
+ "diff",
7814
+ "--name-only",
7815
+ beforeHash,
7816
+ afterHash,
7817
+ "--",
7818
+ ".gitgov/"
7819
+ ]);
7820
+ filesUpdated = diffOutput.trim().split("\n").filter(Boolean).length;
7821
+ } catch {
7822
+ }
7823
+ implicitPull = {
7824
+ hasChanges: true,
7825
+ filesUpdated,
7826
+ reindexed: true
7827
+ };
7828
+ await this.reindex();
7829
+ log(`Implicit pull: ${filesUpdated} files updated`);
7830
+ }
7831
+ } catch {
7832
+ const affectedFiles = await this.getConflictedFiles();
7833
+ return {
7834
+ success: false,
7835
+ filesSynced: stagedCount,
7836
+ sourceBranch: options.sourceBranch ?? "current",
7837
+ commitHash,
7838
+ commitMessage,
7839
+ conflictDetected: true,
7840
+ conflictInfo: {
7841
+ type: "rebase_conflict",
7842
+ affectedFiles,
7843
+ message: "Rebase conflict detected during push reconciliation",
7844
+ resolutionSteps: [
7845
+ `Edit conflicted files in ${this.worktreePath}/.gitgov/`,
7846
+ 'Run `gitgov sync resolve --reason "..."` to finalize'
7847
+ ]
7848
+ },
7849
+ error: "Rebase conflict during push"
7850
+ };
7851
+ }
7852
+ }
7853
+ await this.execInWorktree(["push", "origin", this.stateBranchName]);
7854
+ log("Pushed to remote");
7855
+ const finalHash = (await this.execInWorktree(["rev-parse", "HEAD"])).trim();
7856
+ const result = {
7857
+ success: true,
7858
+ filesSynced: stagedCount,
7859
+ sourceBranch: options.sourceBranch ?? "current",
7860
+ commitHash: finalHash,
7861
+ commitMessage,
7862
+ conflictDetected: false
7863
+ };
7864
+ if (implicitPull) {
7865
+ result.implicitPull = implicitPull;
7866
+ }
7867
+ return result;
7868
+ }
7869
+ // ═══════════════════════════════════════════════
7870
+ // Section C: Pull Operations (WTSYNC-C1..C8)
7871
+ // ═══════════════════════════════════════════════
7872
+ /** [WTSYNC-C1..C8] Pull remote state */
7873
+ async pullState(options) {
7874
+ const { forceReindex = false, force = false } = options ?? {};
7875
+ const log = (msg) => logger7.debug(`[pullState] ${msg}`);
7876
+ if (await this.isRebaseInProgress()) {
7877
+ throw new RebaseAlreadyInProgressError();
7878
+ }
7879
+ await this.ensureWorktree();
7880
+ if (!force) {
7881
+ const statusRaw = await this.execInWorktree(["status", "--porcelain", "-uall", ".gitgov/"]);
7882
+ const statusLines = statusRaw.split("\n").filter((line) => line.length >= 4);
7883
+ const syncableChanges = statusLines.filter((l) => shouldSyncFile2(l.slice(3)));
7884
+ if (syncableChanges.length > 0) {
7885
+ log(`Auto-committing ${syncableChanges.length} local changes before pull`);
7886
+ for (const line of syncableChanges) {
7887
+ const filePath = line.slice(3);
7888
+ await this.execInWorktree(["add", "-f", "--", filePath]);
7889
+ }
7890
+ await this.execInWorktree(["commit", "-m", "state: Auto-commit local changes before pull"]);
7891
+ }
7892
+ }
7893
+ if (force) {
7894
+ try {
7895
+ await this.execInWorktree(["checkout", ".gitgov/"]);
7896
+ await this.execInWorktree([
7897
+ "clean",
7898
+ "-fd",
7899
+ ...LOCAL_ONLY_FILES.flatMap((f) => ["-e", f]),
7900
+ "-e",
7901
+ "*.key",
7902
+ "-e",
7903
+ "*.backup",
7904
+ "-e",
7905
+ "*.backup-*",
7906
+ "-e",
7907
+ "*.tmp",
7908
+ "-e",
7909
+ "*.bak",
7910
+ ".gitgov/"
7911
+ ]);
7912
+ log("Force: discarded local changes");
7913
+ } catch {
7914
+ }
7915
+ }
7916
+ try {
7917
+ await this.execInWorktree(["fetch", "origin", this.stateBranchName]);
7918
+ } catch {
7919
+ log("Fetch failed (possibly no remote configured)");
7920
+ }
7921
+ const localHash = (await this.execInWorktree(["rev-parse", "HEAD"])).trim();
7922
+ let remoteHash;
7923
+ try {
7924
+ remoteHash = (await this.execInWorktree(["rev-parse", `origin/${this.stateBranchName}`])).trim();
7925
+ } catch {
7926
+ if (forceReindex) {
7927
+ await this.reindex();
7928
+ return { success: true, hasChanges: false, filesUpdated: 0, reindexed: true, conflictDetected: false };
7929
+ }
7930
+ return { success: true, hasChanges: false, filesUpdated: 0, reindexed: false, conflictDetected: false };
7931
+ }
7932
+ if (localHash === remoteHash && !forceReindex) {
7933
+ log("No remote changes");
7934
+ return { success: true, hasChanges: false, filesUpdated: 0, reindexed: false, conflictDetected: false };
7935
+ }
7936
+ if (localHash !== remoteHash) {
7937
+ try {
7938
+ await this.execInWorktree(["pull", "--rebase", "origin", this.stateBranchName]);
7939
+ } catch {
7940
+ const affectedFiles = await this.getConflictedFiles();
7941
+ return {
7942
+ success: false,
7943
+ hasChanges: true,
7944
+ filesUpdated: 0,
7945
+ reindexed: false,
7946
+ conflictDetected: true,
7947
+ conflictInfo: {
7948
+ type: "rebase_conflict",
7949
+ affectedFiles,
7950
+ message: "Rebase conflict detected during pull",
7951
+ resolutionSteps: [
7952
+ `Edit conflicted files in ${this.worktreePath}/.gitgov/`,
7953
+ 'Run `gitgov sync resolve --reason "..."` to finalize'
7954
+ ]
7955
+ },
7956
+ error: "Rebase conflict during pull"
7957
+ };
7958
+ }
7959
+ }
7960
+ let filesUpdated = 0;
7961
+ try {
7962
+ const diffOutput = await this.execInWorktree([
7963
+ "diff",
7964
+ "--name-only",
7965
+ localHash,
7966
+ "HEAD",
7967
+ "--",
7968
+ ".gitgov/"
7969
+ ]);
7970
+ filesUpdated = diffOutput.trim().split("\n").filter(Boolean).length;
7971
+ } catch {
7972
+ }
7973
+ await this.reindex();
7974
+ log(`Pulled: ${filesUpdated} files updated`);
7975
+ return {
7976
+ success: true,
7977
+ hasChanges: true,
7978
+ filesUpdated,
7979
+ reindexed: true,
7980
+ conflictDetected: false
7981
+ };
7982
+ }
7983
+ // ═══════════════════════════════════════════════
7984
+ // Section D: Resolve Operations (WTSYNC-D1..D7)
7985
+ // ═══════════════════════════════════════════════
7986
+ /** [WTSYNC-D1..D7] Resolve rebase conflict */
7987
+ async resolveConflict(options) {
7988
+ const { reason, actorId } = options;
7989
+ if (!await this.isRebaseInProgress()) {
7990
+ throw new NoRebaseInProgressError();
7991
+ }
7992
+ const currentActor = await this.deps.identity.getCurrentActor();
7993
+ if (currentActor.id !== actorId) {
7994
+ throw new ActorIdentityMismatchError(actorId, currentActor.id);
7995
+ }
7996
+ const conflictedFiles = await this.getConflictedFiles();
7997
+ const gitgovConflictFiles = conflictedFiles.filter((f) => f.startsWith(".gitgov/")).map((f) => f.replace(/^\.gitgov\//, ""));
7998
+ const markers = await this.checkConflictMarkers(
7999
+ gitgovConflictFiles.length > 0 ? gitgovConflictFiles : conflictedFiles
8000
+ );
8001
+ if (markers.length > 0) {
8002
+ throw new ConflictMarkersPresentError(markers);
8003
+ }
8004
+ await this.resignResolvedRecords(gitgovConflictFiles, actorId, reason);
8005
+ await this.execInWorktree(["add", ".gitgov/"]);
8006
+ await this.execInWorktree(["rebase", "--continue"], { env: { GIT_EDITOR: "true" } });
8007
+ const rebaseCommitHash = (await this.execInWorktree(["rev-parse", "HEAD"])).trim();
8008
+ const resolutionMessage = `gitgov: resolve conflict [actor:${actorId}] reason: ${reason}`;
8009
+ await this.execInWorktree(["commit", "--allow-empty", "-m", resolutionMessage]);
8010
+ const resolutionCommitHash = (await this.execInWorktree(["rev-parse", "HEAD"])).trim();
8011
+ await this.execInWorktree(["push", "origin", this.stateBranchName]);
8012
+ await this.reindex();
8013
+ return {
8014
+ success: true,
8015
+ rebaseCommitHash,
8016
+ resolutionCommitHash,
8017
+ conflictsResolved: conflictedFiles.length,
8018
+ resolvedBy: actorId,
8019
+ reason
8020
+ };
8021
+ }
8022
+ // ═══════════════════════════════════════════════
8023
+ // Section E: Integrity and Audit (WTSYNC-E1..E8)
8024
+ // ═══════════════════════════════════════════════
8025
+ /** [WTSYNC-E8] Get configured state branch name */
8026
+ async getStateBranchName() {
8027
+ return this.stateBranchName;
8028
+ }
8029
+ /** [WTSYNC-A5/A6] Ensure state branch exists */
8030
+ async ensureStateBranch() {
8031
+ try {
8032
+ await this.execGit(["rev-parse", "--verify", this.stateBranchName]);
8033
+ return;
8034
+ } catch {
8035
+ }
8036
+ try {
8037
+ await this.execGit(["rev-parse", "--verify", `origin/${this.stateBranchName}`]);
8038
+ await this.execGit(["branch", this.stateBranchName, `origin/${this.stateBranchName}`]);
8039
+ return;
8040
+ } catch {
8041
+ }
8042
+ try {
8043
+ const emptyTree = (await this.execGit(["hash-object", "-t", "tree", "/dev/null"])).trim();
8044
+ const commitHash = (await this.execGit([
8045
+ "commit-tree",
8046
+ emptyTree,
8047
+ "-m",
8048
+ "gitgov: initialize state branch"
8049
+ ])).trim();
8050
+ await this.execGit(["update-ref", `refs/heads/${this.stateBranchName}`, commitHash]);
8051
+ } catch (error) {
8052
+ throw new StateBranchSetupError(
8053
+ "Failed to create orphan state branch",
8054
+ error instanceof Error ? error : void 0
8055
+ );
8056
+ }
8057
+ }
8058
+ /** Returns pending syncable changes not yet pushed (filters by shouldSyncFile) */
8059
+ async getPendingChanges() {
8060
+ await this.ensureWorktree();
8061
+ const allChanges = await this.calculateFileDelta();
8062
+ return allChanges.filter((f) => shouldSyncFile2(f.file));
8063
+ }
8064
+ /** Calculate delta between source and worktree state branch */
8065
+ async calculateStateDelta(_sourceBranch) {
8066
+ await this.ensureWorktree();
8067
+ try {
8068
+ const diff = await this.execInWorktree([
8069
+ "diff",
8070
+ "--name-status",
8071
+ `origin/${this.stateBranchName}`,
8072
+ "HEAD",
8073
+ "--",
8074
+ ".gitgov/"
8075
+ ]);
8076
+ return this.parseDiffOutput(diff);
8077
+ } catch {
8078
+ return [];
8079
+ }
8080
+ }
8081
+ /** [WTSYNC-E6] Check if rebase is in progress in worktree */
8082
+ async isRebaseInProgress() {
8083
+ try {
8084
+ const gitContent = await promises.readFile(path9__default.join(this.worktreePath, ".git"), "utf8");
8085
+ const gitDir = gitContent.replace("gitdir: ", "").trim();
8086
+ const resolvedGitDir = path9__default.resolve(this.worktreePath, gitDir);
8087
+ return existsSync(path9__default.join(resolvedGitDir, "rebase-merge")) || existsSync(path9__default.join(resolvedGitDir, "rebase-apply"));
8088
+ } catch {
8089
+ return false;
8090
+ }
8091
+ }
8092
+ /** Check for conflict markers in files */
8093
+ async checkConflictMarkers(filePaths) {
8094
+ const filesWithMarkers = [];
8095
+ for (const filePath of filePaths) {
8096
+ const fullPath = path9__default.join(this.gitgovPath, filePath);
8097
+ try {
8098
+ const content = await promises.readFile(fullPath, "utf8");
8099
+ if (content.includes("<<<<<<<") || content.includes(">>>>>>>")) {
8100
+ filesWithMarkers.push(filePath);
8101
+ }
8102
+ } catch {
8103
+ }
8104
+ }
8105
+ return filesWithMarkers;
8106
+ }
8107
+ /** [WTSYNC-E7] Get structured conflict diff */
8108
+ async getConflictDiff(filePaths) {
8109
+ const files = filePaths ?? await this.getConflictedFiles();
8110
+ const diffFiles = [];
8111
+ for (const file of files) {
8112
+ const fullPath = path9__default.join(this.worktreePath, file);
8113
+ try {
8114
+ const content = await promises.readFile(fullPath, "utf8");
8115
+ let localContent = "";
8116
+ let remoteContent = "";
8117
+ let baseContent = null;
8118
+ const lines = content.split("\n");
8119
+ let section = "none";
8120
+ for (const line of lines) {
8121
+ if (line.startsWith("<<<<<<<")) {
8122
+ section = "local";
8123
+ } else if (line.startsWith("|||||||")) {
8124
+ section = "base";
8125
+ baseContent = "";
8126
+ } else if (line.startsWith("=======")) {
8127
+ section = "remote";
8128
+ } else if (line.startsWith(">>>>>>>")) {
8129
+ section = "none";
8130
+ } else {
8131
+ switch (section) {
8132
+ case "local":
8133
+ localContent += line + "\n";
8134
+ break;
8135
+ case "base":
8136
+ baseContent = (baseContent ?? "") + line + "\n";
8137
+ break;
8138
+ case "remote":
8139
+ remoteContent += line + "\n";
8140
+ break;
8141
+ }
8142
+ }
8143
+ }
8144
+ diffFiles.push({
8145
+ filePath: file,
8146
+ localContent,
8147
+ remoteContent,
8148
+ baseContent
8149
+ });
8150
+ } catch {
8151
+ }
8152
+ }
8153
+ return {
8154
+ files: diffFiles,
8155
+ message: `${files.length} file(s) in conflict`,
8156
+ resolutionSteps: [
8157
+ "Edit files to resolve conflicts",
8158
+ 'Run `gitgov sync resolve --reason "..."` to finalize'
8159
+ ]
8160
+ };
8161
+ }
8162
+ /** [WTSYNC-E1] Verify resolution integrity */
8163
+ async verifyResolutionIntegrity() {
8164
+ await this.ensureWorktree();
8165
+ const violations = [];
8166
+ try {
8167
+ const logOutput = await this.execInWorktree([
8168
+ "log",
8169
+ "--format=%H|%s|%ai|%an",
8170
+ this.stateBranchName
8171
+ ]);
8172
+ const commits = logOutput.trim().split("\n").filter(Boolean).map((line) => {
8173
+ const parts = line.split("|");
8174
+ return {
8175
+ hash: parts[0] ?? "",
8176
+ subject: parts[1] ?? "",
8177
+ date: parts[2] ?? "",
8178
+ author: parts[3] ?? ""
8179
+ };
8180
+ });
8181
+ for (let i = 0; i < commits.length; i++) {
8182
+ const commit = commits[i];
8183
+ if (commit.subject.includes("rebase") && !commit.subject.includes("resolve")) {
8184
+ const nextCommit = commits[i - 1];
8185
+ if (!nextCommit || !nextCommit.subject.includes("resolve")) {
8186
+ violations.push({
8187
+ rebaseCommitHash: commit.hash,
8188
+ commitMessage: commit.subject,
8189
+ timestamp: commit.date,
8190
+ author: commit.author
8191
+ });
8192
+ }
8193
+ }
8194
+ }
8195
+ } catch {
8196
+ }
8197
+ return violations;
8198
+ }
8199
+ /** [WTSYNC-E1..E5] Audit state */
8200
+ async auditState(options) {
8201
+ const {
8202
+ scope = "all",
8203
+ verifySignatures: verifySignatures2 = true,
8204
+ verifyChecksums = true,
8205
+ verifyExpectedFiles = true,
8206
+ expectedFilesScope,
8207
+ filePaths
8208
+ } = options ?? {};
8209
+ if (expectedFilesScope === "all-commits") {
8210
+ logger7.debug('expectedFilesScope "all-commits" treated as "head" in worktree module');
8211
+ }
8212
+ await this.ensureWorktree();
8213
+ const integrityViolations = await this.verifyResolutionIntegrity();
8214
+ let lintReport;
8215
+ if (verifySignatures2 || verifyChecksums) {
8216
+ lintReport = await this.deps.lint.lint({
8217
+ validateChecksums: verifyChecksums,
8218
+ validateSignatures: verifySignatures2,
8219
+ validateReferences: false,
8220
+ concurrent: true
8221
+ });
8222
+ }
8223
+ if (verifyExpectedFiles) {
8224
+ if (filePaths && filePaths.length > 0) {
8225
+ for (const fp of filePaths) {
8226
+ if (!existsSync(path9__default.join(this.gitgovPath, fp))) {
8227
+ integrityViolations.push({
8228
+ rebaseCommitHash: "",
8229
+ commitMessage: `Missing expected file: ${fp}`,
8230
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8231
+ author: ""
8232
+ });
8233
+ }
8234
+ }
8235
+ } else {
8236
+ const expectedDirs = ["tasks", "cycles", "actors"];
8237
+ for (const dir of expectedDirs) {
8238
+ if (!existsSync(path9__default.join(this.gitgovPath, dir))) {
8239
+ integrityViolations.push({
8240
+ rebaseCommitHash: "",
8241
+ commitMessage: `Missing expected directory: ${dir}`,
8242
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8243
+ author: ""
8244
+ });
8245
+ }
8246
+ }
8247
+ if (!existsSync(path9__default.join(this.gitgovPath, "config.json"))) {
8248
+ integrityViolations.push({
8249
+ rebaseCommitHash: "",
8250
+ commitMessage: "Missing expected file: config.json",
8251
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8252
+ author: ""
8253
+ });
8254
+ }
8255
+ }
8256
+ }
8257
+ let totalCommits = 0;
8258
+ let rebaseCommits = 0;
8259
+ let resolutionCommits = 0;
8260
+ try {
8261
+ const logOutput = await this.execInWorktree(["log", "--oneline", this.stateBranchName]);
8262
+ const lines = logOutput.trim().split("\n").filter(Boolean);
8263
+ totalCommits = lines.length;
8264
+ rebaseCommits = lines.filter((l) => l.includes("rebase")).length;
8265
+ resolutionCommits = lines.filter((l) => l.includes("resolve")).length;
8266
+ } catch {
8267
+ }
8268
+ const passed = integrityViolations.length === 0 && (lintReport ? lintReport.summary.errors === 0 : true);
8269
+ const report = {
8270
+ passed,
8271
+ scope,
8272
+ totalCommits,
8273
+ rebaseCommits,
8274
+ resolutionCommits,
8275
+ integrityViolations,
8276
+ summary: passed ? `Audit passed: ${totalCommits} commits, no violations` : `Audit failed: ${integrityViolations.length} integrity violations`
8277
+ };
8278
+ if (lintReport) {
8279
+ report.lintReport = lintReport;
8280
+ }
8281
+ return report;
8282
+ }
8283
+ // ═══════════════════════════════════════════════
8284
+ // Private Helpers
8285
+ // ═══════════════════════════════════════════════
8286
+ /** Execute git command in repo root (throws on non-zero exit) */
8287
+ async execGit(args, options) {
8288
+ const result = await this.deps.git.exec("git", args, options);
8289
+ if (result.exitCode !== 0) {
8290
+ throw new Error(`Git command failed (exit ${result.exitCode}): git ${args.join(" ")} => ${result.stderr}`);
8291
+ }
8292
+ return result.stdout;
8293
+ }
8294
+ /** Execute git command in worktree context (throws on non-zero exit) */
8295
+ async execInWorktree(args, options) {
8296
+ return this.execGit(["-C", this.worktreePath, ...args], options);
8297
+ }
8298
+ /** Calculate file delta (uncommitted changes in worktree) */
8299
+ async calculateFileDelta() {
8300
+ try {
8301
+ const status = await this.execInWorktree([
8302
+ "status",
8303
+ "--porcelain",
8304
+ "-uall",
8305
+ "--ignored=traditional",
8306
+ ".gitgov/"
8307
+ ]);
8308
+ return this.parseStatusOutput(status);
8309
+ } catch {
8310
+ return [];
8311
+ }
8312
+ }
8313
+ /** [WTSYNC-B4/B9/B10/B11] Stage only syncable files from delta (adds, mods, and deletions) */
8314
+ async stageSyncableFiles(delta, log) {
8315
+ let stagedCount = 0;
8316
+ for (const file of delta) {
8317
+ if (!shouldSyncFile2(file.file)) {
8318
+ log(`Skipped (not syncable): ${file.file}`);
8319
+ continue;
8320
+ }
8321
+ if (file.status === "D") {
8322
+ await this.execInWorktree(["rm", "--", file.file]);
8323
+ } else {
8324
+ await this.execInWorktree(["add", "-f", "--", file.file]);
8325
+ }
8326
+ log(`Staged (${file.status}): ${file.file}`);
8327
+ stagedCount++;
8328
+ }
8329
+ return stagedCount;
8330
+ }
8331
+ /** Get list of conflicted files during rebase */
8332
+ async getConflictedFiles() {
8333
+ try {
8334
+ const status = await this.execInWorktree(["diff", "--name-only", "--diff-filter=U"]);
8335
+ return status.trim().split("\n").filter(Boolean);
8336
+ } catch {
8337
+ return [];
8338
+ }
8339
+ }
8340
+ /** Re-sign records after conflict resolution */
8341
+ async resignResolvedRecords(filePaths, actorId, reason) {
8342
+ for (const filePath of filePaths) {
8343
+ const fullPath = path9__default.join(this.gitgovPath, filePath);
8344
+ try {
8345
+ const content = await promises.readFile(fullPath, "utf8");
8346
+ const record = JSON.parse(content);
8347
+ const signedRecord = await this.deps.identity.signRecord(record, actorId, "resolver", reason);
8348
+ await promises.writeFile(fullPath, JSON.stringify(signedRecord, null, 2));
8349
+ } catch {
8350
+ }
8351
+ }
8352
+ }
8353
+ /** Re-index records from worktree */
8354
+ async reindex() {
8355
+ try {
8356
+ await this.deps.indexer.generateIndex();
8357
+ } catch {
8358
+ logger7.warn("Re-index failed");
8359
+ }
8360
+ }
8361
+ /** Parse git diff --name-status output */
8362
+ parseDiffOutput(diff) {
8363
+ return diff.trim().split("\n").filter(Boolean).map((line) => {
8364
+ const parts = line.split(" ");
8365
+ const status = parts[0];
8366
+ const file = parts[1] || parts[0].slice(2);
8367
+ return { status, file };
8368
+ });
8369
+ }
8370
+ /** Parse git status --porcelain output */
8371
+ parseStatusOutput(status) {
8372
+ return status.split("\n").filter((line) => line.length >= 4).map((line) => {
8373
+ const xy = line.slice(0, 2);
8374
+ const file = line.slice(3);
8375
+ let statusChar;
8376
+ if (xy.includes("?") || xy.includes("!")) {
8377
+ statusChar = "A";
8378
+ } else if (xy.includes("D")) {
8379
+ statusChar = "D";
8380
+ } else {
8381
+ statusChar = "M";
8382
+ }
8383
+ return { status: statusChar, file };
8384
+ });
8385
+ }
8386
+ };
8387
+
7574
8388
  // src/watcher_state/watcher_state.errors.ts
7575
8389
  var WatcherStateError = class _WatcherStateError extends Error {
7576
8390
  constructor(message) {
@@ -8497,6 +9311,6 @@ var FsRecordProjection = class {
8497
9311
  }
8498
9312
  };
8499
9313
 
8500
- export { DEFAULT_ID_ENCODER, FsAgentRunner, FsConfigStore, FsFileLister, FsKeyProvider, FsLintModule, FsProjectInitializer, FsRecordProjection, FsRecordStore, FsSessionStore, FsSyncStateModule, FsWatcherStateModule, LocalGitModule as GitModule, LocalGitModule, createAgentRunner, createConfigManager, createSessionManager, findGitgovRoot, findProjectRoot, getGitgovPath, isGitgovProject, resetDiscoveryCache };
9314
+ 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 };
8501
9315
  //# sourceMappingURL=fs.js.map
8502
9316
  //# sourceMappingURL=fs.js.map