@zoebuildsai/trace 1.5.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.
Files changed (130) hide show
  1. package/.gitignore +115 -0
  2. package/.trace/progress.json +22 -0
  3. package/README.md +466 -0
  4. package/RELEASE-NOTES-1.5.0.md +410 -0
  5. package/STATUS.md +245 -0
  6. package/dist/auto-commit.d.ts +66 -0
  7. package/dist/auto-commit.d.ts.map +1 -0
  8. package/dist/auto-commit.js +180 -0
  9. package/dist/auto-commit.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +246 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands.d.ts +46 -0
  15. package/dist/commands.d.ts.map +1 -0
  16. package/dist/commands.js +256 -0
  17. package/dist/commands.js.map +1 -0
  18. package/dist/diff.d.ts +23 -0
  19. package/dist/diff.d.ts.map +1 -0
  20. package/dist/diff.js +106 -0
  21. package/dist/diff.js.map +1 -0
  22. package/dist/github.d.ts.map +1 -0
  23. package/dist/github.js.map +1 -0
  24. package/dist/index-cache.d.ts +35 -0
  25. package/dist/index-cache.d.ts.map +1 -0
  26. package/dist/index-cache.js +114 -0
  27. package/dist/index-cache.js.map +1 -0
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/storage.d.ts +45 -0
  33. package/dist/storage.d.ts.map +1 -0
  34. package/dist/storage.js +151 -0
  35. package/dist/storage.js.map +1 -0
  36. package/dist/sync.d.ts +60 -0
  37. package/dist/sync.js +184 -0
  38. package/dist/tags.d.ts +85 -0
  39. package/dist/tags.d.ts.map +1 -0
  40. package/dist/tags.js +219 -0
  41. package/dist/tags.js.map +1 -0
  42. package/dist/types.d.ts +102 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +6 -0
  45. package/dist/types.js.map +1 -0
  46. package/docs/.nojekyll +0 -0
  47. package/docs/README.md +73 -0
  48. package/docs/_config.yml +2 -0
  49. package/docs/index.html +960 -0
  50. package/docs-website/package.json +20 -0
  51. package/jest.config.js +21 -0
  52. package/package.json +50 -0
  53. package/scripts/init.ts +290 -0
  54. package/src/agent-audit.ts +270 -0
  55. package/src/agent-checkout.ts +227 -0
  56. package/src/agent-coordination.ts +318 -0
  57. package/src/async-queue.ts +203 -0
  58. package/src/auto-branching.ts +279 -0
  59. package/src/auto-commit.ts +166 -0
  60. package/src/cherry-pick.ts +252 -0
  61. package/src/chunked-upload.ts +224 -0
  62. package/src/cli-v2.ts +335 -0
  63. package/src/cli.ts +318 -0
  64. package/src/cliff-detection.ts +232 -0
  65. package/src/commands.ts +267 -0
  66. package/src/commit-hash-system.ts +351 -0
  67. package/src/compression.ts +176 -0
  68. package/src/conflict-resolution-ui.ts +277 -0
  69. package/src/conflict-visualization.ts +238 -0
  70. package/src/diff-formatter.ts +184 -0
  71. package/src/diff.ts +124 -0
  72. package/src/distributed-coordination.ts +273 -0
  73. package/src/git-interop.ts +316 -0
  74. package/src/index-cache.ts +88 -0
  75. package/src/index.ts +38 -0
  76. package/src/merge-engine.ts +143 -0
  77. package/src/message-search.ts +370 -0
  78. package/src/performance-monitoring.ts +236 -0
  79. package/src/rebase.ts +327 -0
  80. package/src/rollback.ts +215 -0
  81. package/src/semantic-grouping.ts +245 -0
  82. package/src/stage-area.ts +324 -0
  83. package/src/stash.ts +278 -0
  84. package/src/storage.ts +131 -0
  85. package/src/sync.ts +205 -0
  86. package/src/tags.ts +244 -0
  87. package/src/types.ts +119 -0
  88. package/src/webhooks.ts +119 -0
  89. package/src/workspace-isolation.ts +298 -0
  90. package/tests/auto-commit.test.ts +308 -0
  91. package/tests/checkout.test.ts +136 -0
  92. package/tests/commit.test.ts +118 -0
  93. package/tests/diff.test.ts +191 -0
  94. package/tests/github.test.ts +94 -0
  95. package/tests/integration.test.ts +267 -0
  96. package/tests/log.test.ts +125 -0
  97. package/tests/phase2-integration.test.ts +370 -0
  98. package/tests/storage.test.ts +167 -0
  99. package/tests/tags.test.ts +477 -0
  100. package/tests/types.test.ts +75 -0
  101. package/tests/v1.1/agent-audit.test.ts +472 -0
  102. package/tests/v1.1/agent-coordination.test.ts +308 -0
  103. package/tests/v1.1/async-queue.test.ts +253 -0
  104. package/tests/v1.1/comprehensive.test.ts +521 -0
  105. package/tests/v1.1/diff-formatter.test.ts +238 -0
  106. package/tests/v1.1/integration.test.ts +389 -0
  107. package/tests/v1.1/onboarding.test.ts +365 -0
  108. package/tests/v1.1/rollback.test.ts +370 -0
  109. package/tests/v1.1/semantic-grouping.test.ts +230 -0
  110. package/tests/v1.2/chunked-upload.test.ts +301 -0
  111. package/tests/v1.2/cliff-detection.test.ts +272 -0
  112. package/tests/v1.2/commit-hash-system.test.ts +288 -0
  113. package/tests/v1.2/compression.test.ts +220 -0
  114. package/tests/v1.2/conflict-visualization.test.ts +263 -0
  115. package/tests/v1.2/distributed.test.ts +261 -0
  116. package/tests/v1.2/performance-monitoring.test.ts +328 -0
  117. package/tests/v1.3/auto-branching.test.ts +270 -0
  118. package/tests/v1.3/message-search.test.ts +264 -0
  119. package/tests/v1.3/stage-area.test.ts +330 -0
  120. package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
  121. package/tests/v1.4/cli.test.ts +171 -0
  122. package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
  123. package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
  124. package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
  125. package/tests/v1.4/workspace-isolation.test.ts +268 -0
  126. package/tests/v1.5/agent-coordination.real.test.ts +401 -0
  127. package/tests/v1.5/cli-v2.test.ts +354 -0
  128. package/tests/v1.5/git-interop.real.test.ts +358 -0
  129. package/tests/v1.5/integration-testing.real.test.ts +440 -0
  130. package/tsconfig.json +26 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Async Queue for Trace
3
+ * Enables batch commits without blocking agent execution
4
+ */
5
+
6
+ export interface QueuedOperation {
7
+ id: string;
8
+ type: 'commit' | 'push' | 'pull';
9
+ payload: Record<string, unknown>;
10
+ timestamp: number;
11
+ status: 'pending' | 'executing' | 'completed' | 'failed';
12
+ error?: string;
13
+ result?: unknown;
14
+ webhooks?: string[]; // URLs to notify on completion
15
+ }
16
+
17
+ export class AsyncQueue {
18
+ private queue: QueuedOperation[] = [];
19
+ private executing = false;
20
+ private concurrency = 3; // Run up to 3 operations in parallel
21
+
22
+ /**
23
+ * Enqueue an operation
24
+ */
25
+ enqueue(operation: Omit<QueuedOperation, 'id' | 'timestamp' | 'status'>): string {
26
+ const id = `op-${Date.now()}-${Math.random().toString(36).slice(2)}`;
27
+ const queued: QueuedOperation = {
28
+ ...operation,
29
+ id,
30
+ timestamp: Date.now(),
31
+ status: 'pending',
32
+ };
33
+ this.queue.push(queued);
34
+ this.process(); // Start processing if not already running
35
+ return id;
36
+ }
37
+
38
+ /**
39
+ * Get operation status
40
+ */
41
+ getStatus(id: string): QueuedOperation | undefined {
42
+ return this.queue.find(op => op.id === id);
43
+ }
44
+
45
+ /**
46
+ * Process queue (async, non-blocking)
47
+ */
48
+ private async process(): Promise<void> {
49
+ if (this.executing) return;
50
+ this.executing = true;
51
+
52
+ while (this.queue.length > 0) {
53
+ const batch = this.queue
54
+ .filter(op => op.status === 'pending')
55
+ .slice(0, this.concurrency);
56
+
57
+ if (batch.length === 0) break;
58
+
59
+ // Mark as executing
60
+ batch.forEach(op => { op.status = 'executing'; });
61
+
62
+ // Execute in parallel
63
+ const results = await Promise.allSettled(
64
+ batch.map(op => this.executeOperation(op))
65
+ );
66
+
67
+ // Update statuses
68
+ batch.forEach((op, index) => {
69
+ const result = results[index];
70
+ if (result.status === 'fulfilled') {
71
+ op.status = 'completed';
72
+ op.result = result.value;
73
+ this.notifyWebhooks(op);
74
+ } else {
75
+ op.status = 'failed';
76
+ op.error = String(result.reason);
77
+ this.notifyWebhooks(op);
78
+ }
79
+ });
80
+ }
81
+
82
+ this.executing = false;
83
+ }
84
+
85
+ /**
86
+ * Execute a single operation (stub - to be implemented)
87
+ */
88
+ private async executeOperation(op: QueuedOperation): Promise<unknown> {
89
+ // Implementation would go here
90
+ return { success: true };
91
+ }
92
+
93
+ /**
94
+ * Notify webhooks safely
95
+ */
96
+ private async notifyWebhooks(op: QueuedOperation): Promise<void> {
97
+ if (!op.webhooks || op.webhooks.length === 0) return;
98
+
99
+ for (const url of op.webhooks) {
100
+ try {
101
+ // Validate URL
102
+ if (!this.isValidUrl(url)) {
103
+ console.warn(`Invalid webhook URL: ${url}`);
104
+ continue;
105
+ }
106
+
107
+ // Sanitize payload (remove secrets)
108
+ const payload = this.sanitizePayload({
109
+ operationId: op.id,
110
+ type: op.type,
111
+ status: op.status,
112
+ timestamp: op.timestamp,
113
+ error: op.error,
114
+ // result is intentionally excluded to avoid leaking data
115
+ });
116
+
117
+ // Send webhook with retries
118
+ await this.sendWebhookWithRetry(url, payload, 3);
119
+ } catch (err) {
120
+ console.error(`Failed to notify webhook ${url}:`, err);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Send webhook with retry logic
127
+ */
128
+ private async sendWebhookWithRetry(
129
+ url: string,
130
+ payload: unknown,
131
+ maxRetries: number
132
+ ): Promise<void> {
133
+ let lastError: Error | undefined;
134
+
135
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
136
+ try {
137
+ // Note: In real implementation, would use fetch or similar
138
+ // For now, this is a stub that would call the webhook
139
+ await this.postWebhook(url, payload);
140
+ return;
141
+ } catch (err) {
142
+ lastError = err instanceof Error ? err : new Error(String(err));
143
+ const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
144
+ await new Promise(resolve => setTimeout(resolve, delay));
145
+ }
146
+ }
147
+
148
+ throw lastError || new Error('Failed to send webhook after retries');
149
+ }
150
+
151
+ /**
152
+ * Post to webhook (stub)
153
+ */
154
+ private async postWebhook(url: string, payload: unknown): Promise<void> {
155
+ // Stub - would use fetch in real implementation
156
+ }
157
+
158
+ /**
159
+ * Validate webhook URL
160
+ */
161
+ private isValidUrl(url: string): boolean {
162
+ try {
163
+ const parsed = new URL(url);
164
+ // Only allow https for security
165
+ if (parsed.protocol !== 'https:') return false;
166
+ // Reject localhost in production
167
+ if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
168
+ return false;
169
+ }
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Sanitize payload to remove secrets
178
+ */
179
+ private sanitizePayload(payload: Record<string, unknown>): Record<string, unknown> {
180
+ const sanitized = { ...payload };
181
+ // Remove any fields that might contain secrets
182
+ const secretFields = ['password', 'token', 'secret', 'key', 'apiKey'];
183
+ for (const field of secretFields) {
184
+ delete sanitized[field];
185
+ }
186
+ return sanitized;
187
+ }
188
+
189
+ /**
190
+ * Get queue stats
191
+ */
192
+ getStats() {
193
+ return {
194
+ total: this.queue.length,
195
+ pending: this.queue.filter(op => op.status === 'pending').length,
196
+ executing: this.queue.filter(op => op.status === 'executing').length,
197
+ completed: this.queue.filter(op => op.status === 'completed').length,
198
+ failed: this.queue.filter(op => op.status === 'failed').length,
199
+ };
200
+ }
201
+ }
202
+
203
+ export default AsyncQueue;
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Auto-Branching for Trace
3
+ * Automatically create & manage agent-per-task branches
4
+ */
5
+
6
+ export interface BranchInfo {
7
+ name: string;
8
+ agent: string;
9
+ task: string;
10
+ created: number;
11
+ lastModified: number;
12
+ headCommit: string;
13
+ parentBranch: string;
14
+ isActive: boolean;
15
+ }
16
+
17
+ export interface BranchStats {
18
+ totalBranches: number;
19
+ activeBranches: number;
20
+ commitsByBranch: Map<string, number>;
21
+ oldestBranch: BranchInfo | undefined;
22
+ newestBranch: BranchInfo | undefined;
23
+ }
24
+
25
+ export class AutoBranching {
26
+ private branches: Map<string, BranchInfo> = new Map();
27
+ private taskAgentMap: Map<string, string> = new Map(); // task -> agent
28
+ private agentBranches: Map<string, string[]> = new Map(); // agent -> [branches]
29
+ private commitCounts: Map<string, number> = new Map(); // branch -> commit count
30
+
31
+ /**
32
+ * Create branch for agent task
33
+ */
34
+ createBranchForTask(
35
+ agent: string,
36
+ task: string,
37
+ parentBranch: string = 'main',
38
+ headCommit: string = ''
39
+ ): BranchInfo {
40
+ // Normalize branch name: agent-task format
41
+ const branchName = `${agent}/${task}`.toLowerCase().replace(/\s+/g, '-');
42
+
43
+ if (this.branches.has(branchName)) {
44
+ throw new Error(`Branch ${branchName} already exists`);
45
+ }
46
+
47
+ const branch: BranchInfo = {
48
+ name: branchName,
49
+ agent,
50
+ task,
51
+ created: Date.now(),
52
+ lastModified: Date.now(),
53
+ headCommit,
54
+ parentBranch,
55
+ isActive: true,
56
+ };
57
+
58
+ this.branches.set(branchName, branch);
59
+ this.taskAgentMap.set(task, agent);
60
+
61
+ if (!this.agentBranches.has(agent)) {
62
+ this.agentBranches.set(agent, []);
63
+ }
64
+ this.agentBranches.get(agent)!.push(branchName);
65
+ this.commitCounts.set(branchName, 0);
66
+
67
+ return branch;
68
+ }
69
+
70
+ /**
71
+ * Get or create branch for agent (auto-detect from context)
72
+ */
73
+ getOrCreateBranch(agent: string, task: string): BranchInfo {
74
+ const branchName = `${agent}/${task}`.toLowerCase().replace(/\s+/g, '-');
75
+
76
+ if (this.branches.has(branchName)) {
77
+ return this.branches.get(branchName)!;
78
+ }
79
+
80
+ return this.createBranchForTask(agent, task);
81
+ }
82
+
83
+ /**
84
+ * Get branch by name
85
+ */
86
+ getBranch(name: string): BranchInfo | undefined {
87
+ return this.branches.get(name);
88
+ }
89
+
90
+ /**
91
+ * Get all branches for agent
92
+ */
93
+ getAgentBranches(agent: string): BranchInfo[] {
94
+ const branchNames = this.agentBranches.get(agent) || [];
95
+ return branchNames
96
+ .map(name => this.branches.get(name))
97
+ .filter((b): b is BranchInfo => b !== undefined);
98
+ }
99
+
100
+ /**
101
+ * List all branches
102
+ */
103
+ listBranches(activeOnly: boolean = false): BranchInfo[] {
104
+ const all = Array.from(this.branches.values());
105
+ if (activeOnly) {
106
+ return all.filter(b => b.isActive);
107
+ }
108
+ return all;
109
+ }
110
+
111
+ /**
112
+ * Record commit on branch
113
+ */
114
+ recordCommit(branchName: string, commitHash: string): void {
115
+ const branch = this.branches.get(branchName);
116
+ if (!branch) {
117
+ throw new Error(`Branch ${branchName} not found`);
118
+ }
119
+
120
+ branch.lastModified = Date.now();
121
+ branch.headCommit = commitHash;
122
+ const count = (this.commitCounts.get(branchName) || 0) + 1;
123
+ this.commitCounts.set(branchName, count);
124
+ }
125
+
126
+ /**
127
+ * Merge branch (mark complete, deactivate)
128
+ */
129
+ mergeBranch(branchName: string): BranchInfo {
130
+ const branch = this.branches.get(branchName);
131
+ if (!branch) {
132
+ throw new Error(`Branch ${branchName} not found`);
133
+ }
134
+
135
+ branch.isActive = false;
136
+ branch.lastModified = Date.now();
137
+
138
+ return branch;
139
+ }
140
+
141
+ /**
142
+ * Merge and delete branch
143
+ */
144
+ mergeBranchAndDelete(branchName: string): BranchInfo {
145
+ const branch = this.mergeBranch(branchName);
146
+
147
+ // Remove from agent branches list
148
+ const agentBranches = this.agentBranches.get(branch.agent) || [];
149
+ this.agentBranches.set(
150
+ branch.agent,
151
+ agentBranches.filter(b => b !== branchName)
152
+ );
153
+
154
+ // Could optionally keep archived, or delete
155
+ // For now, keep for history
156
+ return branch;
157
+ }
158
+
159
+ /**
160
+ * Checkout branch (for agent)
161
+ */
162
+ checkoutBranch(branchName: string): BranchInfo {
163
+ const branch = this.branches.get(branchName);
164
+ if (!branch) {
165
+ throw new Error(`Branch ${branchName} not found`);
166
+ }
167
+
168
+ // In real implementation, would restore working directory
169
+ // to branch's head commit
170
+ return branch;
171
+ }
172
+
173
+ /**
174
+ * Get stats
175
+ */
176
+ getStats(): BranchStats {
177
+ const branches = Array.from(this.branches.values());
178
+ const activeBranches = branches.filter(b => b.isActive);
179
+
180
+ const commitsByBranch = new Map<string, number>();
181
+ for (const [branchName, count] of this.commitCounts) {
182
+ commitsByBranch.set(branchName, count);
183
+ }
184
+
185
+ let oldest = branches[0];
186
+ let newest = branches[0];
187
+ for (const branch of branches) {
188
+ if (branch.created < (oldest?.created || Infinity)) oldest = branch;
189
+ if (branch.created > (newest?.created || 0)) newest = branch;
190
+ }
191
+
192
+ return {
193
+ totalBranches: branches.length,
194
+ activeBranches: activeBranches.length,
195
+ commitsByBranch,
196
+ oldestBranch: oldest,
197
+ newestBranch: newest,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Detect stale branches (no commits in N days)
203
+ */
204
+ detectStaleBranches(daysOld: number = 7): BranchInfo[] {
205
+ const threshold = Date.now() - daysOld * 24 * 60 * 60 * 1000;
206
+ return Array.from(this.branches.values()).filter(
207
+ b => b.isActive && b.lastModified < threshold
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Get branch tree (parent-child relationships)
213
+ */
214
+ getBranchTree(): Map<string, BranchInfo[]> {
215
+ const tree = new Map<string, BranchInfo[]>();
216
+
217
+ for (const branch of this.branches.values()) {
218
+ if (!tree.has(branch.parentBranch)) {
219
+ tree.set(branch.parentBranch, []);
220
+ }
221
+ tree.get(branch.parentBranch)!.push(branch);
222
+ }
223
+
224
+ return tree;
225
+ }
226
+
227
+ /**
228
+ * Format branch list for display
229
+ */
230
+ static formatBranchList(branches: BranchInfo[]): string {
231
+ if (branches.length === 0) {
232
+ return 'No branches found.';
233
+ }
234
+
235
+ let output = `\n📦 BRANCHES (${branches.length} total):\n`;
236
+
237
+ for (const branch of branches.sort((a, b) => b.created - a.created)) {
238
+ const status = branch.isActive ? '✓' : '✗';
239
+ output += `${status} ${branch.name}\n`;
240
+ output += ` Agent: ${branch.agent} | Task: ${branch.task}\n`;
241
+ output += ` Head: ${branch.headCommit.substring(0, 7)} | Parent: ${branch.parentBranch}\n`;
242
+ output += ` Created: ${new Date(branch.created).toISOString()}\n`;
243
+ }
244
+
245
+ return output;
246
+ }
247
+
248
+ /**
249
+ * Rebase branch (move to new parent)
250
+ */
251
+ rebaseBranch(branchName: string, newParent: string): BranchInfo {
252
+ const branch = this.branches.get(branchName);
253
+ if (!branch) {
254
+ throw new Error(`Branch ${branchName} not found`);
255
+ }
256
+
257
+ const parentBranch = this.branches.get(newParent);
258
+ if (!parentBranch) {
259
+ throw new Error(`Parent branch ${newParent} not found`);
260
+ }
261
+
262
+ branch.parentBranch = newParent;
263
+ branch.lastModified = Date.now();
264
+
265
+ return branch;
266
+ }
267
+
268
+ /**
269
+ * Clear branch (for testing)
270
+ */
271
+ clearBranches(): void {
272
+ this.branches.clear();
273
+ this.taskAgentMap.clear();
274
+ this.agentBranches.clear();
275
+ this.commitCounts.clear();
276
+ }
277
+ }
278
+
279
+ export default AutoBranching;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Auto-Commit Feature
3
+ *
4
+ * Watches memory directory for changes and auto-commits on a timer.
5
+ * - Polling-based change detection
6
+ * - Configurable interval (default 60s)
7
+ * - Skips if no changes detected
8
+ * - Batches rapid changes
9
+ * - Marks commits with [auto] prefix
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { TraceCommands } from './commands';
15
+
16
+ export interface AutoCommitterOptions {
17
+ interval?: number; // milliseconds, default 60000
18
+ ignorePatterns?: string[];
19
+ workdir?: string;
20
+ }
21
+
22
+ export class AutoCommitter {
23
+ private commands: TraceCommands;
24
+ private interval: number = 60000; // default 60s
25
+ private isRunningFlag: boolean = false;
26
+ private watcher: fs.FSWatcher | null = null;
27
+ private lastCommitHash: string | null = null;
28
+ private changeQueue: Set<string> = new Set();
29
+ private ignorePatterns: string[] = ['node_modules', '.git', '.memory-git'];
30
+ private workdir: string;
31
+ private commitTimer: NodeJS.Timeout | null = null;
32
+ private lastSeenState: Map<string, number> = new Map();
33
+
34
+ constructor(commands: TraceCommands, workdir?: string) {
35
+ this.commands = commands;
36
+ this.workdir = workdir || process.cwd();
37
+ }
38
+
39
+ /**
40
+ * Start auto-commit watch
41
+ */
42
+ start(options: AutoCommitterOptions = {}): void {
43
+ if (this.isRunningFlag) return;
44
+
45
+ this.interval = options.interval || 60000;
46
+ if (options.ignorePatterns) {
47
+ this.ignorePatterns = options.ignorePatterns;
48
+ }
49
+
50
+ this.isRunningFlag = true;
51
+ this.setupWatcher();
52
+ this.scheduleCommit();
53
+ }
54
+
55
+ /**
56
+ * Stop auto-commit watch
57
+ */
58
+ stop(): void {
59
+ if (this.watcher) {
60
+ try {
61
+ this.watcher.close();
62
+ } catch (e) {
63
+ // Already closed
64
+ }
65
+ this.watcher = null;
66
+ }
67
+ if (this.commitTimer) {
68
+ clearTimeout(this.commitTimer);
69
+ this.commitTimer = null;
70
+ }
71
+ this.isRunningFlag = false;
72
+ this.changeQueue.clear();
73
+ }
74
+
75
+ /**
76
+ * Check if auto-committer is running
77
+ */
78
+ isRunning_(): boolean {
79
+ return this.isRunningFlag;
80
+ }
81
+
82
+ /**
83
+ * Get current interval in milliseconds
84
+ */
85
+ getInterval(): number {
86
+ return this.interval;
87
+ }
88
+
89
+ /**
90
+ * Set new interval
91
+ */
92
+ setInterval(ms: number): void {
93
+ this.interval = ms;
94
+ if (this.isRunningFlag) {
95
+ this.stop();
96
+ this.start({ interval: ms, ignorePatterns: this.ignorePatterns });
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Private: Setup file system watcher
102
+ */
103
+ private setupWatcher(): void {
104
+ try {
105
+ this.watcher = fs.watch(this.workdir, { recursive: true }, (eventType, filename) => {
106
+ if (filename && !this.shouldIgnore(filename)) {
107
+ this.changeQueue.add(filename);
108
+ }
109
+ });
110
+ } catch (err) {
111
+ // fs.watch may fail on some systems; continue with polling only
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Private: Schedule periodic commit check
117
+ */
118
+ private scheduleCommit(): void {
119
+ if (!this.isRunningFlag) return;
120
+
121
+ this.commitTimer = setTimeout(() => {
122
+ this.checkAndCommit();
123
+ this.scheduleCommit();
124
+ }, this.interval);
125
+ }
126
+
127
+ /**
128
+ * Private: Check for changes and commit if any
129
+ */
130
+ private checkAndCommit(): void {
131
+ try {
132
+ const status = this.commands.status();
133
+
134
+ // Skip if clean
135
+ if (status.clean) {
136
+ return;
137
+ }
138
+
139
+ // Count changed files
140
+ const totalChanges = (status.modified?.length || 0) +
141
+ (status.added?.length || 0) +
142
+ (status.deleted?.length || 0);
143
+
144
+ const message = `[auto] Memory snapshot (files: ${totalChanges})`;
145
+ const metadata = {
146
+ auto: true,
147
+ filesChanged: totalChanges,
148
+ timestamp: Date.now(),
149
+ };
150
+
151
+ this.lastCommitHash = this.commands.commit(message, 'agent', metadata);
152
+ this.changeQueue.clear();
153
+ } catch (err) {
154
+ // Log but don't throw
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Private: Check if file should be ignored
160
+ */
161
+ private shouldIgnore(filename: string): boolean {
162
+ return this.ignorePatterns.some(pattern => {
163
+ return filename.includes(pattern);
164
+ });
165
+ }
166
+ }