@gitgov/core 1.8.0 → 1.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/index.js CHANGED
@@ -1388,6 +1388,29 @@ var execution_record_schema_default = {
1388
1388
  },
1389
1389
  default: [],
1390
1390
  description: "Optional list of typed references to relevant commits, files, PRs, or external documents.\nShould use typed prefixes for clarity and trazabilidad (see execution_protocol_appendix.md):\n- commit: Git commit SHA\n- pr: Pull Request number\n- file: File path (relative to repo root)\n- url: External URL\n- issue: GitHub Issue number\n- task: TaskRecord ID\n- exec: ExecutionRecord ID (for corrections or dependencies)\n- changelog: ChangelogRecord ID\n"
1391
+ },
1392
+ metadata: {
1393
+ type: "object",
1394
+ additionalProperties: true,
1395
+ description: "Optional structured data for machine consumption.\nUse this field for data that needs to be programmatically processed (e.g., audit findings,\nperformance metrics, scan results). This complements result (human-readable WHAT) and\nnotes (narrative HOW/WHY) by providing structured, queryable data.\nCommon use cases: audit findings arrays, performance metrics, tool outputs, scan summaries.\n",
1396
+ examples: [
1397
+ {
1398
+ findings: [
1399
+ {
1400
+ type: "PII",
1401
+ file: "src/user.ts",
1402
+ line: 42
1403
+ }
1404
+ ],
1405
+ scannedFiles: 245
1406
+ },
1407
+ {
1408
+ metrics: {
1409
+ duration_ms: 1250,
1410
+ memory_mb: 512
1411
+ }
1412
+ }
1413
+ ]
1391
1414
  }
1392
1415
  },
1393
1416
  examples: [
@@ -1460,6 +1483,52 @@ var execution_record_schema_default = {
1460
1483
  references: [
1461
1484
  "exec:1752275500-exec-refactor-queries"
1462
1485
  ]
1486
+ },
1487
+ {
1488
+ id: "1752276000-exec-gdpr-audit-scan",
1489
+ taskId: "1752274500-task-gdpr-compliance",
1490
+ type: "analysis",
1491
+ title: "GDPR Audit Scan - 2025-01-15",
1492
+ result: "Escaneados 245 archivos. Encontrados 10 findings (3 critical, 4 high, 3 medium). Ver metadata para detalles estructurados.",
1493
+ notes: "Scan ejecutado con RegexDetector + HeuristicDetector. LLM calls: 0 (tier free).",
1494
+ references: [
1495
+ "file:src/config/db.ts",
1496
+ "file:src/auth/keys.ts"
1497
+ ],
1498
+ metadata: {
1499
+ scannedFiles: 245,
1500
+ scannedLines: 18420,
1501
+ duration_ms: 1250,
1502
+ findings: [
1503
+ {
1504
+ id: "SEC-001",
1505
+ severity: "critical",
1506
+ file: "src/config/db.ts",
1507
+ line: 5,
1508
+ type: "api_key"
1509
+ },
1510
+ {
1511
+ id: "SEC-003",
1512
+ severity: "critical",
1513
+ file: "src/auth/keys.ts",
1514
+ line: 2,
1515
+ type: "private_key"
1516
+ },
1517
+ {
1518
+ id: "PII-003",
1519
+ severity: "critical",
1520
+ file: "src/payments/stripe.ts",
1521
+ line: 8,
1522
+ type: "credit_card"
1523
+ }
1524
+ ],
1525
+ summary: {
1526
+ critical: 3,
1527
+ high: 4,
1528
+ medium: 3,
1529
+ low: 0
1530
+ }
1531
+ }
1463
1532
  }
1464
1533
  ]
1465
1534
  };
@@ -3000,12 +3069,61 @@ var ConfigManager = class _ConfigManager {
3000
3069
  }
3001
3070
  /**
3002
3071
  * Load GitGovernance session state
3072
+ * [EARS-53] Auto-detects actor from .key files if no session or no actorId exists
3003
3073
  */
3004
3074
  async loadSession() {
3005
3075
  try {
3006
3076
  const sessionContent = await promises.readFile(this.sessionPath, "utf-8");
3007
- return JSON.parse(sessionContent);
3077
+ const session = JSON.parse(sessionContent);
3078
+ if (!session.lastSession?.actorId) {
3079
+ const detectedActorId = await this.detectActorFromKeyFiles();
3080
+ if (detectedActorId) {
3081
+ session.lastSession = {
3082
+ actorId: detectedActorId,
3083
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3084
+ };
3085
+ await promises.writeFile(this.sessionPath, JSON.stringify(session, null, 2), "utf-8");
3086
+ }
3087
+ }
3088
+ return session;
3008
3089
  } catch (error) {
3090
+ const detectedActorId = await this.detectActorFromKeyFiles();
3091
+ if (detectedActorId) {
3092
+ const newSession = {
3093
+ lastSession: {
3094
+ actorId: detectedActorId,
3095
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3096
+ },
3097
+ actorState: {}
3098
+ };
3099
+ try {
3100
+ await promises.writeFile(this.sessionPath, JSON.stringify(newSession, null, 2), "utf-8");
3101
+ return newSession;
3102
+ } catch {
3103
+ return newSession;
3104
+ }
3105
+ }
3106
+ return null;
3107
+ }
3108
+ }
3109
+ /**
3110
+ * [EARS-53] Detect actor from .key files in .gitgov/actors/
3111
+ * Returns the actor ID if exactly one .key file exists, or the first one if multiple exist.
3112
+ * Private keys (.key files) indicate which actors can sign on this machine.
3113
+ */
3114
+ async detectActorFromKeyFiles() {
3115
+ try {
3116
+ const gitgovDir = path6.dirname(this.sessionPath);
3117
+ const actorsDir = path6.join(gitgovDir, "actors");
3118
+ const files = await promises.readdir(actorsDir);
3119
+ const keyFiles = files.filter((f) => f.endsWith(".key"));
3120
+ const firstKeyFile = keyFiles[0];
3121
+ if (!firstKeyFile) {
3122
+ return null;
3123
+ }
3124
+ const actorId = firstKeyFile.replace(".key", "");
3125
+ return actorId;
3126
+ } catch {
3009
3127
  return null;
3010
3128
  }
3011
3129
  }
@@ -3598,7 +3716,7 @@ var IdentityAdapter = class {
3598
3716
  }
3599
3717
  return actors;
3600
3718
  }
3601
- async signRecord(record, actorId, role) {
3719
+ async signRecord(record, actorId, role, notes) {
3602
3720
  const actor = await this.getActor(actorId);
3603
3721
  if (!actor) {
3604
3722
  throw new Error(`Actor not found: ${actorId}`);
@@ -3617,12 +3735,12 @@ var IdentityAdapter = class {
3617
3735
  }
3618
3736
  let signature;
3619
3737
  if (privateKey) {
3620
- signature = signPayload(record.payload, privateKey, actorId, role, "Record signed");
3738
+ signature = signPayload(record.payload, privateKey, actorId, role, notes);
3621
3739
  } else {
3622
3740
  signature = {
3623
3741
  keyId: actorId,
3624
3742
  role,
3625
- notes: "Record signed",
3743
+ notes,
3626
3744
  signature: `mock-signature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
3627
3745
  timestamp: Math.floor(Date.now() / 1e3)
3628
3746
  };
@@ -4069,7 +4187,7 @@ var FeedbackAdapter = class {
4069
4187
  },
4070
4188
  payload: validatedPayload
4071
4189
  };
4072
- const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
4190
+ const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Feedback record created");
4073
4191
  await this.feedbackStore.write(signedRecord);
4074
4192
  this.eventBus.publish({
4075
4193
  type: "feedback.created",
@@ -4271,7 +4389,7 @@ function createExecutionRecord(payload) {
4271
4389
  title: payload.title,
4272
4390
  notes: payload.notes,
4273
4391
  references: payload.references,
4274
- ...payload
4392
+ metadata: payload.metadata
4275
4393
  };
4276
4394
  const validation = validateExecutionRecordDetailed(execution);
4277
4395
  if (!validation.isValid) {
@@ -4336,7 +4454,7 @@ var ExecutionAdapter = class {
4336
4454
  },
4337
4455
  payload: validatedPayload
4338
4456
  };
4339
- const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
4457
+ const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Execution record created");
4340
4458
  await this.executionStore.write(signedRecord);
4341
4459
  this.eventBus.publish({
4342
4460
  type: "execution.created",
@@ -4568,7 +4686,7 @@ var ChangelogAdapter = class {
4568
4686
  },
4569
4687
  payload: validatedPayload
4570
4688
  };
4571
- const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
4689
+ const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Changelog record created");
4572
4690
  await this.changelogStore.write(signedRecord);
4573
4691
  this.eventBus.publish({
4574
4692
  type: "changelog.created",
@@ -5264,7 +5382,7 @@ var BacklogAdapter = class {
5264
5382
  },
5265
5383
  payload: validatedPayload
5266
5384
  };
5267
- const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
5385
+ const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Task created");
5268
5386
  await this.taskStore.write(signedRecord);
5269
5387
  this.eventBus.publish({
5270
5388
  type: "task.created",
@@ -5326,7 +5444,7 @@ var BacklogAdapter = class {
5326
5444
  }
5327
5445
  const updatedPayload = { ...task, status: "review" };
5328
5446
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5329
- const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "submitter");
5447
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "submitter", "Task submitted for review");
5330
5448
  await this.taskStore.write(signedRecord);
5331
5449
  this.eventBus.publish({
5332
5450
  type: "task.status.changed",
@@ -5378,7 +5496,7 @@ var BacklogAdapter = class {
5378
5496
  }
5379
5497
  const updatedPayload = { ...task, status: targetState };
5380
5498
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5381
- const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver");
5499
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver", `Task approved: ${task.status} \u2192 ${targetState}`);
5382
5500
  await this.taskStore.write(signedRecord);
5383
5501
  this.eventBus.publish({
5384
5502
  type: "task.status.changed",
@@ -5418,7 +5536,7 @@ var BacklogAdapter = class {
5418
5536
  }
5419
5537
  const updatedPayload = { ...task, status: "active" };
5420
5538
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5421
- const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "executor");
5539
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "executor", "Task activated");
5422
5540
  await this.taskStore.write(signedRecord);
5423
5541
  await this.configManager.updateActorState(actorId, {
5424
5542
  activeTaskId: taskId
@@ -5469,7 +5587,7 @@ var BacklogAdapter = class {
5469
5587
  }
5470
5588
  };
5471
5589
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5472
- const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "pauser");
5590
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "pauser", `Task paused: ${reason || "No reason provided"}`);
5473
5591
  await this.taskStore.write(signedRecord);
5474
5592
  await this.configManager.updateActorState(actorId, {
5475
5593
  activeTaskId: void 0
@@ -5519,7 +5637,7 @@ var BacklogAdapter = class {
5519
5637
  }
5520
5638
  const updatedPayload = { ...task, status: "active" };
5521
5639
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5522
- const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "resumer");
5640
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "resumer", "Task resumed");
5523
5641
  await this.taskStore.write(signedRecord);
5524
5642
  await this.configManager.updateActorState(actorId, {
5525
5643
  activeTaskId: taskId
@@ -5562,7 +5680,7 @@ var BacklogAdapter = class {
5562
5680
  }
5563
5681
  const updatedPayload = { ...task, status: "done" };
5564
5682
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5565
- const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver");
5683
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver", "Task completed");
5566
5684
  await this.taskStore.write(signedRecord);
5567
5685
  await this.configManager.updateActorState(actorId, {
5568
5686
  activeTaskId: void 0
@@ -5617,7 +5735,7 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
5617
5735
  }
5618
5736
  };
5619
5737
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5620
- const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "canceller");
5738
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "canceller", `Task discarded: ${reason || "No reason provided"}`);
5621
5739
  await this.taskStore.write(signedRecord);
5622
5740
  await this.configManager.updateActorState(actorId, {
5623
5741
  activeTaskId: void 0
@@ -5671,8 +5789,9 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
5671
5789
  }
5672
5790
  /**
5673
5791
  * Updates a task with new payload
5792
+ * [EARS-28] Signs the updated record with the editor's signature
5674
5793
  */
5675
- async updateTask(taskId, payload) {
5794
+ async updateTask(taskId, payload, actorId) {
5676
5795
  const taskRecord = await this.taskStore.read(taskId);
5677
5796
  if (!taskRecord) {
5678
5797
  throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
@@ -5682,7 +5801,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
5682
5801
  }
5683
5802
  const updatedPayload = createTaskRecord({ ...taskRecord.payload, ...payload });
5684
5803
  const updatedRecord = { ...taskRecord, payload: updatedPayload };
5685
- await this.taskStore.write(updatedRecord);
5804
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "editor", "Task updated");
5805
+ await this.taskStore.write(signedRecord);
5686
5806
  return updatedPayload;
5687
5807
  }
5688
5808
  // ===== PHASE 2: AGENT NAVIGATION (IMPLEMENTED) =====
@@ -5993,7 +6113,7 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
5993
6113
  },
5994
6114
  payload: validatedPayload
5995
6115
  };
5996
- const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
6116
+ const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Cycle created");
5997
6117
  await this.cycleStore.write(signedRecord);
5998
6118
  this.eventBus.publish({
5999
6119
  type: "cycle.created",
@@ -6096,12 +6216,14 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
6096
6216
  const signedCycleRecord = await this.identity.signRecord(
6097
6217
  { ...cycleRecord, payload: updatedCycle },
6098
6218
  currentActor.id,
6099
- "author"
6219
+ "author",
6220
+ `Task ${taskId} added to cycle`
6100
6221
  );
6101
6222
  const signedTaskRecord = await this.identity.signRecord(
6102
6223
  { ...taskRecord, payload: updatedTask },
6103
6224
  currentActor.id,
6104
- "author"
6225
+ "author",
6226
+ `Task linked to cycle ${cycleId}`
6105
6227
  );
6106
6228
  await Promise.all([
6107
6229
  this.cycleStore.write(signedCycleRecord),
@@ -6145,7 +6267,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
6145
6267
  const signedCycleRecord = await this.identity.signRecord(
6146
6268
  { ...cycleRecord, payload: updatedCycle },
6147
6269
  currentActor.id,
6148
- "author"
6270
+ "author",
6271
+ `Tasks removed from cycle: ${taskIds.join(", ")}`
6149
6272
  );
6150
6273
  const signedTaskRecords = await Promise.all(
6151
6274
  taskRecords.map(async ({ record }) => {
@@ -6157,7 +6280,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
6157
6280
  return await this.identity.signRecord(
6158
6281
  { ...record, payload: updatedTask },
6159
6282
  currentActor.id,
6160
- "author"
6283
+ "author",
6284
+ "Task removed from deleted cycle"
6161
6285
  );
6162
6286
  })
6163
6287
  );
@@ -6223,12 +6347,14 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
6223
6347
  this.identity.signRecord(
6224
6348
  { ...sourceCycleRecord, payload: updatedSourceCycle },
6225
6349
  currentActor.id,
6226
- "author"
6350
+ "author",
6351
+ "Tasks moved from cycle"
6227
6352
  ),
6228
6353
  this.identity.signRecord(
6229
6354
  { ...targetCycleRecord, payload: updatedTargetCycle },
6230
6355
  currentActor.id,
6231
- "author"
6356
+ "author",
6357
+ "Tasks moved to cycle"
6232
6358
  )
6233
6359
  ]);
6234
6360
  const signedTaskRecords = await Promise.all(
@@ -6242,7 +6368,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
6242
6368
  return await this.identity.signRecord(
6243
6369
  { ...record, payload: updatedTask },
6244
6370
  currentActor.id,
6245
- "author"
6371
+ "author",
6372
+ "Task cycle updated"
6246
6373
  );
6247
6374
  })
6248
6375
  );
@@ -7254,7 +7381,7 @@ var ProjectAdapter = class {
7254
7381
  const projectRoot2 = process.env["GITGOV_ORIGINAL_DIR"] || process.cwd();
7255
7382
  const gitgovPath = path6.join(projectRoot2, ".gitgov");
7256
7383
  await this.createDirectoryStructure(gitgovPath);
7257
- await this.copyAgentPrompt(gitgovPath);
7384
+ await this.copyAgentPrompt(projectRoot2);
7258
7385
  const actor = await this.identityAdapter.createActor(
7259
7386
  {
7260
7387
  type: "human",
@@ -7333,7 +7460,8 @@ var ProjectAdapter = class {
7333
7460
  }
7334
7461
  console.warn(`\u26A0\uFE0F ${warnings.join(", ")}.`);
7335
7462
  console.warn(` State sync will be available after 'git remote add origin <url>' and first commit.`);
7336
- console.warn(` Run 'gitgov sync push' when ready to enable multi-machine collaboration.`);
7463
+ console.warn(` Run 'gitgov sync push' when ready to enable multi-machine collaboration.
7464
+ `);
7337
7465
  }
7338
7466
  await this.initializeSession(actor.id, gitgovPath);
7339
7467
  await this.setupGitIntegration(projectRoot2);
@@ -7552,8 +7680,8 @@ var ProjectAdapter = class {
7552
7680
  await promises.mkdir(path6.join(gitgovPath, dir), { recursive: true });
7553
7681
  }
7554
7682
  }
7555
- async copyAgentPrompt(gitgovPath) {
7556
- const targetPrompt = path6.join(gitgovPath, "gitgov");
7683
+ async copyAgentPrompt(projectRoot2) {
7684
+ const targetPrompt = path6.join(projectRoot2, "gitgov");
7557
7685
  const potentialSources = [];
7558
7686
  potentialSources.push(
7559
7687
  path6.join(process.cwd(), "prompts/gitgov_agent_prompt.md")
@@ -7583,7 +7711,7 @@ var ProjectAdapter = class {
7583
7711
  try {
7584
7712
  await promises.access(source);
7585
7713
  await promises.copyFile(source, targetPrompt);
7586
- logger3.debug(`\u{1F4CB} @gitgov agent prompt copied to .gitgov/gitgov
7714
+ logger3.debug(`\u{1F4CB} @gitgov agent prompt copied to project root (./gitgov)
7587
7715
  `);
7588
7716
  return;
7589
7717
  } catch {
@@ -7625,8 +7753,8 @@ var ProjectAdapter = class {
7625
7753
  # Ignore entire .gitgov/ directory (state lives in gitgov-state branch)
7626
7754
  .gitgov/
7627
7755
 
7628
- # Exception: Don't ignore .gitgov/.gitignore itself (meta!)
7629
- !.gitgov/.gitignore
7756
+ # Ignore agent prompt file (project-specific, created by gitgov init)
7757
+ gitgov
7630
7758
  `;
7631
7759
  try {
7632
7760
  let existingContent = "";
@@ -11355,7 +11483,31 @@ function shouldSyncFile(filePath) {
11355
11483
  if (LOCAL_ONLY_FILES.includes(fileName)) {
11356
11484
  return false;
11357
11485
  }
11358
- return true;
11486
+ const normalizedPath = filePath.replace(/\\/g, "/");
11487
+ const parts = normalizedPath.split("/");
11488
+ const gitgovIndex = parts.findIndex((p) => p === ".gitgov");
11489
+ let relativeParts;
11490
+ if (gitgovIndex !== -1) {
11491
+ relativeParts = parts.slice(gitgovIndex + 1);
11492
+ } else {
11493
+ const syncDirIndex = parts.findIndex(
11494
+ (p) => SYNC_DIRECTORIES.includes(p)
11495
+ );
11496
+ if (syncDirIndex !== -1) {
11497
+ relativeParts = parts.slice(syncDirIndex);
11498
+ } else if (SYNC_ROOT_FILES.includes(fileName)) {
11499
+ return true;
11500
+ } else {
11501
+ return false;
11502
+ }
11503
+ }
11504
+ if (relativeParts.length === 1) {
11505
+ return SYNC_ROOT_FILES.includes(relativeParts[0]);
11506
+ } else if (relativeParts.length >= 2) {
11507
+ const dirName = relativeParts[0];
11508
+ return SYNC_DIRECTORIES.includes(dirName);
11509
+ }
11510
+ return false;
11359
11511
  }
11360
11512
  async function getAllFiles(dir, baseDir = dir) {
11361
11513
  const files = [];
@@ -11374,7 +11526,7 @@ async function getAllFiles(dir, baseDir = dir) {
11374
11526
  }
11375
11527
  return files;
11376
11528
  }
11377
- async function copySyncableFiles(sourceDir, destDir, log) {
11529
+ async function copySyncableFiles(sourceDir, destDir, log, excludeFiles = /* @__PURE__ */ new Set()) {
11378
11530
  let copiedCount = 0;
11379
11531
  for (const dirName of SYNC_DIRECTORIES) {
11380
11532
  const sourcePath = path6__default.join(sourceDir, dirName);
@@ -11386,6 +11538,11 @@ async function copySyncableFiles(sourceDir, destDir, log) {
11386
11538
  for (const relativePath of allFiles) {
11387
11539
  const fullSourcePath = path6__default.join(sourcePath, relativePath);
11388
11540
  const fullDestPath = path6__default.join(destPath, relativePath);
11541
+ const gitgovRelativePath = `.gitgov/${dirName}/${relativePath}`;
11542
+ if (excludeFiles.has(gitgovRelativePath)) {
11543
+ log(`[EARS-60] Skipped (remote-only change preserved): ${gitgovRelativePath}`);
11544
+ continue;
11545
+ }
11389
11546
  if (shouldSyncFile(fullSourcePath)) {
11390
11547
  await promises.mkdir(path6__default.dirname(fullDestPath), { recursive: true });
11391
11548
  await promises.copyFile(fullSourcePath, fullDestPath);
@@ -11405,6 +11562,11 @@ async function copySyncableFiles(sourceDir, destDir, log) {
11405
11562
  for (const fileName of SYNC_ROOT_FILES) {
11406
11563
  const sourcePath = path6__default.join(sourceDir, fileName);
11407
11564
  const destPath = path6__default.join(destDir, fileName);
11565
+ const gitgovRelativePath = `.gitgov/${fileName}`;
11566
+ if (excludeFiles.has(gitgovRelativePath)) {
11567
+ log(`[EARS-60] Skipped (remote-only change preserved): ${gitgovRelativePath}`);
11568
+ continue;
11569
+ }
11408
11570
  try {
11409
11571
  await promises.copyFile(sourcePath, destPath);
11410
11572
  log(`Copied root file: ${fileName}`);
@@ -11628,7 +11790,28 @@ var SyncModule = class {
11628
11790
  await this.git.checkoutOrphanBranch(stateBranch);
11629
11791
  try {
11630
11792
  await execAsync("git rm -rf . 2>/dev/null || true", { cwd: repoRoot });
11631
- await execAsync('git commit --allow-empty -m "Initialize state branch"', { cwd: repoRoot });
11793
+ const gitignoreContent = `# GitGovernance State Branch .gitignore
11794
+ # This file is auto-generated during gitgov init
11795
+ # These files are machine-specific and should NOT be synced
11796
+
11797
+ # Local-only files (regenerated/machine-specific)
11798
+ index.json
11799
+ .session.json
11800
+ gitgov
11801
+
11802
+ # Private keys (never synced for security)
11803
+ *.key
11804
+
11805
+ # Backup and temporary files
11806
+ *.backup
11807
+ *.backup-*
11808
+ *.tmp
11809
+ *.bak
11810
+ `;
11811
+ const gitignorePath = path6__default.join(repoRoot, ".gitignore");
11812
+ await promises.writeFile(gitignorePath, gitignoreContent, "utf-8");
11813
+ await execAsync("git add .gitignore", { cwd: repoRoot });
11814
+ await execAsync('git commit -m "Initialize state branch with .gitignore"', { cwd: repoRoot });
11632
11815
  } catch (commitError) {
11633
11816
  const error = commitError;
11634
11817
  throw new Error(`Failed to create initial commit on orphan branch: ${error.stderr || error.message}`);
@@ -11683,6 +11866,17 @@ var SyncModule = class {
11683
11866
  );
11684
11867
  }
11685
11868
  }
11869
+ /**
11870
+ * [EARS-60] Detect file-level conflicts and identify remote-only changes.
11871
+ *
11872
+ * A conflict exists when:
11873
+ * 1. A file was modified by the remote during implicit pull
11874
+ * 2. AND the LOCAL USER also modified that same file (content in tempDir differs from what was in git before pull)
11875
+ *
11876
+ * This catches conflicts that git rebase can't detect because we copy files AFTER the pull.
11877
+ *
11878
+ * @param tempDir - Directory containing local .gitgov/ files (preserved before checkout)
11879
+ * @param repoRoot - Repository root path
11686
11880
  /**
11687
11881
  * Checks if a rebase is in progress.
11688
11882
  *
@@ -11817,8 +12011,14 @@ var SyncModule = class {
11817
12011
  const commit = commits[i];
11818
12012
  if (!commit) continue;
11819
12013
  const message = commit.message.toLowerCase();
11820
- const isRebaseCommit = message.includes("rebase") || message.includes("pick") || message.includes("conflict");
11821
- if (isRebaseCommit) {
12014
+ if (message.startsWith("resolution:")) {
12015
+ continue;
12016
+ }
12017
+ if (message.startsWith("sync:")) {
12018
+ continue;
12019
+ }
12020
+ const isExplicitRebaseCommit = message.includes("rebase") || message.includes("pick ");
12021
+ if (isExplicitRebaseCommit) {
11822
12022
  const nextCommit = commits[i + 1];
11823
12023
  const isResolutionNext = nextCommit && nextCommit.message.toLowerCase().startsWith("resolution:");
11824
12024
  if (!isResolutionNext) {
@@ -11963,14 +12163,34 @@ Then push your changes: git push -u origin ${sourceBranch}`
11963
12163
  log(`Audit result: ${auditReport.passed ? "PASSED" : "FAILED"}`);
11964
12164
  if (!auditReport.passed) {
11965
12165
  log(`Audit violations: ${auditReport.summary}`);
12166
+ const affectedFiles = [];
12167
+ const detailedErrors = [];
12168
+ if (auditReport.lintReport?.results) {
12169
+ for (const r of auditReport.lintReport.results) {
12170
+ if (r.level === "error") {
12171
+ if (!affectedFiles.includes(r.filePath)) {
12172
+ affectedFiles.push(r.filePath);
12173
+ }
12174
+ detailedErrors.push(` \u2022 ${r.filePath}: [${r.validator}] ${r.message}`);
12175
+ }
12176
+ }
12177
+ }
12178
+ for (const v of auditReport.integrityViolations) {
12179
+ detailedErrors.push(` \u2022 Commit ${v.rebaseCommitHash.slice(0, 8)}: ${v.commitMessage} (by ${v.author})`);
12180
+ }
12181
+ const detailSection = detailedErrors.length > 0 ? `
12182
+
12183
+ Details:
12184
+ ${detailedErrors.join("\n")}` : "";
11966
12185
  result.conflictDetected = true;
11967
12186
  result.conflictInfo = {
11968
12187
  type: "integrity_violation",
11969
- affectedFiles: [],
11970
- message: auditReport.summary,
12188
+ affectedFiles,
12189
+ message: auditReport.summary + detailSection,
11971
12190
  resolutionSteps: [
11972
- "Review audit violations",
11973
- "Fix integrity issues before pushing"
12191
+ "Run 'gitgov lint --fix' to auto-fix signature/checksum issues",
12192
+ "If issues persist, manually review the affected files",
12193
+ "Then retry: gitgov sync push"
11974
12194
  ]
11975
12195
  };
11976
12196
  result.error = "Integrity violations detected. Cannot push.";
@@ -12009,11 +12229,53 @@ Then push your changes: git push -u origin ${sourceBranch}`
12009
12229
  await this.git.checkoutBranch(savedBranch);
12010
12230
  if (tempDir) {
12011
12231
  try {
12012
- log("[EARS-47] Restoring .gitgov/ from temp directory (early return)...");
12013
12232
  const repoRoot2 = await this.git.getRepoRoot();
12014
12233
  const gitgovDir = path6__default.join(repoRoot2, ".gitgov");
12015
- await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
12016
- log("[EARS-47] .gitgov/ restored from temp (early return)");
12234
+ if (returnResult.implicitPull?.hasChanges) {
12235
+ log("[EARS-58] Implicit pull detected in early return - preserving new files from gitgov-state...");
12236
+ try {
12237
+ await this.git.checkoutFilesFromBranch(stateBranch, [".gitgov/"]);
12238
+ await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot2 });
12239
+ log("[EARS-58] Synced files copied from gitgov-state (unstaged)");
12240
+ } catch (checkoutError) {
12241
+ log(`[EARS-58] Warning: Failed to checkout from gitgov-state: ${checkoutError}`);
12242
+ }
12243
+ for (const fileName of LOCAL_ONLY_FILES) {
12244
+ const tempFilePath = path6__default.join(tempDir, fileName);
12245
+ const destFilePath = path6__default.join(gitgovDir, fileName);
12246
+ try {
12247
+ await promises.access(tempFilePath);
12248
+ await promises.cp(tempFilePath, destFilePath, { force: true });
12249
+ log(`[EARS-58] Restored LOCAL_ONLY_FILE: ${fileName}`);
12250
+ } catch {
12251
+ }
12252
+ }
12253
+ const restoreExcludedFilesEarly = async (dir, destDir) => {
12254
+ try {
12255
+ const entries = await promises.readdir(dir, { withFileTypes: true });
12256
+ for (const entry of entries) {
12257
+ const srcPath = path6__default.join(dir, entry.name);
12258
+ const dstPath = path6__default.join(destDir, entry.name);
12259
+ if (entry.isDirectory()) {
12260
+ await restoreExcludedFilesEarly(srcPath, dstPath);
12261
+ } else {
12262
+ const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
12263
+ if (isExcluded) {
12264
+ await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
12265
+ await promises.copyFile(srcPath, dstPath);
12266
+ log(`[EARS-59] Restored excluded file (early return): ${entry.name}`);
12267
+ }
12268
+ }
12269
+ }
12270
+ } catch {
12271
+ }
12272
+ };
12273
+ await restoreExcludedFilesEarly(tempDir, gitgovDir);
12274
+ } else {
12275
+ log("[EARS-47] Restoring .gitgov/ from temp directory (early return)...");
12276
+ await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
12277
+ log("[EARS-47] .gitgov/ restored from temp (early return)");
12278
+ }
12017
12279
  await promises.rm(tempDir, { recursive: true, force: true });
12018
12280
  log("[EARS-47] Temp directory cleaned up (early return)");
12019
12281
  } catch (tempRestoreError) {
@@ -12030,42 +12292,32 @@ Then push your changes: git push -u origin ${sourceBranch}`
12030
12292
  returnResult.error = returnResult.error ? `${returnResult.error}. Failed to restore stashed changes.` : "Failed to restore stashed changes. Run 'git stash pop' manually.";
12031
12293
  }
12032
12294
  }
12295
+ if (returnResult.implicitPull?.hasChanges) {
12296
+ log("[EARS-58] Regenerating index after implicit pull (early return)...");
12297
+ try {
12298
+ await this.indexer.generateIndex();
12299
+ returnResult.implicitPull.reindexed = true;
12300
+ log("[EARS-58] Index regenerated successfully");
12301
+ } catch (indexError) {
12302
+ log(`[EARS-58] Warning: Failed to regenerate index: ${indexError}`);
12303
+ returnResult.implicitPull.reindexed = false;
12304
+ }
12305
+ }
12033
12306
  return returnResult;
12034
12307
  };
12035
12308
  log(`Checking out to ${stateBranch}...`);
12036
12309
  await this.git.checkoutBranch(stateBranch);
12037
12310
  log(`Now on branch: ${await this.git.getCurrentBranch()}`);
12038
- log("Attempting pull --rebase...");
12311
+ let filesBeforeChanges = /* @__PURE__ */ new Set();
12039
12312
  try {
12040
- await this.git.pullRebase("origin", stateBranch);
12041
- log("Pull rebase successful");
12042
- } catch (error) {
12043
- const errorMsg = error instanceof Error ? error.message : String(error);
12044
- log(`Pull rebase failed: ${errorMsg}`);
12045
- const isAlreadyUpToDate = errorMsg.includes("up to date") || errorMsg.includes("up-to-date");
12046
- const isNoRemote = errorMsg.includes("does not appear to be") || errorMsg.includes("Could not read from remote");
12047
- if (isAlreadyUpToDate || isNoRemote) {
12048
- log("Pull failed but continuing (already up-to-date or no remote)");
12049
- } else {
12050
- const conflictedFiles = await this.git.getConflictedFiles();
12051
- if (conflictedFiles.length > 0) {
12052
- await this.git.rebaseAbort();
12053
- result.conflictDetected = true;
12054
- result.conflictInfo = {
12055
- type: "rebase_conflict",
12056
- affectedFiles: conflictedFiles,
12057
- message: "Conflict detected during automatic reconciliation",
12058
- resolutionSteps: [
12059
- "Pull remote changes first: gitgov sync pull",
12060
- "Resolve any conflicts",
12061
- "Then push your changes"
12062
- ]
12063
- };
12064
- result.error = "Conflict detected during reconciliation";
12065
- return await restoreStashAndReturn(result);
12066
- }
12067
- throw error;
12068
- }
12313
+ const repoRoot2 = await this.git.getRepoRoot();
12314
+ const { stdout: filesOutput } = await execAsync(
12315
+ `git ls-files ".gitgov" 2>/dev/null || true`,
12316
+ { cwd: repoRoot2 }
12317
+ );
12318
+ filesBeforeChanges = new Set(filesOutput.trim().split("\n").filter((f) => f && shouldSyncFile(f)));
12319
+ log(`[EARS-57] Files in gitgov-state before changes: ${filesBeforeChanges.size}`);
12320
+ } catch {
12069
12321
  }
12070
12322
  log("=== Phase 2: Publication ===");
12071
12323
  log("Checking if .gitgov/ exists in gitgov-state...");
@@ -12213,6 +12465,43 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12213
12465
  } catch {
12214
12466
  }
12215
12467
  log("[EARS-47] Non-syncable files cleanup complete");
12468
+ log("[EARS-57] Checking for deleted files to sync...");
12469
+ try {
12470
+ const sourceFiles = /* @__PURE__ */ new Set();
12471
+ const findSourceFiles = async (dir, prefix = ".gitgov") => {
12472
+ try {
12473
+ const entries = await promises.readdir(dir, { withFileTypes: true });
12474
+ for (const entry of entries) {
12475
+ const fullPath = path6__default.join(dir, entry.name);
12476
+ const relativePath = `${prefix}/${entry.name}`;
12477
+ if (entry.isDirectory()) {
12478
+ await findSourceFiles(fullPath, relativePath);
12479
+ } else if (shouldSyncFile(relativePath)) {
12480
+ sourceFiles.add(relativePath);
12481
+ }
12482
+ }
12483
+ } catch {
12484
+ }
12485
+ };
12486
+ const sourceDir = tempDir || path6__default.join(repoRoot, ".gitgov");
12487
+ await findSourceFiles(sourceDir);
12488
+ log(`[EARS-57] Found ${sourceFiles.size} syncable files in source (user's local state)`);
12489
+ log(`[EARS-57] Files that existed before changes: ${filesBeforeChanges.size}`);
12490
+ let deletedCount = 0;
12491
+ for (const fileBeforeChange of filesBeforeChanges) {
12492
+ if (!sourceFiles.has(fileBeforeChange)) {
12493
+ try {
12494
+ await execAsync(`git rm -f "${fileBeforeChange}"`, { cwd: repoRoot });
12495
+ log(`[EARS-57] Deleted (user removed): ${fileBeforeChange}`);
12496
+ deletedCount++;
12497
+ } catch {
12498
+ }
12499
+ }
12500
+ }
12501
+ log(`[EARS-57] Deleted ${deletedCount} files that user removed locally`);
12502
+ } catch (err) {
12503
+ log(`[EARS-57] Warning: Failed to sync deleted files: ${err}`);
12504
+ }
12216
12505
  const hasStaged = await this.git.hasUncommittedChanges();
12217
12506
  log(`Has staged changes: ${hasStaged}`);
12218
12507
  if (!hasStaged) {
@@ -12221,10 +12510,10 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12221
12510
  result.filesSynced = 0;
12222
12511
  return await restoreStashAndReturn(result);
12223
12512
  }
12224
- log("Creating commit...");
12513
+ log("Creating local commit...");
12225
12514
  try {
12226
12515
  const commitHash = await this.git.commit(commitMessage);
12227
- log(`Commit created: ${commitHash}`);
12516
+ log(`Local commit created: ${commitHash}`);
12228
12517
  result.commitHash = commitHash;
12229
12518
  } catch (commitError) {
12230
12519
  const errorMsg = commitError instanceof Error ? commitError.message : String(commitError);
@@ -12242,6 +12531,116 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12242
12531
  log(`ERROR: Git stderr: ${stderr}`);
12243
12532
  throw new Error(`Failed to create commit: ${errorMsg} | stderr: ${stderr}`);
12244
12533
  }
12534
+ log("=== Phase 3: Reconcile with Remote (Git-Native) ===");
12535
+ let hashBeforePull = null;
12536
+ try {
12537
+ const { stdout: beforeHash } = await execAsync(`git rev-parse HEAD`, { cwd: repoRoot });
12538
+ hashBeforePull = beforeHash.trim();
12539
+ log(`Hash before pull: ${hashBeforePull}`);
12540
+ } catch {
12541
+ }
12542
+ log("Attempting git pull --rebase origin gitgov-state...");
12543
+ try {
12544
+ await this.git.pullRebase("origin", stateBranch);
12545
+ log("Pull rebase successful - no conflicts");
12546
+ if (hashBeforePull) {
12547
+ try {
12548
+ const { stdout: afterHash } = await execAsync(`git rev-parse HEAD`, { cwd: repoRoot });
12549
+ const hashAfterPull = afterHash.trim();
12550
+ if (hashAfterPull !== hashBeforePull) {
12551
+ const pulledChangedFiles = await this.git.getChangedFiles(hashBeforePull, hashAfterPull, ".gitgov/");
12552
+ result.implicitPull = {
12553
+ hasChanges: true,
12554
+ filesUpdated: pulledChangedFiles.length,
12555
+ reindexed: false
12556
+ // Will be set to true after actual reindex
12557
+ };
12558
+ log(`[EARS-54] Implicit pull: ${pulledChangedFiles.length} files from remote were rebased`);
12559
+ }
12560
+ } catch (e) {
12561
+ log(`[EARS-54] Could not capture implicit pull details: ${e}`);
12562
+ }
12563
+ }
12564
+ } catch (pullError) {
12565
+ const errorMsg = pullError instanceof Error ? pullError.message : String(pullError);
12566
+ log(`Pull rebase result: ${errorMsg}`);
12567
+ const isAlreadyUpToDate = errorMsg.includes("up to date") || errorMsg.includes("up-to-date");
12568
+ const isNoRemote = errorMsg.includes("does not appear to be") || errorMsg.includes("Could not read from remote");
12569
+ const isNoUpstream = errorMsg.includes("no tracking information") || errorMsg.includes("There is no tracking information");
12570
+ if (isAlreadyUpToDate || isNoRemote || isNoUpstream) {
12571
+ log("Pull not needed or no remote - continuing to push");
12572
+ } else {
12573
+ const isRebaseInProgress = await this.isRebaseInProgress();
12574
+ const conflictedFiles = await this.git.getConflictedFiles();
12575
+ if (isRebaseInProgress || conflictedFiles.length > 0) {
12576
+ log(`[GIT-NATIVE] Conflict detected! Files: ${conflictedFiles.join(", ")}`);
12577
+ result.conflictDetected = true;
12578
+ const fileWord = conflictedFiles.length === 1 ? "file" : "files";
12579
+ const stageCommand = conflictedFiles.length === 1 ? `git add ${conflictedFiles[0]}` : "git add .gitgov/";
12580
+ result.conflictInfo = {
12581
+ type: "rebase_conflict",
12582
+ affectedFiles: conflictedFiles,
12583
+ message: "Conflict detected during sync - Git has paused the rebase for manual resolution",
12584
+ resolutionSteps: [
12585
+ `1. Edit the conflicted ${fileWord} to resolve conflicts (remove <<<<<<, ======, >>>>>> markers)`,
12586
+ `2. Stage resolved ${fileWord}: ${stageCommand}`,
12587
+ "3. Complete sync: gitgov sync resolve --reason 'your reason'",
12588
+ "(This will continue the rebase, re-sign the record, and return you to your original branch)"
12589
+ ]
12590
+ };
12591
+ result.error = `Conflict detected: ${conflictedFiles.length} file(s) need manual resolution. Use 'git status' to see details.`;
12592
+ if (stashHash) {
12593
+ try {
12594
+ await this.git.checkoutBranch(sourceBranch);
12595
+ await execAsync("git stash pop", { cwd: repoRoot });
12596
+ await this.git.checkoutBranch(stateBranch);
12597
+ log("Restored stash to original branch during conflict");
12598
+ } catch (stashErr) {
12599
+ log(`Warning: Could not restore stash: ${stashErr}`);
12600
+ }
12601
+ }
12602
+ if (tempDir) {
12603
+ log("Restoring local files (.key, .session.json, etc.) for conflict resolution...");
12604
+ const gitgovInState = path6__default.join(repoRoot, ".gitgov");
12605
+ for (const fileName of LOCAL_ONLY_FILES) {
12606
+ const srcPath = path6__default.join(tempDir, fileName);
12607
+ const destPath = path6__default.join(gitgovInState, fileName);
12608
+ try {
12609
+ await promises.access(srcPath);
12610
+ await promises.cp(srcPath, destPath, { force: true });
12611
+ log(`Restored LOCAL_ONLY_FILE for conflict resolution: ${fileName}`);
12612
+ } catch {
12613
+ }
12614
+ }
12615
+ const restoreExcluded = async (srcDir, destDir) => {
12616
+ try {
12617
+ const entries = await promises.readdir(srcDir, { withFileTypes: true });
12618
+ for (const entry of entries) {
12619
+ const srcPath = path6__default.join(srcDir, entry.name);
12620
+ const dstPath = path6__default.join(destDir, entry.name);
12621
+ if (entry.isDirectory()) {
12622
+ await restoreExcluded(srcPath, dstPath);
12623
+ } else {
12624
+ const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
12625
+ if (isExcluded) {
12626
+ await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
12627
+ await promises.copyFile(srcPath, dstPath);
12628
+ log(`Restored EXCLUDED file for conflict resolution: ${entry.name}`);
12629
+ }
12630
+ }
12631
+ }
12632
+ } catch {
12633
+ }
12634
+ };
12635
+ await restoreExcluded(tempDir, gitgovInState);
12636
+ log("Local files restored for conflict resolution");
12637
+ }
12638
+ return result;
12639
+ }
12640
+ throw pullError;
12641
+ }
12642
+ }
12643
+ log("=== Phase 4: Push to Remote ===");
12245
12644
  log("Pushing to remote...");
12246
12645
  try {
12247
12646
  await this.git.push("origin", stateBranch);
@@ -12271,11 +12670,57 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12271
12670
  }
12272
12671
  }
12273
12672
  if (tempDir) {
12274
- log("[EARS-43] Restoring ENTIRE .gitgov/ from temp directory to working tree...");
12275
12673
  const repoRoot2 = await this.git.getRepoRoot();
12276
12674
  const gitgovDir = path6__default.join(repoRoot2, ".gitgov");
12277
- await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
12278
- log("[EARS-43] Entire .gitgov/ restored from temp");
12675
+ if (result.implicitPull?.hasChanges) {
12676
+ log("[EARS-56] Implicit pull detected - copying synced files from gitgov-state first...");
12677
+ try {
12678
+ await this.git.checkoutFilesFromBranch(stateBranch, [".gitgov/"]);
12679
+ await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot2 });
12680
+ log("[EARS-56] Synced files copied from gitgov-state to work branch (unstaged)");
12681
+ } catch (checkoutError) {
12682
+ log(`[EARS-56] Warning: Failed to checkout from gitgov-state: ${checkoutError}`);
12683
+ await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
12684
+ log("[EARS-56] Fallback: Entire .gitgov/ restored from temp");
12685
+ }
12686
+ log("[EARS-56] Restoring local-only files from temp directory...");
12687
+ for (const fileName of LOCAL_ONLY_FILES) {
12688
+ const tempFilePath = path6__default.join(tempDir, fileName);
12689
+ const destFilePath = path6__default.join(gitgovDir, fileName);
12690
+ try {
12691
+ await promises.access(tempFilePath);
12692
+ await promises.cp(tempFilePath, destFilePath, { force: true });
12693
+ log(`[EARS-56] Restored LOCAL_ONLY_FILE: ${fileName}`);
12694
+ } catch {
12695
+ }
12696
+ }
12697
+ const restoreExcludedFiles = async (dir, destDir) => {
12698
+ try {
12699
+ const entries = await promises.readdir(dir, { withFileTypes: true });
12700
+ for (const entry of entries) {
12701
+ const srcPath = path6__default.join(dir, entry.name);
12702
+ const dstPath = path6__default.join(destDir, entry.name);
12703
+ if (entry.isDirectory()) {
12704
+ await restoreExcludedFiles(srcPath, dstPath);
12705
+ } else {
12706
+ const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
12707
+ if (isExcluded) {
12708
+ await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
12709
+ await promises.copyFile(srcPath, dstPath);
12710
+ log(`[EARS-59] Restored excluded file: ${entry.name}`);
12711
+ }
12712
+ }
12713
+ }
12714
+ } catch {
12715
+ }
12716
+ };
12717
+ await restoreExcludedFiles(tempDir, gitgovDir);
12718
+ log("[EARS-59] Local-only and excluded files restored from temp");
12719
+ } else {
12720
+ log("[EARS-43] Restoring ENTIRE .gitgov/ from temp directory to working tree...");
12721
+ await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
12722
+ log("[EARS-43] Entire .gitgov/ restored from temp");
12723
+ }
12279
12724
  log("[EARS-43] Cleaning up temp directory...");
12280
12725
  try {
12281
12726
  await promises.rm(tempDir, { recursive: true, force: true });
@@ -12284,6 +12729,17 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12284
12729
  log(`[EARS-43] Warning: Failed to cleanup temp directory: ${cleanupError}`);
12285
12730
  }
12286
12731
  }
12732
+ if (result.implicitPull?.hasChanges) {
12733
+ log("[EARS-54] Regenerating index after implicit pull...");
12734
+ try {
12735
+ await this.indexer.generateIndex();
12736
+ result.implicitPull.reindexed = true;
12737
+ log("[EARS-54] Index regenerated successfully after implicit pull");
12738
+ } catch (indexError) {
12739
+ log(`[EARS-54] Warning: Failed to regenerate index after implicit pull: ${indexError}`);
12740
+ result.implicitPull.reindexed = false;
12741
+ }
12742
+ }
12287
12743
  result.success = true;
12288
12744
  result.filesSynced = delta.length;
12289
12745
  log(`=== pushState COMPLETED SUCCESSFULLY: ${delta.length} files synced ===`);
@@ -12326,7 +12782,7 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12326
12782
  * [EARS-44] Requires remote to be configured (pull without remote makes no sense)
12327
12783
  */
12328
12784
  async pullState(options = {}) {
12329
- const { forceReindex = false } = options;
12785
+ const { forceReindex = false, force = false } = options;
12330
12786
  const stateBranch = await this.getStateBranchName();
12331
12787
  if (!stateBranch) {
12332
12788
  throw new SyncError("Failed to get state branch name");
@@ -12393,6 +12849,37 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12393
12849
  } catch (error) {
12394
12850
  log(`[EARS-51] Warning: Could not save local files: ${error.message}`);
12395
12851
  }
12852
+ const savedSyncableFiles = /* @__PURE__ */ new Map();
12853
+ try {
12854
+ const gitgovPath2 = path6__default.join(pullRepoRoot, ".gitgov");
12855
+ const gitgovExists2 = await promises.access(gitgovPath2).then(() => true).catch(() => false);
12856
+ if (gitgovExists2) {
12857
+ const readSyncableFilesRecursive = async (dir, baseDir) => {
12858
+ try {
12859
+ const entries = await promises.readdir(dir, { withFileTypes: true });
12860
+ for (const entry of entries) {
12861
+ const fullPath = path6__default.join(dir, entry.name);
12862
+ const relativePath = path6__default.relative(baseDir, fullPath);
12863
+ const gitgovRelativePath = `.gitgov/${relativePath}`;
12864
+ if (entry.isDirectory()) {
12865
+ await readSyncableFilesRecursive(fullPath, baseDir);
12866
+ } else if (shouldSyncFile(gitgovRelativePath)) {
12867
+ try {
12868
+ const content = await promises.readFile(fullPath, "utf-8");
12869
+ savedSyncableFiles.set(gitgovRelativePath, content);
12870
+ } catch {
12871
+ }
12872
+ }
12873
+ }
12874
+ } catch {
12875
+ }
12876
+ };
12877
+ await readSyncableFilesRecursive(gitgovPath2, gitgovPath2);
12878
+ log(`[EARS-61] Saved ${savedSyncableFiles.size} syncable files before checkout for conflict detection`);
12879
+ }
12880
+ } catch (error) {
12881
+ log(`[EARS-61] Warning: Could not save syncable files: ${error.message}`);
12882
+ }
12396
12883
  try {
12397
12884
  await this.git.checkoutBranch(stateBranch);
12398
12885
  } catch (checkoutError) {
@@ -12427,6 +12914,90 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12427
12914
  });
12428
12915
  const hashBefore = commitBefore[0]?.hash;
12429
12916
  await this.git.fetch("origin");
12917
+ log("[EARS-61] Checking for local changes that would be overwritten...");
12918
+ let remoteChangedFiles = [];
12919
+ try {
12920
+ const { stdout: remoteChanges } = await execAsync(
12921
+ `git diff --name-only ${stateBranch} origin/${stateBranch} -- .gitgov/ 2>/dev/null || true`,
12922
+ { cwd: pullRepoRoot }
12923
+ );
12924
+ remoteChangedFiles = remoteChanges.trim().split("\n").filter((f) => f && shouldSyncFile(f));
12925
+ log(`[EARS-61] Remote changed files: ${remoteChangedFiles.length} - ${remoteChangedFiles.join(", ")}`);
12926
+ } catch {
12927
+ log("[EARS-61] Could not determine remote changes, continuing...");
12928
+ }
12929
+ let localModifiedFiles = [];
12930
+ if (remoteChangedFiles.length > 0 && savedSyncableFiles.size > 0) {
12931
+ try {
12932
+ for (const remoteFile of remoteChangedFiles) {
12933
+ const savedContent = savedSyncableFiles.get(remoteFile);
12934
+ if (savedContent !== void 0) {
12935
+ try {
12936
+ const { stdout: gitStateContent } = await execAsync(
12937
+ `git show HEAD:${remoteFile} 2>/dev/null`,
12938
+ { cwd: pullRepoRoot }
12939
+ );
12940
+ if (savedContent !== gitStateContent) {
12941
+ localModifiedFiles.push(remoteFile);
12942
+ log(`[EARS-61] Local file was modified since last sync: ${remoteFile}`);
12943
+ }
12944
+ } catch {
12945
+ localModifiedFiles.push(remoteFile);
12946
+ log(`[EARS-61] Local file is new (not in gitgov-state): ${remoteFile}`);
12947
+ }
12948
+ }
12949
+ }
12950
+ log(`[EARS-61] Local modified files that overlap with remote: ${localModifiedFiles.length}`);
12951
+ } catch (error) {
12952
+ log(`[EARS-61] Warning: Could not check local modifications: ${error.message}`);
12953
+ }
12954
+ }
12955
+ if (localModifiedFiles.length > 0) {
12956
+ if (force) {
12957
+ log(`[EARS-62] Force flag set - will overwrite ${localModifiedFiles.length} local file(s)`);
12958
+ logger6.warn(`[pullState] Force pull: overwriting local changes to ${localModifiedFiles.length} file(s)`);
12959
+ result.forcedOverwrites = localModifiedFiles;
12960
+ } else {
12961
+ log(`[EARS-61] CONFLICT: Local changes would be overwritten by pull`);
12962
+ await this.git.checkoutBranch(savedBranch);
12963
+ for (const [filePath, content] of savedSyncableFiles) {
12964
+ const fullPath = path6__default.join(pullRepoRoot, filePath);
12965
+ try {
12966
+ await promises.mkdir(path6__default.dirname(fullPath), { recursive: true });
12967
+ await promises.writeFile(fullPath, content, "utf-8");
12968
+ log(`[EARS-61] Restored syncable file: ${filePath}`);
12969
+ } catch {
12970
+ }
12971
+ }
12972
+ for (const [fileName, content] of savedLocalFiles) {
12973
+ const filePath = path6__default.join(pullRepoRoot, ".gitgov", fileName);
12974
+ try {
12975
+ await promises.mkdir(path6__default.dirname(filePath), { recursive: true });
12976
+ await promises.writeFile(filePath, content, "utf-8");
12977
+ log(`[EARS-61] Restored local-only file: ${fileName}`);
12978
+ } catch {
12979
+ }
12980
+ }
12981
+ result.success = false;
12982
+ result.conflictDetected = true;
12983
+ result.conflictInfo = {
12984
+ type: "local_changes_conflict",
12985
+ affectedFiles: localModifiedFiles,
12986
+ message: `Your local changes to the following files would be overwritten by pull.
12987
+ You have modified these files locally, and they were also modified remotely.
12988
+ To avoid losing your changes, push first or use --force to overwrite.`,
12989
+ resolutionSteps: [
12990
+ "1. Run 'gitgov sync push' to push your local changes first",
12991
+ " \u2192 This will trigger a rebase and let you resolve conflicts properly",
12992
+ "2. Or run 'gitgov sync pull --force' to discard your local changes"
12993
+ ]
12994
+ };
12995
+ result.error = "Aborting pull: local changes would be overwritten by remote changes";
12996
+ logger6.warn(`[pullState] Aborting: local changes to ${localModifiedFiles.length} file(s) would be overwritten by pull`);
12997
+ return result;
12998
+ }
12999
+ }
13000
+ log("[EARS-61] No conflicting local changes (or force enabled), proceeding with pull...");
12430
13001
  try {
12431
13002
  await this.git.pullRebase("origin", stateBranch);
12432
13003
  } catch (error) {
@@ -12457,7 +13028,10 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12457
13028
  const hashAfter = commitAfter[0]?.hash;
12458
13029
  const hasNewChanges = hashBefore !== hashAfter;
12459
13030
  result.hasChanges = hasNewChanges;
12460
- if (hasNewChanges || forceReindex) {
13031
+ const indexPath = path6__default.join(pullRepoRoot, ".gitgov", "index.json");
13032
+ const indexExists = await promises.access(indexPath).then(() => true).catch(() => false);
13033
+ const shouldReindex = hasNewChanges || forceReindex || !indexExists;
13034
+ if (shouldReindex) {
12461
13035
  result.reindexed = true;
12462
13036
  if (hasNewChanges && hashBefore && hashAfter) {
12463
13037
  const changedFiles = await this.git.getChangedFiles(
@@ -12467,13 +13041,6 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12467
13041
  );
12468
13042
  result.filesUpdated = changedFiles.length;
12469
13043
  }
12470
- logger6.info("Invoking IndexerAdapter.generateIndex() after pull...");
12471
- try {
12472
- await this.indexer.generateIndex();
12473
- logger6.info("Index regenerated successfully");
12474
- } catch (error) {
12475
- logger6.warn(`Failed to regenerate index: ${error.message}`);
12476
- }
12477
13044
  }
12478
13045
  const gitgovPath = path6__default.join(pullRepoRoot, ".gitgov");
12479
13046
  const gitgovExists = await promises.access(gitgovPath).then(() => true).catch(() => false);
@@ -12518,6 +13085,15 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12518
13085
  logger6.warn(`[pullState] Failed to restore .gitgov/ to filesystem: ${error.message}`);
12519
13086
  }
12520
13087
  }
13088
+ if (shouldReindex) {
13089
+ logger6.info("Invoking IndexerAdapter.generateIndex() after pull...");
13090
+ try {
13091
+ await this.indexer.generateIndex();
13092
+ logger6.info("Index regenerated successfully");
13093
+ } catch (error) {
13094
+ logger6.warn(`Failed to regenerate index: ${error.message}`);
13095
+ }
13096
+ }
12521
13097
  result.success = true;
12522
13098
  log(`=== pullState COMPLETED: ${hasNewChanges ? "new changes pulled" : "no changes"}, reindexed: ${result.reindexed} ===`);
12523
13099
  return result;
@@ -12531,25 +13107,37 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12531
13107
  }
12532
13108
  }
12533
13109
  /**
12534
- * Resolves state conflicts in a governed manner.
12535
- * Updates resolved Records (recalculates checksum and adds resolver signature),
12536
- * creates rebase and resolution commits signed according to protocol.
13110
+ * Resolves state conflicts in a governed manner (Git-Native).
13111
+ *
13112
+ * Git-Native Flow:
13113
+ * 1. User resolves conflicts using standard Git tools (edit files, remove markers)
13114
+ * 2. User stages resolved files: git add .gitgov/
13115
+ * 3. User runs: gitgov sync resolve --reason "reason"
13116
+ *
13117
+ * This method:
13118
+ * - Verifies that a rebase is in progress
13119
+ * - Checks that no conflict markers remain in staged files
13120
+ * - Updates resolved Records with new checksums and signatures
13121
+ * - Continues the git rebase (git rebase --continue)
13122
+ * - Creates a signed resolution commit
13123
+ * - Regenerates the index
12537
13124
  *
12538
13125
  * [EARS-17 through EARS-23]
12539
13126
  */
12540
13127
  async resolveConflict(options) {
12541
13128
  const { reason, actorId } = options;
12542
13129
  const log = (msg) => logger6.debug(`[resolveConflict] ${msg}`);
12543
- log("=== STARTING resolveConflict ===");
12544
- log("Phase 0: Verifying rebase state...");
13130
+ log("=== STARTING resolveConflict (Git-Native) ===");
13131
+ log("Phase 0: Verifying rebase in progress...");
12545
13132
  const rebaseInProgress = await this.isRebaseInProgress();
12546
13133
  if (!rebaseInProgress) {
12547
13134
  throw new NoRebaseInProgressError();
12548
13135
  }
13136
+ log("Conflict mode: rebase_conflict (Git-Native)");
12549
13137
  console.log("[resolveConflict] Getting staged files...");
12550
13138
  const allStagedFiles = await this.git.getStagedFiles();
12551
13139
  console.log("[resolveConflict] All staged files:", allStagedFiles);
12552
- const resolvedRecords = allStagedFiles.filter(
13140
+ let resolvedRecords = allStagedFiles.filter(
12553
13141
  (f) => f.startsWith(".gitgov/") && f.endsWith(".json")
12554
13142
  );
12555
13143
  console.log("[resolveConflict] Resolved Records (staged .gitgov/*.json):", resolvedRecords);
@@ -12559,7 +13147,33 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12559
13147
  if (filesWithMarkers.length > 0) {
12560
13148
  throw new ConflictMarkersPresentError(filesWithMarkers);
12561
13149
  }
12562
- console.log("[resolveConflict] Updating resolved Records...");
13150
+ let rebaseCommitHash = "";
13151
+ console.log("[resolveConflict] Step 4: Calling git.rebaseContinue()...");
13152
+ await this.git.rebaseContinue();
13153
+ console.log("[resolveConflict] rebaseContinue completed successfully");
13154
+ const currentBranch = await this.git.getCurrentBranch();
13155
+ const rebaseCommit = await this.git.getCommitHistory(currentBranch, {
13156
+ maxCount: 1
13157
+ });
13158
+ rebaseCommitHash = rebaseCommit[0]?.hash ?? "";
13159
+ if (resolvedRecords.length === 0 && rebaseCommitHash) {
13160
+ console.log("[resolveConflict] No staged files detected, getting files from rebase commit...");
13161
+ const repoRoot2 = await this.git.getRepoRoot();
13162
+ try {
13163
+ const { stdout } = await execAsync(
13164
+ `git diff-tree --no-commit-id --name-only -r ${rebaseCommitHash}`,
13165
+ { cwd: repoRoot2 }
13166
+ );
13167
+ const commitFiles = stdout.trim().split("\n").filter((f) => f);
13168
+ resolvedRecords = commitFiles.filter(
13169
+ (f) => f.startsWith(".gitgov/") && f.endsWith(".json")
13170
+ );
13171
+ console.log("[resolveConflict] Files from rebase commit:", resolvedRecords);
13172
+ } catch (e) {
13173
+ console.log("[resolveConflict] Could not get files from rebase commit:", e);
13174
+ }
13175
+ }
13176
+ console.log("[resolveConflict] Updating resolved Records with signatures...");
12563
13177
  if (resolvedRecords.length > 0) {
12564
13178
  const currentActor = await this.identity.getCurrentActor();
12565
13179
  console.log("[resolveConflict] Current actor:", currentActor);
@@ -12567,8 +13181,8 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12567
13181
  for (const filePath of resolvedRecords) {
12568
13182
  console.log("[resolveConflict] Processing Record:", filePath);
12569
13183
  try {
12570
- const repoRoot = await this.git.getRepoRoot();
12571
- const fullPath = join(repoRoot, filePath);
13184
+ const repoRoot2 = await this.git.getRepoRoot();
13185
+ const fullPath = join(repoRoot2, filePath);
12572
13186
  const content = readFileSync(fullPath, "utf-8");
12573
13187
  const record = JSON.parse(content);
12574
13188
  if (!record.header || !record.payload) {
@@ -12577,7 +13191,8 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12577
13191
  const signedRecord = await this.identity.signRecord(
12578
13192
  record,
12579
13193
  currentActor.id,
12580
- "resolver"
13194
+ "resolver",
13195
+ `Conflict resolved: ${reason}`
12581
13196
  );
12582
13197
  writeFileSync(fullPath, JSON.stringify(signedRecord, null, 2) + "\n", "utf-8");
12583
13198
  logger6.info(`Updated Record: ${filePath} (new checksum + resolver signature)`);
@@ -12587,20 +13202,12 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
12587
13202
  console.log("[resolveConflict] Error updating Record:", filePath, error);
12588
13203
  }
12589
13204
  }
12590
- console.log("[resolveConflict] All Records updated, re-staging...");
13205
+ console.log("[resolveConflict] All Records updated, staging...");
12591
13206
  }
12592
- console.log("[resolveConflict] Re-staging .gitgov/ with updated metadata...");
13207
+ console.log("[resolveConflict] Staging .gitgov/ with updated metadata...");
12593
13208
  await this.git.add([".gitgov"], { force: true });
12594
- console.log("[resolveConflict] Step 6: Calling git.rebaseContinue() (THIS MAY HANG)...");
12595
- await this.git.rebaseContinue();
12596
- console.log("[resolveConflict] rebaseContinue completed successfully");
12597
- const currentBranch = await this.git.getCurrentBranch();
12598
- const rebaseCommit = await this.git.getCommitHistory(currentBranch, {
12599
- maxCount: 1
12600
- });
12601
- const rebaseCommitHash = rebaseCommit[0]?.hash ?? "";
12602
13209
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12603
- const resolutionMessage = `resolution: Conflict resolved by ${actorId}
13210
+ const resolutionMessage = `resolution: conflict resolved by ${actorId}
12604
13211
 
12605
13212
  Actor: ${actorId}
12606
13213
  Timestamp: ${timestamp}
@@ -12608,9 +13215,116 @@ Reason: ${reason}
12608
13215
  Files: ${resolvedRecords.length} file(s) resolved
12609
13216
 
12610
13217
  Signed-off-by: ${actorId}`;
12611
- const resolutionCommitHash = await this.git.commitAllowEmpty(
12612
- resolutionMessage
12613
- );
13218
+ let resolutionCommitHash = "";
13219
+ try {
13220
+ resolutionCommitHash = await this.git.commit(resolutionMessage);
13221
+ } catch (commitError) {
13222
+ const stdout = commitError.stdout || "";
13223
+ const stderr = commitError.stderr || "";
13224
+ const isNothingToCommit = stdout.includes("nothing to commit") || stderr.includes("nothing to commit") || stdout.includes("nothing added to commit") || stderr.includes("nothing added to commit");
13225
+ if (isNothingToCommit) {
13226
+ log("No additional changes to commit (no records needed re-signing)");
13227
+ resolutionCommitHash = rebaseCommitHash;
13228
+ } else {
13229
+ throw commitError;
13230
+ }
13231
+ }
13232
+ log("Pushing resolved state to remote...");
13233
+ try {
13234
+ await this.git.push("origin", "gitgov-state");
13235
+ log("Push successful");
13236
+ } catch (pushError) {
13237
+ const pushErrorMsg = pushError instanceof Error ? pushError.message : String(pushError);
13238
+ log(`Push failed (non-fatal): ${pushErrorMsg}`);
13239
+ }
13240
+ log("Returning to original branch and restoring .gitgov/ files...");
13241
+ const repoRoot = await this.git.getRepoRoot();
13242
+ const gitgovDir = path6__default.join(repoRoot, ".gitgov");
13243
+ const tempDir = path6__default.join(os.tmpdir(), `gitgov-resolve-${Date.now()}`);
13244
+ await promises.mkdir(tempDir, { recursive: true });
13245
+ log(`Created temp directory for local files: ${tempDir}`);
13246
+ for (const fileName of LOCAL_ONLY_FILES) {
13247
+ const srcPath = path6__default.join(gitgovDir, fileName);
13248
+ const destPath = path6__default.join(tempDir, fileName);
13249
+ try {
13250
+ await promises.access(srcPath);
13251
+ await promises.cp(srcPath, destPath, { force: true });
13252
+ log(`Saved LOCAL_ONLY_FILE to temp: ${fileName}`);
13253
+ } catch {
13254
+ }
13255
+ }
13256
+ const saveExcludedFiles = async (srcDir, destDir) => {
13257
+ try {
13258
+ const entries = await promises.readdir(srcDir, { withFileTypes: true });
13259
+ for (const entry of entries) {
13260
+ const srcPath = path6__default.join(srcDir, entry.name);
13261
+ const dstPath = path6__default.join(destDir, entry.name);
13262
+ if (entry.isDirectory()) {
13263
+ await promises.mkdir(dstPath, { recursive: true });
13264
+ await saveExcludedFiles(srcPath, dstPath);
13265
+ } else {
13266
+ const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
13267
+ if (isExcluded) {
13268
+ await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
13269
+ await promises.copyFile(srcPath, dstPath);
13270
+ log(`Saved EXCLUDED file to temp: ${entry.name}`);
13271
+ }
13272
+ }
13273
+ }
13274
+ } catch {
13275
+ }
13276
+ };
13277
+ await saveExcludedFiles(gitgovDir, tempDir);
13278
+ try {
13279
+ await execAsync("git checkout -", { cwd: repoRoot });
13280
+ log("Returned to original branch");
13281
+ } catch (checkoutError) {
13282
+ log(`Warning: Could not return to original branch: ${checkoutError}`);
13283
+ }
13284
+ log("Restoring .gitgov/ from gitgov-state...");
13285
+ try {
13286
+ await this.git.checkoutFilesFromBranch("gitgov-state", [".gitgov/"]);
13287
+ await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot });
13288
+ log("Restored .gitgov/ from gitgov-state (unstaged)");
13289
+ } catch (checkoutFilesError) {
13290
+ log(`Warning: Could not restore .gitgov/ from gitgov-state: ${checkoutFilesError}`);
13291
+ }
13292
+ for (const fileName of LOCAL_ONLY_FILES) {
13293
+ const srcPath = path6__default.join(tempDir, fileName);
13294
+ const destPath = path6__default.join(gitgovDir, fileName);
13295
+ try {
13296
+ await promises.access(srcPath);
13297
+ await promises.cp(srcPath, destPath, { force: true });
13298
+ log(`Restored LOCAL_ONLY_FILE from temp: ${fileName}`);
13299
+ } catch {
13300
+ }
13301
+ }
13302
+ const restoreExcludedFiles = async (srcDir, destDir) => {
13303
+ try {
13304
+ const entries = await promises.readdir(srcDir, { withFileTypes: true });
13305
+ for (const entry of entries) {
13306
+ const srcPath = path6__default.join(srcDir, entry.name);
13307
+ const dstPath = path6__default.join(destDir, entry.name);
13308
+ if (entry.isDirectory()) {
13309
+ await restoreExcludedFiles(srcPath, dstPath);
13310
+ } else {
13311
+ const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
13312
+ if (isExcluded) {
13313
+ await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
13314
+ await promises.copyFile(srcPath, dstPath);
13315
+ log(`Restored EXCLUDED file from temp: ${entry.name}`);
13316
+ }
13317
+ }
13318
+ }
13319
+ } catch {
13320
+ }
13321
+ };
13322
+ await restoreExcludedFiles(tempDir, gitgovDir);
13323
+ try {
13324
+ await promises.rm(tempDir, { recursive: true, force: true });
13325
+ log("Temp directory cleaned up");
13326
+ } catch {
13327
+ }
12614
13328
  logger6.info("Invoking IndexerAdapter.generateIndex() after conflict resolution...");
12615
13329
  try {
12616
13330
  await this.indexer.generateIndex();