@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/README.md +1 -1
- package/dist/src/index.d.ts +185 -23
- package/dist/src/index.js +827 -113
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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,
|
|
3738
|
+
signature = signPayload(record.payload, privateKey, actorId, role, notes);
|
|
3621
3739
|
} else {
|
|
3622
3740
|
signature = {
|
|
3623
3741
|
keyId: actorId,
|
|
3624
3742
|
role,
|
|
3625
|
-
notes
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
7556
|
-
const targetPrompt = path6.join(
|
|
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
|
|
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
|
-
#
|
|
7629
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11821
|
-
|
|
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
|
-
"
|
|
11973
|
-
"
|
|
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
|
-
|
|
12016
|
-
|
|
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
|
-
|
|
12311
|
+
let filesBeforeChanges = /* @__PURE__ */ new Set();
|
|
12039
12312
|
try {
|
|
12040
|
-
await this.git.
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
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(`
|
|
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
|
-
|
|
12278
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
12536
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12571
|
-
const fullPath = join(
|
|
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,
|
|
13205
|
+
console.log("[resolveConflict] All Records updated, staging...");
|
|
12591
13206
|
}
|
|
12592
|
-
console.log("[resolveConflict]
|
|
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:
|
|
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
|
-
|
|
12612
|
-
|
|
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();
|