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