@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.
- package/.gitignore +115 -0
- package/.trace/progress.json +22 -0
- package/README.md +466 -0
- package/RELEASE-NOTES-1.5.0.md +410 -0
- package/STATUS.md +245 -0
- package/dist/auto-commit.d.ts +66 -0
- package/dist/auto-commit.d.ts.map +1 -0
- package/dist/auto-commit.js +180 -0
- package/dist/auto-commit.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +246 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +46 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +256 -0
- package/dist/commands.js.map +1 -0
- package/dist/diff.d.ts +23 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +106 -0
- package/dist/diff.js.map +1 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js.map +1 -0
- package/dist/index-cache.d.ts +35 -0
- package/dist/index-cache.d.ts.map +1 -0
- package/dist/index-cache.js +114 -0
- package/dist/index-cache.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +45 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +151 -0
- package/dist/storage.js.map +1 -0
- package/dist/sync.d.ts +60 -0
- package/dist/sync.js +184 -0
- package/dist/tags.d.ts +85 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +219 -0
- package/dist/tags.js.map +1 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +73 -0
- package/docs/_config.yml +2 -0
- package/docs/index.html +960 -0
- package/docs-website/package.json +20 -0
- package/jest.config.js +21 -0
- package/package.json +50 -0
- package/scripts/init.ts +290 -0
- package/src/agent-audit.ts +270 -0
- package/src/agent-checkout.ts +227 -0
- package/src/agent-coordination.ts +318 -0
- package/src/async-queue.ts +203 -0
- package/src/auto-branching.ts +279 -0
- package/src/auto-commit.ts +166 -0
- package/src/cherry-pick.ts +252 -0
- package/src/chunked-upload.ts +224 -0
- package/src/cli-v2.ts +335 -0
- package/src/cli.ts +318 -0
- package/src/cliff-detection.ts +232 -0
- package/src/commands.ts +267 -0
- package/src/commit-hash-system.ts +351 -0
- package/src/compression.ts +176 -0
- package/src/conflict-resolution-ui.ts +277 -0
- package/src/conflict-visualization.ts +238 -0
- package/src/diff-formatter.ts +184 -0
- package/src/diff.ts +124 -0
- package/src/distributed-coordination.ts +273 -0
- package/src/git-interop.ts +316 -0
- package/src/index-cache.ts +88 -0
- package/src/index.ts +38 -0
- package/src/merge-engine.ts +143 -0
- package/src/message-search.ts +370 -0
- package/src/performance-monitoring.ts +236 -0
- package/src/rebase.ts +327 -0
- package/src/rollback.ts +215 -0
- package/src/semantic-grouping.ts +245 -0
- package/src/stage-area.ts +324 -0
- package/src/stash.ts +278 -0
- package/src/storage.ts +131 -0
- package/src/sync.ts +205 -0
- package/src/tags.ts +244 -0
- package/src/types.ts +119 -0
- package/src/webhooks.ts +119 -0
- package/src/workspace-isolation.ts +298 -0
- package/tests/auto-commit.test.ts +308 -0
- package/tests/checkout.test.ts +136 -0
- package/tests/commit.test.ts +118 -0
- package/tests/diff.test.ts +191 -0
- package/tests/github.test.ts +94 -0
- package/tests/integration.test.ts +267 -0
- package/tests/log.test.ts +125 -0
- package/tests/phase2-integration.test.ts +370 -0
- package/tests/storage.test.ts +167 -0
- package/tests/tags.test.ts +477 -0
- package/tests/types.test.ts +75 -0
- package/tests/v1.1/agent-audit.test.ts +472 -0
- package/tests/v1.1/agent-coordination.test.ts +308 -0
- package/tests/v1.1/async-queue.test.ts +253 -0
- package/tests/v1.1/comprehensive.test.ts +521 -0
- package/tests/v1.1/diff-formatter.test.ts +238 -0
- package/tests/v1.1/integration.test.ts +389 -0
- package/tests/v1.1/onboarding.test.ts +365 -0
- package/tests/v1.1/rollback.test.ts +370 -0
- package/tests/v1.1/semantic-grouping.test.ts +230 -0
- package/tests/v1.2/chunked-upload.test.ts +301 -0
- package/tests/v1.2/cliff-detection.test.ts +272 -0
- package/tests/v1.2/commit-hash-system.test.ts +288 -0
- package/tests/v1.2/compression.test.ts +220 -0
- package/tests/v1.2/conflict-visualization.test.ts +263 -0
- package/tests/v1.2/distributed.test.ts +261 -0
- package/tests/v1.2/performance-monitoring.test.ts +328 -0
- package/tests/v1.3/auto-branching.test.ts +270 -0
- package/tests/v1.3/message-search.test.ts +264 -0
- package/tests/v1.3/stage-area.test.ts +330 -0
- package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
- package/tests/v1.4/cli.test.ts +171 -0
- package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
- package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
- package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
- package/tests/v1.4/workspace-isolation.test.ts +268 -0
- package/tests/v1.5/agent-coordination.real.test.ts +401 -0
- package/tests/v1.5/cli-v2.test.ts +354 -0
- package/tests/v1.5/git-interop.real.test.ts +358 -0
- package/tests/v1.5/integration-testing.real.test.ts +440 -0
- 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
|
+
}
|