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.
@@ -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