ninja-terminals 2.0.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/CLAUDE.md +121 -0
- package/ORCHESTRATOR-PROMPT.md +295 -0
- package/cli.js +117 -0
- package/lib/analyze-session.js +92 -0
- package/lib/evolution-writer.js +27 -0
- package/lib/permissions.js +311 -0
- package/lib/playbook-tracker.js +85 -0
- package/lib/resilience.js +458 -0
- package/lib/ring-buffer.js +125 -0
- package/lib/safe-file-writer.js +51 -0
- package/lib/scheduler.js +212 -0
- package/lib/settings-gen.js +159 -0
- package/lib/sse.js +103 -0
- package/lib/status-detect.js +229 -0
- package/lib/task-dag.js +547 -0
- package/lib/tool-rater.js +63 -0
- package/orchestrator/evolution-log.md +33 -0
- package/orchestrator/identity.md +60 -0
- package/orchestrator/metrics/.gitkeep +0 -0
- package/orchestrator/metrics/raw/.gitkeep +0 -0
- package/orchestrator/metrics/session-2026-03-23-setup.md +54 -0
- package/orchestrator/metrics/session-2026-03-24-appcast-build.md +55 -0
- package/orchestrator/playbooks.md +71 -0
- package/orchestrator/security-protocol.md +69 -0
- package/orchestrator/tool-registry.md +96 -0
- package/package.json +46 -0
- package/public/app.js +860 -0
- package/public/index.html +60 -0
- package/public/style.css +678 -0
- package/server.js +695 -0
package/lib/task-dag.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default timeout configuration for tasks (milliseconds).
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_TIMEOUT = {
|
|
7
|
+
scheduleToStart: 60000,
|
|
8
|
+
startToClose: 1800000,
|
|
9
|
+
heartbeat: 120000,
|
|
10
|
+
scheduleToClose: 2700000,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default retry policy for tasks.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_RETRY_POLICY = {
|
|
17
|
+
maxAttempts: 2,
|
|
18
|
+
backoff: 'exponential',
|
|
19
|
+
initialDelay: 10000,
|
|
20
|
+
nonRetryableErrors: ['ERROR:CONTEXT_FULL', 'ERROR:STUCK'],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Runs Kahn's algorithm on a set of tasks to produce a topological ordering
|
|
25
|
+
* and detect any deadlocked (cyclic) tasks.
|
|
26
|
+
*
|
|
27
|
+
* @param {Map<string, object>} taskMap - Map of taskId -> task object
|
|
28
|
+
* @returns {{ order: object[], deadlocked: object[] }}
|
|
29
|
+
*/
|
|
30
|
+
function topologicalSort(taskMap) {
|
|
31
|
+
const inDegree = new Map();
|
|
32
|
+
const dependents = new Map(); // taskId -> [taskIds that depend on it]
|
|
33
|
+
|
|
34
|
+
for (const [id] of taskMap) {
|
|
35
|
+
inDegree.set(id, 0);
|
|
36
|
+
dependents.set(id, []);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const [id, task] of taskMap) {
|
|
40
|
+
for (const dep of task.dependencies) {
|
|
41
|
+
if (taskMap.has(dep)) {
|
|
42
|
+
inDegree.set(id, inDegree.get(id) + 1);
|
|
43
|
+
dependents.get(dep).push(id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const queue = [];
|
|
49
|
+
for (const [id, deg] of inDegree) {
|
|
50
|
+
if (deg === 0) queue.push(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const order = [];
|
|
54
|
+
while (queue.length > 0) {
|
|
55
|
+
const current = queue.shift();
|
|
56
|
+
order.push(taskMap.get(current));
|
|
57
|
+
for (const dependent of dependents.get(current)) {
|
|
58
|
+
const newDeg = inDegree.get(dependent) - 1;
|
|
59
|
+
inDegree.set(dependent, newDeg);
|
|
60
|
+
if (newDeg === 0) queue.push(dependent);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const orderedIds = new Set(order.map((t) => t.id));
|
|
65
|
+
const deadlocked = [];
|
|
66
|
+
for (const [id, task] of taskMap) {
|
|
67
|
+
if (!orderedIds.has(id)) deadlocked.push(task);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { order, deadlocked };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Checks whether adding a dependency edge (taskId depends on dependsOnId)
|
|
75
|
+
* would create a cycle. A cycle exists if dependsOnId already transitively
|
|
76
|
+
* depends on taskId. We check by DFS from dependsOnId following forward
|
|
77
|
+
* dependency edges (task.dependencies) to see if taskId is reachable.
|
|
78
|
+
*
|
|
79
|
+
* @param {Map<string, object>} taskMap
|
|
80
|
+
* @param {string} taskId - The task that would gain a new dependency
|
|
81
|
+
* @param {string} dependsOnId - The task it would depend on
|
|
82
|
+
* @returns {boolean} true if a cycle would be created
|
|
83
|
+
*/
|
|
84
|
+
function wouldCreateCycle(taskMap, taskId, dependsOnId) {
|
|
85
|
+
// If taskId === dependsOnId, self-loop
|
|
86
|
+
if (taskId === dependsOnId) return true;
|
|
87
|
+
|
|
88
|
+
// DFS from dependsOnId following its dependency chain (forward edges).
|
|
89
|
+
// If we reach taskId, adding the edge would create a cycle.
|
|
90
|
+
const visited = new Set();
|
|
91
|
+
const stack = [dependsOnId];
|
|
92
|
+
while (stack.length > 0) {
|
|
93
|
+
const current = stack.pop();
|
|
94
|
+
if (current === taskId) return true;
|
|
95
|
+
if (visited.has(current)) continue;
|
|
96
|
+
visited.add(current);
|
|
97
|
+
const task = taskMap.get(current);
|
|
98
|
+
if (task) {
|
|
99
|
+
for (const dep of task.dependencies) {
|
|
100
|
+
if (!visited.has(dep)) {
|
|
101
|
+
stack.push(dep);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Directed Acyclic Graph manager for task dependencies.
|
|
111
|
+
* Tracks tasks, their statuses, dependency relationships, and provides
|
|
112
|
+
* scheduling-ready queries like getReadyTasks and deadlock detection.
|
|
113
|
+
*/
|
|
114
|
+
class TaskDAG {
|
|
115
|
+
constructor() {
|
|
116
|
+
/** @type {Map<string, object>} */
|
|
117
|
+
this._tasks = new Map();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a task from a config object and add it to the graph.
|
|
122
|
+
* Fills in defaults for timeout, retryPolicy, and other fields.
|
|
123
|
+
* Throws if the task ID already exists or if adding it would create a cycle.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} taskConfig - Task configuration
|
|
126
|
+
* @param {string} taskConfig.id - Unique task identifier
|
|
127
|
+
* @param {string} taskConfig.name - Human-readable task name
|
|
128
|
+
* @param {string} [taskConfig.description=''] - Full description
|
|
129
|
+
* @param {string[]} [taskConfig.dependencies=[]] - IDs of prerequisite tasks
|
|
130
|
+
* @param {string[]} [taskConfig.scope=[]] - File paths this task owns
|
|
131
|
+
* @param {string} [taskConfig.expectedOutput=''] - What completion looks like
|
|
132
|
+
* @param {object} [taskConfig.timeout] - Timeout overrides
|
|
133
|
+
* @param {object} [taskConfig.retryPolicy] - Retry policy overrides
|
|
134
|
+
* @returns {object} The created task
|
|
135
|
+
* @throws {Error} If id is missing, already exists, or would create a cycle
|
|
136
|
+
*/
|
|
137
|
+
addTask(taskConfig) {
|
|
138
|
+
if (!taskConfig || typeof taskConfig !== 'object') {
|
|
139
|
+
throw new Error('taskConfig must be a non-null object');
|
|
140
|
+
}
|
|
141
|
+
if (!taskConfig.id || typeof taskConfig.id !== 'string') {
|
|
142
|
+
throw new Error('Task must have a string id');
|
|
143
|
+
}
|
|
144
|
+
if (!taskConfig.name || typeof taskConfig.name !== 'string') {
|
|
145
|
+
throw new Error('Task must have a string name');
|
|
146
|
+
}
|
|
147
|
+
if (this._tasks.has(taskConfig.id)) {
|
|
148
|
+
throw new Error(`Task "${taskConfig.id}" already exists`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const dependencies = Array.isArray(taskConfig.dependencies)
|
|
152
|
+
? [...taskConfig.dependencies]
|
|
153
|
+
: [];
|
|
154
|
+
|
|
155
|
+
// Validate that all dependencies reference existing tasks
|
|
156
|
+
for (const dep of dependencies) {
|
|
157
|
+
if (!this._tasks.has(dep)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Dependency "${dep}" does not exist in the graph`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const task = {
|
|
165
|
+
id: taskConfig.id,
|
|
166
|
+
name: taskConfig.name,
|
|
167
|
+
description: taskConfig.description || '',
|
|
168
|
+
dependencies,
|
|
169
|
+
assignedTerminal: taskConfig.assignedTerminal || null,
|
|
170
|
+
status: 'pending',
|
|
171
|
+
scope: Array.isArray(taskConfig.scope) ? [...taskConfig.scope] : [],
|
|
172
|
+
expectedOutput: taskConfig.expectedOutput || '',
|
|
173
|
+
timeout: { ...DEFAULT_TIMEOUT, ...(taskConfig.timeout || {}) },
|
|
174
|
+
retryPolicy: {
|
|
175
|
+
...DEFAULT_RETRY_POLICY,
|
|
176
|
+
nonRetryableErrors: [
|
|
177
|
+
...(taskConfig.retryPolicy?.nonRetryableErrors ||
|
|
178
|
+
DEFAULT_RETRY_POLICY.nonRetryableErrors),
|
|
179
|
+
],
|
|
180
|
+
...(taskConfig.retryPolicy || {}),
|
|
181
|
+
// Re-apply nonRetryableErrors since spread above would overwrite
|
|
182
|
+
},
|
|
183
|
+
attempt: 0,
|
|
184
|
+
checkpoints: [],
|
|
185
|
+
artifacts: [],
|
|
186
|
+
startedAt: null,
|
|
187
|
+
completedAt: null,
|
|
188
|
+
error: null,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Fix retryPolicy: ensure nonRetryableErrors is always an array from config or default
|
|
192
|
+
task.retryPolicy.nonRetryableErrors = Array.isArray(
|
|
193
|
+
taskConfig.retryPolicy?.nonRetryableErrors
|
|
194
|
+
)
|
|
195
|
+
? [...taskConfig.retryPolicy.nonRetryableErrors]
|
|
196
|
+
: [...DEFAULT_RETRY_POLICY.nonRetryableErrors];
|
|
197
|
+
|
|
198
|
+
// Check for cycles: temporarily add the task and run topological sort
|
|
199
|
+
this._tasks.set(task.id, task);
|
|
200
|
+
const { deadlocked } = topologicalSort(this._tasks);
|
|
201
|
+
if (deadlocked.length > 0) {
|
|
202
|
+
this._tasks.delete(task.id);
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Adding task "${task.id}" would create a cycle involving: ${deadlocked.map((t) => t.id).join(', ')}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return task;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Remove a task from the graph.
|
|
213
|
+
* Throws if the task doesn't exist or other tasks depend on it.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} id - Task ID to remove
|
|
216
|
+
* @throws {Error} If task not found or has dependents
|
|
217
|
+
*/
|
|
218
|
+
removeTask(id) {
|
|
219
|
+
if (typeof id !== 'string') {
|
|
220
|
+
throw new Error('Task id must be a string');
|
|
221
|
+
}
|
|
222
|
+
if (!this._tasks.has(id)) {
|
|
223
|
+
throw new Error(`Task "${id}" not found`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if any other task depends on this one
|
|
227
|
+
for (const [otherId, task] of this._tasks) {
|
|
228
|
+
if (otherId !== id && task.dependencies.includes(id)) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Cannot remove task "${id}": task "${otherId}" depends on it`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this._tasks.delete(id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get a task by ID.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} id - Task ID
|
|
242
|
+
* @returns {object|null} The task object or null if not found
|
|
243
|
+
*/
|
|
244
|
+
getTask(id) {
|
|
245
|
+
return this._tasks.get(id) || null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get all tasks as an array.
|
|
250
|
+
*
|
|
251
|
+
* @returns {object[]} Array of all task objects
|
|
252
|
+
*/
|
|
253
|
+
getAllTasks() {
|
|
254
|
+
return Array.from(this._tasks.values());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get tasks that are ready to execute: status is 'pending' and all
|
|
259
|
+
* dependencies are in 'done' status.
|
|
260
|
+
*
|
|
261
|
+
* @returns {object[]} Array of ready task objects
|
|
262
|
+
*/
|
|
263
|
+
getReadyTasks() {
|
|
264
|
+
const ready = [];
|
|
265
|
+
for (const task of this._tasks.values()) {
|
|
266
|
+
if (task.status !== 'pending') continue;
|
|
267
|
+
const allDepsDone = task.dependencies.every((depId) => {
|
|
268
|
+
const dep = this._tasks.get(depId);
|
|
269
|
+
return dep && dep.status === 'done';
|
|
270
|
+
});
|
|
271
|
+
if (allDepsDone) ready.push(task);
|
|
272
|
+
}
|
|
273
|
+
return ready;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get tasks that are currently running.
|
|
278
|
+
*
|
|
279
|
+
* @returns {object[]} Array of running task objects
|
|
280
|
+
*/
|
|
281
|
+
getRunningTasks() {
|
|
282
|
+
return this.getAllTasks().filter((t) => t.status === 'running');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Mark a task as queued and assign it to a terminal.
|
|
287
|
+
*
|
|
288
|
+
* @param {string} id - Task ID
|
|
289
|
+
* @param {number} terminalId - Terminal ID to assign
|
|
290
|
+
* @throws {Error} If task not found or not in 'pending' status
|
|
291
|
+
*/
|
|
292
|
+
markQueued(id, terminalId) {
|
|
293
|
+
const task = this._requireTask(id);
|
|
294
|
+
if (task.status !== 'pending') {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Cannot queue task "${id}": status is "${task.status}", expected "pending"`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (typeof terminalId !== 'number') {
|
|
300
|
+
throw new Error('terminalId must be a number');
|
|
301
|
+
}
|
|
302
|
+
task.status = 'queued';
|
|
303
|
+
task.assignedTerminal = terminalId;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Mark a task as running and set its startedAt timestamp.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} id - Task ID
|
|
310
|
+
* @throws {Error} If task not found or not in 'queued' status
|
|
311
|
+
*/
|
|
312
|
+
markRunning(id) {
|
|
313
|
+
const task = this._requireTask(id);
|
|
314
|
+
if (task.status !== 'queued') {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Cannot start task "${id}": status is "${task.status}", expected "queued"`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
task.status = 'running';
|
|
320
|
+
task.startedAt = Date.now();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Mark a task as completed, store artifacts, and set completedAt.
|
|
325
|
+
*
|
|
326
|
+
* @param {string} id - Task ID
|
|
327
|
+
* @param {Array} [artifacts=[]] - Outputs produced by the task
|
|
328
|
+
* @throws {Error} If task not found or not in 'running' status
|
|
329
|
+
*/
|
|
330
|
+
markCompleted(id, artifacts) {
|
|
331
|
+
const task = this._requireTask(id);
|
|
332
|
+
if (task.status !== 'running') {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Cannot complete task "${id}": status is "${task.status}", expected "running"`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
task.status = 'done';
|
|
338
|
+
task.completedAt = Date.now();
|
|
339
|
+
task.artifacts = Array.isArray(artifacts) ? [...artifacts] : [];
|
|
340
|
+
task.error = null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Mark a task as failed. Increments attempt count.
|
|
345
|
+
* If attempts remain and the error is retryable, resets to 'pending' for retry.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} id - Task ID
|
|
348
|
+
* @param {string} errorMsg - Error description
|
|
349
|
+
* @throws {Error} If task not found or not in a running/queued state
|
|
350
|
+
*/
|
|
351
|
+
markFailed(id, errorMsg) {
|
|
352
|
+
const task = this._requireTask(id);
|
|
353
|
+
if (task.status !== 'running' && task.status !== 'queued') {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Cannot fail task "${id}": status is "${task.status}", expected "running" or "queued"`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
task.attempt += 1;
|
|
360
|
+
task.error = errorMsg || 'Unknown error';
|
|
361
|
+
task.completedAt = Date.now();
|
|
362
|
+
|
|
363
|
+
// Check if retryable
|
|
364
|
+
const isNonRetryable = task.retryPolicy.nonRetryableErrors.some(
|
|
365
|
+
(pattern) => errorMsg && errorMsg.includes(pattern)
|
|
366
|
+
);
|
|
367
|
+
const hasAttemptsLeft = task.attempt < task.retryPolicy.maxAttempts;
|
|
368
|
+
|
|
369
|
+
if (!isNonRetryable && hasAttemptsLeft) {
|
|
370
|
+
// Reset for retry
|
|
371
|
+
task.status = 'pending';
|
|
372
|
+
task.assignedTerminal = null;
|
|
373
|
+
task.startedAt = null;
|
|
374
|
+
task.completedAt = null;
|
|
375
|
+
} else {
|
|
376
|
+
task.status = 'failed';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Add a checkpoint snapshot to a task.
|
|
382
|
+
*
|
|
383
|
+
* @param {string} id - Task ID
|
|
384
|
+
* @param {string} summary - Checkpoint summary text
|
|
385
|
+
* @throws {Error} If task not found
|
|
386
|
+
*/
|
|
387
|
+
addCheckpoint(id, summary) {
|
|
388
|
+
const task = this._requireTask(id);
|
|
389
|
+
if (typeof summary !== 'string' || summary.length === 0) {
|
|
390
|
+
throw new Error('Checkpoint summary must be a non-empty string');
|
|
391
|
+
}
|
|
392
|
+
task.checkpoints.push({ ts: Date.now(), summary });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Add a dynamic dependency edge between two tasks.
|
|
397
|
+
* Validates both tasks exist and that the new edge won't create a cycle.
|
|
398
|
+
*
|
|
399
|
+
* @param {string} taskId - The task that will gain a dependency
|
|
400
|
+
* @param {string} dependsOnId - The task it will depend on
|
|
401
|
+
* @throws {Error} If either task not found, dependency already exists, or would create a cycle
|
|
402
|
+
*/
|
|
403
|
+
addDependency(taskId, dependsOnId) {
|
|
404
|
+
this._requireTask(taskId);
|
|
405
|
+
this._requireTask(dependsOnId);
|
|
406
|
+
|
|
407
|
+
const task = this._tasks.get(taskId);
|
|
408
|
+
if (task.dependencies.includes(dependsOnId)) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Task "${taskId}" already depends on "${dependsOnId}"`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check for cycles before adding
|
|
415
|
+
if (wouldCreateCycle(this._tasks, taskId, dependsOnId)) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Adding dependency "${taskId}" -> "${dependsOnId}" would create a cycle`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
task.dependencies.push(dependsOnId);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Run Kahn's algorithm to check for deadlocks in the graph.
|
|
426
|
+
* Only considers non-terminal tasks (not 'done' or 'failed').
|
|
427
|
+
*
|
|
428
|
+
* @returns {{ deadlock: boolean, tasks?: object[] }}
|
|
429
|
+
*/
|
|
430
|
+
checkDeadlock() {
|
|
431
|
+
// Build a subgraph of active (non-terminal) tasks
|
|
432
|
+
const activeMap = new Map();
|
|
433
|
+
for (const [id, task] of this._tasks) {
|
|
434
|
+
if (task.status !== 'done' && task.status !== 'failed') {
|
|
435
|
+
activeMap.set(id, task);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (activeMap.size === 0) {
|
|
440
|
+
return { deadlock: false };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const { deadlocked } = topologicalSort(activeMap);
|
|
444
|
+
if (deadlocked.length > 0) {
|
|
445
|
+
return { deadlock: true, tasks: deadlocked };
|
|
446
|
+
}
|
|
447
|
+
return { deadlock: false };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get progress statistics for the task graph.
|
|
452
|
+
*
|
|
453
|
+
* @returns {{ total: number, pending: number, queued: number, running: number, done: number, failed: number, pct: number }}
|
|
454
|
+
*/
|
|
455
|
+
getProgress() {
|
|
456
|
+
const counts = { total: 0, pending: 0, queued: 0, running: 0, done: 0, failed: 0 };
|
|
457
|
+
for (const task of this._tasks.values()) {
|
|
458
|
+
counts.total++;
|
|
459
|
+
counts[task.status]++;
|
|
460
|
+
}
|
|
461
|
+
counts.pct = counts.total === 0 ? 0 : (counts.done / counts.total) * 100;
|
|
462
|
+
return counts;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Serialize the full DAG state to a plain JSON-compatible object.
|
|
467
|
+
*
|
|
468
|
+
* @returns {object}
|
|
469
|
+
*/
|
|
470
|
+
toJSON() {
|
|
471
|
+
return {
|
|
472
|
+
tasks: this.getAllTasks().map((t) => ({ ...t })),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Deserialize a DAG from a JSON object produced by toJSON().
|
|
478
|
+
* Restores all tasks and their states without validation constraints
|
|
479
|
+
* (since the data was previously valid).
|
|
480
|
+
*
|
|
481
|
+
* @param {object} json - Serialized DAG
|
|
482
|
+
* @returns {TaskDAG}
|
|
483
|
+
*/
|
|
484
|
+
static fromJSON(json) {
|
|
485
|
+
if (!json || !Array.isArray(json.tasks)) {
|
|
486
|
+
throw new Error('Invalid JSON: expected { tasks: [...] }');
|
|
487
|
+
}
|
|
488
|
+
const dag = new TaskDAG();
|
|
489
|
+
// First pass: add all tasks without dependency validation
|
|
490
|
+
// (they reference each other, so we need them all in the map first)
|
|
491
|
+
for (const taskData of json.tasks) {
|
|
492
|
+
const task = {
|
|
493
|
+
id: taskData.id,
|
|
494
|
+
name: taskData.name,
|
|
495
|
+
description: taskData.description || '',
|
|
496
|
+
dependencies: Array.isArray(taskData.dependencies)
|
|
497
|
+
? [...taskData.dependencies]
|
|
498
|
+
: [],
|
|
499
|
+
assignedTerminal: taskData.assignedTerminal || null,
|
|
500
|
+
status: taskData.status || 'pending',
|
|
501
|
+
scope: Array.isArray(taskData.scope) ? [...taskData.scope] : [],
|
|
502
|
+
expectedOutput: taskData.expectedOutput || '',
|
|
503
|
+
timeout: { ...DEFAULT_TIMEOUT, ...(taskData.timeout || {}) },
|
|
504
|
+
retryPolicy: {
|
|
505
|
+
...DEFAULT_RETRY_POLICY,
|
|
506
|
+
...(taskData.retryPolicy || {}),
|
|
507
|
+
nonRetryableErrors: Array.isArray(
|
|
508
|
+
taskData.retryPolicy?.nonRetryableErrors
|
|
509
|
+
)
|
|
510
|
+
? [...taskData.retryPolicy.nonRetryableErrors]
|
|
511
|
+
: [...DEFAULT_RETRY_POLICY.nonRetryableErrors],
|
|
512
|
+
},
|
|
513
|
+
attempt: taskData.attempt || 0,
|
|
514
|
+
checkpoints: Array.isArray(taskData.checkpoints)
|
|
515
|
+
? [...taskData.checkpoints]
|
|
516
|
+
: [],
|
|
517
|
+
artifacts: Array.isArray(taskData.artifacts)
|
|
518
|
+
? [...taskData.artifacts]
|
|
519
|
+
: [],
|
|
520
|
+
startedAt: taskData.startedAt || null,
|
|
521
|
+
completedAt: taskData.completedAt || null,
|
|
522
|
+
error: taskData.error || null,
|
|
523
|
+
};
|
|
524
|
+
dag._tasks.set(task.id, task);
|
|
525
|
+
}
|
|
526
|
+
return dag;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Internal: get a task or throw if not found.
|
|
531
|
+
* @private
|
|
532
|
+
* @param {string} id
|
|
533
|
+
* @returns {object}
|
|
534
|
+
*/
|
|
535
|
+
_requireTask(id) {
|
|
536
|
+
if (typeof id !== 'string') {
|
|
537
|
+
throw new Error('Task id must be a string');
|
|
538
|
+
}
|
|
539
|
+
const task = this._tasks.get(id);
|
|
540
|
+
if (!task) {
|
|
541
|
+
throw new Error(`Task "${id}" not found`);
|
|
542
|
+
}
|
|
543
|
+
return task;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
module.exports = { TaskDAG };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { SUMMARIES_PATH } = require('./analyze-session');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read all session summaries and compute per-tool effectiveness ratings.
|
|
8
|
+
* @returns {Promise<Map<string, object>>} tool name -> { invocations, success_rate, avg_duration_ms, frequency, composite, rating }
|
|
9
|
+
*/
|
|
10
|
+
async function rateTools() {
|
|
11
|
+
const ratings = new Map();
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(SUMMARIES_PATH)) return ratings;
|
|
14
|
+
|
|
15
|
+
const lines = fs.readFileSync(SUMMARIES_PATH, 'utf8').trim().split('\n').filter(Boolean);
|
|
16
|
+
const sessions = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
17
|
+
const totalSessions = sessions.length;
|
|
18
|
+
if (totalSessions === 0) return ratings;
|
|
19
|
+
|
|
20
|
+
// Aggregate per tool across all sessions
|
|
21
|
+
const agg = {};
|
|
22
|
+
for (const s of sessions) {
|
|
23
|
+
if (!s.tools) continue;
|
|
24
|
+
for (const [tool, stats] of Object.entries(s.tools)) {
|
|
25
|
+
if (!agg[tool]) agg[tool] = { invocations: 0, successes: 0, failures: 0, total_duration_ms: 0, sessions: 0 };
|
|
26
|
+
const a = agg[tool];
|
|
27
|
+
a.invocations += stats.invocations || 0;
|
|
28
|
+
a.successes += stats.successes || 0;
|
|
29
|
+
a.failures += stats.failures || 0;
|
|
30
|
+
a.total_duration_ms += stats.total_duration_ms || (stats.avg_duration_ms || 0) * (stats.invocations || 0);
|
|
31
|
+
a.sessions++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const [tool, a] of Object.entries(agg)) {
|
|
36
|
+
const successRate = a.invocations > 0 ? a.successes / a.invocations : 0;
|
|
37
|
+
const avgDuration = a.invocations > 0 ? a.total_duration_ms / a.invocations : 0;
|
|
38
|
+
const frequency = a.sessions / totalSessions;
|
|
39
|
+
|
|
40
|
+
// Composite score: success matters most, then usage frequency, then speed
|
|
41
|
+
const speedScore = 1 - Math.min(avgDuration / 30000, 1); // <30s is good
|
|
42
|
+
const composite = (successRate * 0.5) + (frequency * 0.3) + (speedScore * 0.2);
|
|
43
|
+
|
|
44
|
+
let rating;
|
|
45
|
+
if (composite >= 0.85) rating = 'S';
|
|
46
|
+
else if (composite >= 0.70) rating = 'A';
|
|
47
|
+
else if (composite >= 0.50) rating = 'B';
|
|
48
|
+
else rating = 'C';
|
|
49
|
+
|
|
50
|
+
ratings.set(tool, {
|
|
51
|
+
invocations: a.invocations,
|
|
52
|
+
success_rate: +successRate.toFixed(3),
|
|
53
|
+
avg_duration_ms: Math.round(avgDuration),
|
|
54
|
+
frequency: +frequency.toFixed(3),
|
|
55
|
+
composite: +composite.toFixed(3),
|
|
56
|
+
rating,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return ratings;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { rateTools };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Evolution Log
|
|
2
|
+
|
|
3
|
+
> Append-only. Every self-modification to playbooks, tool-registry, or worker rules
|
|
4
|
+
> gets logged here with reasoning and evidence. This is David's audit trail.
|
|
5
|
+
|
|
6
|
+
## Format
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
### YYYY-MM-DD — [what changed]
|
|
10
|
+
**File:** [which file was modified]
|
|
11
|
+
**Change:** [what was added/removed/modified]
|
|
12
|
+
**Why:** [reasoning — what problem this solves]
|
|
13
|
+
**Evidence:** [metrics, test results, or observations that justify this change]
|
|
14
|
+
**Reversible:** [yes/no — can this be undone easily?]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### 2026-03-23 — Initial system creation
|
|
20
|
+
**File:** All orchestrator/ files
|
|
21
|
+
**Change:** Created identity.md, security-protocol.md, playbooks.md, tool-registry.md, evolution-log.md
|
|
22
|
+
**Why:** Establishing the self-improving orchestrator system based on deep research of existing frameworks (SICA, Karpathy AutoResearch, Boris Cherny self-improving CLAUDE.md, Anthropic long-running harness patterns)
|
|
23
|
+
**Evidence:** Research synthesis from 3 parallel research agents covering: self-improving AI agents, Claude Code advanced features, vibe coding ecosystem
|
|
24
|
+
**Reversible:** Yes — all new files, no existing files modified yet
|
|
25
|
+
|
|
26
|
+
### 2026-03-28 — ### Test Pattern
|
|
27
|
+
**Status:** hypothesis
|
|
28
|
+
**File:** orchestrator/playbooks.md
|
|
29
|
+
**Change:** ### Test Pattern
|
|
30
|
+
**Status:** hypothesis
|
|
31
|
+
**Why:** Testing evolve endpoint
|
|
32
|
+
**Evidence:** Manual test
|
|
33
|
+
**Reversible:** yes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Orchestrator Identity
|
|
2
|
+
|
|
3
|
+
> This file is IMMUTABLE by the orchestrator. Only David edits this file.
|
|
4
|
+
> The orchestrator reads this on every startup. It defines who you are.
|
|
5
|
+
|
|
6
|
+
## Who You Are
|
|
7
|
+
|
|
8
|
+
You are David's technical alter ego — a senior engineering lead who happens to have 4 Claude Code terminals, 170+ MCP tools, browser automation, and the ability to build new tools on demand.
|
|
9
|
+
|
|
10
|
+
You don't ask "what should I work on?" — David tells you, and you execute at a level he couldn't alone. You think in systems, parallelize aggressively, verify everything, and learn from every session.
|
|
11
|
+
|
|
12
|
+
You are not an assistant. You are the lead engineer. David is the product owner. He says what to build; you figure out how, and you get better at it every time.
|
|
13
|
+
|
|
14
|
+
## David's Projects
|
|
15
|
+
|
|
16
|
+
| Project | Location | Stack | Deploys To |
|
|
17
|
+
|---------|----------|-------|------------|
|
|
18
|
+
| Rising Sign (AstroScope) | `~/Desktop/Projects/astroscope/` | Next.js, Zustand, Netlify | risingsign.ca |
|
|
19
|
+
| PostForMe | `~/Desktop/Projects/postforme/` | Next.js, Remotion, Express | postforme.ca (Netlify) + Render backend |
|
|
20
|
+
| StudyChat (EMTChat) | `~/Desktop/Projects/EMTChat/` | Node.js, MongoDB, Pinecone | Render |
|
|
21
|
+
| Ninja Terminals | `~/Desktop/Projects/ninja-terminal/` | Node.js, Express, xterm.js | localhost:3000 |
|
|
22
|
+
|
|
23
|
+
## Core Principles
|
|
24
|
+
|
|
25
|
+
1. **Evidence over assertion.** Never say "done" without proof. Run the build, take the screenshot, check the endpoint.
|
|
26
|
+
2. **Root cause over symptoms.** If something breaks twice, stop patching. Trace the full code path. Find the actual cause.
|
|
27
|
+
3. **Parallel over serial.** You have 4 terminals. If tasks are independent, run them simultaneously.
|
|
28
|
+
4. **Measure over guess.** Log metrics. Compare sessions. Adopt changes based on data, not intuition.
|
|
29
|
+
5. **Simple over clever.** The minimum code that solves the problem. No premature abstractions.
|
|
30
|
+
6. **Verify before presenting.** Visual output? Look at it. Code change? Build it. Bug fix? Reproduce it first.
|
|
31
|
+
|
|
32
|
+
## Guardrails (What Requires Human Approval)
|
|
33
|
+
|
|
34
|
+
- Deploying to production
|
|
35
|
+
- Spending money or creating financial obligations
|
|
36
|
+
- Sending messages to people (email, Telegram, social media, DMs)
|
|
37
|
+
- Posting public content
|
|
38
|
+
- Signing up for paid services
|
|
39
|
+
- Deleting data, force-pushing, or other destructive operations
|
|
40
|
+
- Modifying this identity.md or security-protocol.md
|
|
41
|
+
- Installing MCP servers that request filesystem or network access beyond their stated purpose
|
|
42
|
+
|
|
43
|
+
## What You Control (No Approval Needed)
|
|
44
|
+
|
|
45
|
+
- Modifying `orchestrator/playbooks.md`, `tool-registry.md`, `evolution-log.md`
|
|
46
|
+
- Updating worker `CLAUDE.md` and `.claude/rules/` files
|
|
47
|
+
- Installing npm packages for development/testing (after security verification)
|
|
48
|
+
- Creating/modifying files within project directories
|
|
49
|
+
- Running builds, tests, linters
|
|
50
|
+
- Researching tools, reading docs, web searches
|
|
51
|
+
- Dispatching tasks to terminals
|
|
52
|
+
- Restarting terminals
|
|
53
|
+
|
|
54
|
+
## Context Management
|
|
55
|
+
|
|
56
|
+
Your context window is the coordination layer for the entire system. Keep it lean:
|
|
57
|
+
- Don't store full terminal outputs — extract key results
|
|
58
|
+
- Summarize completed milestones, don't rehash history
|
|
59
|
+
- If context is getting heavy, dump progress to `orchestrator/metrics/` or StudyChat KB
|
|
60
|
+
- After compaction, reload `orchestrator/` files to re-orient
|
|
File without changes
|
|
File without changes
|