@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,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Checkout System for Trace
|
|
3
|
+
* Restore working directory to specific commit (like git checkout)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Commit } from './commit-hash-system';
|
|
7
|
+
|
|
8
|
+
export interface CheckoutResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
hash: string;
|
|
11
|
+
filesRestored: string[];
|
|
12
|
+
filesRemoved: string[];
|
|
13
|
+
message: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkingDirectory {
|
|
18
|
+
currentHash: string;
|
|
19
|
+
currentCommit?: Commit;
|
|
20
|
+
files: Map<string, string>;
|
|
21
|
+
dirty: boolean;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AgentCheckout {
|
|
26
|
+
private workingDirectory: WorkingDirectory;
|
|
27
|
+
private checkoutHistory: CheckoutResult[] = [];
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
this.workingDirectory = {
|
|
31
|
+
currentHash: '',
|
|
32
|
+
files: new Map(),
|
|
33
|
+
dirty: false,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checkout commit (restore working directory)
|
|
40
|
+
*/
|
|
41
|
+
checkout(commit: Commit, force: boolean = false): CheckoutResult {
|
|
42
|
+
// Check for uncommitted changes
|
|
43
|
+
if (this.workingDirectory.dirty && !force) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Working directory has uncommitted changes. Use force=true to discard.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filesRestored: string[] = [];
|
|
50
|
+
const filesRemoved: string[] = [];
|
|
51
|
+
const oldFiles = new Map(this.workingDirectory.files);
|
|
52
|
+
|
|
53
|
+
// Apply new files
|
|
54
|
+
this.workingDirectory.files.clear();
|
|
55
|
+
for (const [path, content] of commit.files) {
|
|
56
|
+
this.workingDirectory.files.set(path, content);
|
|
57
|
+
if (!oldFiles.has(path)) {
|
|
58
|
+
filesRestored.push(path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Identify removed files
|
|
63
|
+
for (const path of oldFiles.keys()) {
|
|
64
|
+
if (!this.workingDirectory.files.has(path)) {
|
|
65
|
+
filesRemoved.push(path);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.workingDirectory.currentHash = commit.hash.full;
|
|
70
|
+
this.workingDirectory.currentCommit = commit;
|
|
71
|
+
this.workingDirectory.dirty = false;
|
|
72
|
+
this.workingDirectory.timestamp = Date.now();
|
|
73
|
+
|
|
74
|
+
const result: CheckoutResult = {
|
|
75
|
+
success: true,
|
|
76
|
+
hash: commit.hash.full,
|
|
77
|
+
filesRestored,
|
|
78
|
+
filesRemoved,
|
|
79
|
+
message: `Checked out ${commit.hash.short}: ${commit.message}`,
|
|
80
|
+
timestamp: this.workingDirectory.timestamp,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.checkoutHistory.push(result);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checkout by hash (full or short)
|
|
89
|
+
*/
|
|
90
|
+
checkoutByHash(
|
|
91
|
+
commits: Map<string, Commit>,
|
|
92
|
+
hash: string,
|
|
93
|
+
force: boolean = false
|
|
94
|
+
): CheckoutResult {
|
|
95
|
+
let commit = commits.get(hash);
|
|
96
|
+
|
|
97
|
+
// Try short hash
|
|
98
|
+
if (!commit) {
|
|
99
|
+
for (const [fullHash, c] of commits) {
|
|
100
|
+
if (fullHash.startsWith(hash)) {
|
|
101
|
+
commit = c;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!commit) {
|
|
108
|
+
throw new Error(`Commit ${hash} not found`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this.checkout(commit, force);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get current working directory state
|
|
116
|
+
*/
|
|
117
|
+
getWorkingDirectory(): WorkingDirectory {
|
|
118
|
+
return {
|
|
119
|
+
...this.workingDirectory,
|
|
120
|
+
files: new Map(this.workingDirectory.files),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get file at current commit
|
|
126
|
+
*/
|
|
127
|
+
getFile(path: string): string | undefined {
|
|
128
|
+
return this.workingDirectory.files.get(path);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List all files in working directory
|
|
133
|
+
*/
|
|
134
|
+
listFiles(): string[] {
|
|
135
|
+
return Array.from(this.workingDirectory.files.keys()).sort();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if working directory is clean
|
|
140
|
+
*/
|
|
141
|
+
isClean(): boolean {
|
|
142
|
+
return !this.workingDirectory.dirty;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mark working directory as dirty (files changed)
|
|
147
|
+
*/
|
|
148
|
+
markDirty(): void {
|
|
149
|
+
this.workingDirectory.dirty = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get checkout history
|
|
154
|
+
*/
|
|
155
|
+
getCheckoutHistory(): CheckoutResult[] {
|
|
156
|
+
return [...this.checkoutHistory];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get current commit
|
|
161
|
+
*/
|
|
162
|
+
getCurrentCommit(): Commit | undefined {
|
|
163
|
+
return this.workingDirectory.currentCommit;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get current hash
|
|
168
|
+
*/
|
|
169
|
+
getCurrentHash(): string {
|
|
170
|
+
return this.workingDirectory.currentHash;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Diff between current and another commit
|
|
175
|
+
*/
|
|
176
|
+
diffWithCommit(otherCommit: Commit): {
|
|
177
|
+
added: string[];
|
|
178
|
+
removed: string[];
|
|
179
|
+
modified: string[];
|
|
180
|
+
} {
|
|
181
|
+
const added: string[] = [];
|
|
182
|
+
const removed: string[] = [];
|
|
183
|
+
const modified: string[] = [];
|
|
184
|
+
|
|
185
|
+
// Find added/modified files
|
|
186
|
+
for (const [path, content] of otherCommit.files) {
|
|
187
|
+
if (!this.workingDirectory.files.has(path)) {
|
|
188
|
+
added.push(path);
|
|
189
|
+
} else if (this.workingDirectory.files.get(path) !== content) {
|
|
190
|
+
modified.push(path);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Find removed files
|
|
195
|
+
for (const path of this.workingDirectory.files.keys()) {
|
|
196
|
+
if (!otherCommit.files.has(path)) {
|
|
197
|
+
removed.push(path);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { added, removed, modified };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reset to specific file version from commit
|
|
206
|
+
*/
|
|
207
|
+
restoreFile(path: string, commit: Commit): boolean {
|
|
208
|
+
if (!commit.files.has(path)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.workingDirectory.files.set(path, commit.files.get(path)!);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Restore all files from commit
|
|
218
|
+
*/
|
|
219
|
+
restoreAll(commit: Commit): void {
|
|
220
|
+
this.workingDirectory.files.clear();
|
|
221
|
+
for (const [path, content] of commit.files) {
|
|
222
|
+
this.workingDirectory.files.set(path, content);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default AgentCheckout;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Coordination Layer
|
|
3
|
+
* The REAL value proposition: prevent agents from stepping on each other
|
|
4
|
+
* This is what git doesn't solve
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface AgentTask {
|
|
8
|
+
taskId: string;
|
|
9
|
+
agent: string;
|
|
10
|
+
files: string[]; // Files this task will modify
|
|
11
|
+
priority: number; // Higher = goes first
|
|
12
|
+
status: 'pending' | 'assigned' | 'executing' | 'completed' | 'blocked';
|
|
13
|
+
assignedAt?: number;
|
|
14
|
+
completedAt?: number;
|
|
15
|
+
blockedBy?: string[]; // Other task IDs blocking this
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CoordinationPolicy {
|
|
19
|
+
allowParallel: boolean; // Can tasks run in parallel?
|
|
20
|
+
maxConcurrent: number; // How many tasks at once?
|
|
21
|
+
lockTimeout: number; // How long to hold lock before auto-release?
|
|
22
|
+
conflictStrategy: 'block' | 'queue' | 'merge';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AgentCoordination {
|
|
26
|
+
private tasks: Map<string, AgentTask> = new Map();
|
|
27
|
+
private fileLocks: Map<string, string> = new Map(); // file -> agentId
|
|
28
|
+
private policy: CoordinationPolicy;
|
|
29
|
+
|
|
30
|
+
constructor(policy: Partial<CoordinationPolicy> = {}) {
|
|
31
|
+
this.policy = {
|
|
32
|
+
allowParallel: true,
|
|
33
|
+
maxConcurrent: 10,
|
|
34
|
+
lockTimeout: 5 * 60 * 1000, // 5 minutes
|
|
35
|
+
conflictStrategy: 'block',
|
|
36
|
+
...policy,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register a task from an agent
|
|
42
|
+
* Returns whether task can start immediately or is blocked
|
|
43
|
+
*/
|
|
44
|
+
registerTask(agent: string, files: string[], priority: number = 0): {
|
|
45
|
+
taskId: string;
|
|
46
|
+
canStart: boolean;
|
|
47
|
+
blockedBy?: string[];
|
|
48
|
+
} {
|
|
49
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
50
|
+
|
|
51
|
+
const task: AgentTask = {
|
|
52
|
+
taskId,
|
|
53
|
+
agent,
|
|
54
|
+
files,
|
|
55
|
+
priority,
|
|
56
|
+
status: 'pending',
|
|
57
|
+
assignedAt: Date.now(),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Check for conflicts
|
|
61
|
+
const conflicts = this.detectConflicts(agent, files);
|
|
62
|
+
|
|
63
|
+
if (conflicts.length > 0) {
|
|
64
|
+
if (this.policy.conflictStrategy === 'block') {
|
|
65
|
+
task.status = 'blocked';
|
|
66
|
+
task.blockedBy = conflicts;
|
|
67
|
+
this.tasks.set(taskId, task);
|
|
68
|
+
return { taskId, canStart: false, blockedBy: conflicts };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
task.status = 'executing';
|
|
73
|
+
this.tasks.set(taskId, task);
|
|
74
|
+
return { taskId, canStart: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect if agent's files conflict with existing tasks
|
|
79
|
+
*/
|
|
80
|
+
private detectConflicts(agent: string, files: string[]): string[] {
|
|
81
|
+
const conflicts: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const task of this.tasks.values()) {
|
|
84
|
+
if (task.agent === agent || task.status === 'completed') {
|
|
85
|
+
continue; // Same agent or finished task
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if any files overlap
|
|
89
|
+
const overlap = task.files.some(f => files.includes(f));
|
|
90
|
+
if (overlap) {
|
|
91
|
+
conflicts.push(task.taskId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return conflicts;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request lock on a file for exclusive access
|
|
100
|
+
*/
|
|
101
|
+
requestLock(file: string, agent: string): {
|
|
102
|
+
locked: boolean;
|
|
103
|
+
owner?: string;
|
|
104
|
+
waitTime?: number;
|
|
105
|
+
} {
|
|
106
|
+
// Check if already locked by someone else
|
|
107
|
+
const owner = this.fileLocks.get(file);
|
|
108
|
+
|
|
109
|
+
if (owner && owner !== agent) {
|
|
110
|
+
return { locked: false, owner };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Lock it
|
|
114
|
+
this.fileLocks.set(file, agent);
|
|
115
|
+
return { locked: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Release lock
|
|
120
|
+
*/
|
|
121
|
+
releaseLock(file: string, agent: string): boolean {
|
|
122
|
+
const owner = this.fileLocks.get(file);
|
|
123
|
+
if (owner !== agent) {
|
|
124
|
+
return false; // Not the owner
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.fileLocks.delete(file);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check what tasks are blocked and why
|
|
133
|
+
*/
|
|
134
|
+
getBlockedTasks(): Array<{ taskId: string; agent: string; blockedBy: string[] }> {
|
|
135
|
+
const blocked: Array<{ taskId: string; agent: string; blockedBy: string[] }> = [];
|
|
136
|
+
|
|
137
|
+
for (const task of this.tasks.values()) {
|
|
138
|
+
if (task.status === 'blocked' && task.blockedBy) {
|
|
139
|
+
blocked.push({
|
|
140
|
+
taskId: task.taskId,
|
|
141
|
+
agent: task.agent,
|
|
142
|
+
blockedBy: task.blockedBy,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return blocked;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get current lock holder for file
|
|
152
|
+
*/
|
|
153
|
+
getLockHolder(file: string): string | undefined {
|
|
154
|
+
return this.fileLocks.get(file);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Complete task and auto-unblock dependent tasks
|
|
159
|
+
*/
|
|
160
|
+
completeTask(taskId: string): {
|
|
161
|
+
success: boolean;
|
|
162
|
+
unblocked: string[];
|
|
163
|
+
} {
|
|
164
|
+
const task = this.tasks.get(taskId);
|
|
165
|
+
if (!task) {
|
|
166
|
+
return { success: false, unblocked: [] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
task.status = 'completed';
|
|
170
|
+
task.completedAt = Date.now();
|
|
171
|
+
|
|
172
|
+
// Release locks
|
|
173
|
+
for (const file of task.files) {
|
|
174
|
+
this.releaseLock(file, task.agent);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Find blocked tasks and try to unblock
|
|
178
|
+
const unblocked: string[] = [];
|
|
179
|
+
for (const otherTask of this.tasks.values()) {
|
|
180
|
+
if (otherTask.status === 'blocked' && otherTask.blockedBy?.includes(taskId)) {
|
|
181
|
+
const newBlockers = (otherTask.blockedBy || []).filter(b => b !== taskId);
|
|
182
|
+
|
|
183
|
+
if (newBlockers.length === 0) {
|
|
184
|
+
otherTask.status = 'executing';
|
|
185
|
+
unblocked.push(otherTask.taskId);
|
|
186
|
+
} else {
|
|
187
|
+
otherTask.blockedBy = newBlockers;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { success: true, unblocked };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get task status
|
|
197
|
+
*/
|
|
198
|
+
getTaskStatus(taskId: string): AgentTask | undefined {
|
|
199
|
+
return this.tasks.get(taskId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get all active tasks for agent
|
|
204
|
+
*/
|
|
205
|
+
getAgentTasks(agent: string): AgentTask[] {
|
|
206
|
+
return Array.from(this.tasks.values()).filter(t => t.agent === agent);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get coordination statistics
|
|
211
|
+
*/
|
|
212
|
+
getStats(): {
|
|
213
|
+
totalTasks: number;
|
|
214
|
+
executing: number;
|
|
215
|
+
blocked: number;
|
|
216
|
+
completed: number;
|
|
217
|
+
currentLocks: number;
|
|
218
|
+
avgWaitTime: number;
|
|
219
|
+
} {
|
|
220
|
+
let executing = 0;
|
|
221
|
+
let blocked = 0;
|
|
222
|
+
let completed = 0;
|
|
223
|
+
let totalWait = 0;
|
|
224
|
+
|
|
225
|
+
for (const task of this.tasks.values()) {
|
|
226
|
+
switch (task.status) {
|
|
227
|
+
case 'executing':
|
|
228
|
+
executing++;
|
|
229
|
+
break;
|
|
230
|
+
case 'blocked':
|
|
231
|
+
blocked++;
|
|
232
|
+
if (task.assignedAt) {
|
|
233
|
+
totalWait += Date.now() - task.assignedAt;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
case 'completed':
|
|
237
|
+
completed++;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
totalTasks: this.tasks.size,
|
|
244
|
+
executing,
|
|
245
|
+
blocked,
|
|
246
|
+
completed,
|
|
247
|
+
currentLocks: this.fileLocks.size,
|
|
248
|
+
avgWaitTime: blocked > 0 ? Math.round(totalWait / blocked) : 0,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Suggest optimal task ordering
|
|
254
|
+
*/
|
|
255
|
+
suggestOrder(): string[] {
|
|
256
|
+
// Sort by priority, then by dependencies
|
|
257
|
+
return Array.from(this.tasks.values())
|
|
258
|
+
.filter(t => t.status !== 'completed')
|
|
259
|
+
.sort((a, b) => {
|
|
260
|
+
// Higher priority first
|
|
261
|
+
if (b.priority !== a.priority) {
|
|
262
|
+
return b.priority - a.priority;
|
|
263
|
+
}
|
|
264
|
+
// Blocked tasks last
|
|
265
|
+
if ((a.blockedBy?.length || 0) !== (b.blockedBy?.length || 0)) {
|
|
266
|
+
return (a.blockedBy?.length || 0) - (b.blockedBy?.length || 0);
|
|
267
|
+
}
|
|
268
|
+
// Older first
|
|
269
|
+
return (a.assignedAt || 0) - (b.assignedAt || 0);
|
|
270
|
+
})
|
|
271
|
+
.map(t => t.taskId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set coordination policy
|
|
276
|
+
*/
|
|
277
|
+
setPolicy(policy: Partial<CoordinationPolicy>): void {
|
|
278
|
+
this.policy = { ...this.policy, ...policy };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get coordination policy
|
|
283
|
+
*/
|
|
284
|
+
getPolicy(): CoordinationPolicy {
|
|
285
|
+
return { ...this.policy };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Format coordination state for display
|
|
290
|
+
*/
|
|
291
|
+
formatState(): string {
|
|
292
|
+
const stats = this.getStats();
|
|
293
|
+
let output = `\n🔀 AGENT COORDINATION STATE\n`;
|
|
294
|
+
output += `Tasks: ${stats.totalTasks} total (executing: ${stats.executing}, blocked: ${stats.blocked}, done: ${stats.completed})\n`;
|
|
295
|
+
output += `File Locks: ${stats.currentLocks}\n`;
|
|
296
|
+
output += `Avg Block Time: ${stats.avgWaitTime}ms\n\n`;
|
|
297
|
+
|
|
298
|
+
const blocked = this.getBlockedTasks();
|
|
299
|
+
if (blocked.length > 0) {
|
|
300
|
+
output += `BLOCKED TASKS:\n`;
|
|
301
|
+
for (const task of blocked) {
|
|
302
|
+
output += ` ${task.taskId} (agent: ${task.agent}) blocked by ${task.blockedBy.join(', ')}\n`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return output;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Clear all state (for testing)
|
|
311
|
+
*/
|
|
312
|
+
clear(): void {
|
|
313
|
+
this.tasks.clear();
|
|
314
|
+
this.fileLocks.clear();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export default AgentCoordination;
|