@askexenow/exe-os 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/package.json +1 -1
  2. package/dist/bin/backfill-responses.js +0 -2064
  3. package/dist/bin/backfill-vectors.js +0 -1771
  4. package/dist/bin/cleanup-stale-review-tasks.js +0 -1468
  5. package/dist/bin/cli.js +0 -21371
  6. package/dist/bin/exe-agent.js +0 -2016
  7. package/dist/bin/exe-assign.js +0 -2176
  8. package/dist/bin/exe-boot.js +0 -6332
  9. package/dist/bin/exe-call.js +0 -341
  10. package/dist/bin/exe-cloud.js +0 -861
  11. package/dist/bin/exe-dispatch.js +0 -1159
  12. package/dist/bin/exe-doctor.js +0 -1786
  13. package/dist/bin/exe-export-behaviors.js +0 -1637
  14. package/dist/bin/exe-forget.js +0 -1784
  15. package/dist/bin/exe-gateway.js +0 -8140
  16. package/dist/bin/exe-healthcheck.js +0 -207
  17. package/dist/bin/exe-heartbeat.js +0 -1778
  18. package/dist/bin/exe-kill.js +0 -1622
  19. package/dist/bin/exe-launch-agent.js +0 -1847
  20. package/dist/bin/exe-link.js +0 -192
  21. package/dist/bin/exe-new-employee.js +0 -1384
  22. package/dist/bin/exe-pending-messages.js +0 -1576
  23. package/dist/bin/exe-pending-notifications.js +0 -1450
  24. package/dist/bin/exe-pending-reviews.js +0 -1598
  25. package/dist/bin/exe-repo-drift.js +0 -95
  26. package/dist/bin/exe-review.js +0 -1742
  27. package/dist/bin/exe-search.js +0 -3116
  28. package/dist/bin/exe-session-cleanup.js +0 -3360
  29. package/dist/bin/exe-settings.js +0 -365
  30. package/dist/bin/exe-status.js +0 -1661
  31. package/dist/bin/exe-team.js +0 -1453
  32. package/dist/bin/git-sweep.js +0 -2441
  33. package/dist/bin/graph-backfill.js +0 -2111
  34. package/dist/bin/graph-export.js +0 -1747
  35. package/dist/bin/install.js +0 -661
  36. package/dist/bin/list-providers.js +0 -140
  37. package/dist/bin/scan-tasks.js +0 -2039
  38. package/dist/bin/setup.js +0 -2717
  39. package/dist/bin/shard-migrate.js +0 -1637
  40. package/dist/bin/update.js +0 -98
  41. package/dist/bin/wiki-sync.js +0 -1657
  42. package/dist/gateway/index.js +0 -9256
  43. package/dist/hooks/bug-report-worker.js +0 -2903
  44. package/dist/hooks/commit-complete.js +0 -2364
  45. package/dist/hooks/error-recall.js +0 -3327
  46. package/dist/hooks/exe-heartbeat-hook.js +0 -237
  47. package/dist/hooks/ingest-worker.js +0 -5065
  48. package/dist/hooks/ingest.js +0 -695
  49. package/dist/hooks/instructions-loaded.js +0 -2069
  50. package/dist/hooks/notification.js +0 -1915
  51. package/dist/hooks/post-compact.js +0 -1940
  52. package/dist/hooks/pre-compact.js +0 -1935
  53. package/dist/hooks/pre-tool-use.js +0 -2380
  54. package/dist/hooks/prompt-ingest-worker.js +0 -2291
  55. package/dist/hooks/prompt-submit.js +0 -5189
  56. package/dist/hooks/response-ingest-worker.js +0 -2431
  57. package/dist/hooks/session-end.js +0 -2196
  58. package/dist/hooks/session-start.js +0 -3259
  59. package/dist/hooks/stop.js +0 -2025
  60. package/dist/hooks/subagent-stop.js +0 -1915
  61. package/dist/hooks/summary-worker.js +0 -2931
  62. package/dist/index.js +0 -12327
  63. package/dist/lib/cloud-sync.js +0 -500
  64. package/dist/lib/config.js +0 -235
  65. package/dist/lib/consolidation.js +0 -481
  66. package/dist/lib/crypto.js +0 -51
  67. package/dist/lib/database.js +0 -834
  68. package/dist/lib/device-registry.js +0 -1009
  69. package/dist/lib/embedder.js +0 -645
  70. package/dist/lib/employee-templates.js +0 -570
  71. package/dist/lib/employees.js +0 -182
  72. package/dist/lib/error-detector.js +0 -156
  73. package/dist/lib/exe-daemon-client.js +0 -456
  74. package/dist/lib/exe-daemon.js +0 -8699
  75. package/dist/lib/file-grep.js +0 -215
  76. package/dist/lib/hybrid-search.js +0 -3064
  77. package/dist/lib/identity-templates.js +0 -441
  78. package/dist/lib/identity.js +0 -228
  79. package/dist/lib/keychain.js +0 -145
  80. package/dist/lib/license.js +0 -382
  81. package/dist/lib/messaging.js +0 -1389
  82. package/dist/lib/reminders.js +0 -63
  83. package/dist/lib/schedules.js +0 -1525
  84. package/dist/lib/session-registry.js +0 -52
  85. package/dist/lib/skill-learning.js +0 -488
  86. package/dist/lib/status-brief.js +0 -240
  87. package/dist/lib/store.js +0 -1740
  88. package/dist/lib/task-router.js +0 -128
  89. package/dist/lib/tasks.js +0 -2585
  90. package/dist/lib/tmux-routing.js +0 -2969
  91. package/dist/lib/tmux-status.js +0 -261
  92. package/dist/lib/tmux-transport.js +0 -83
  93. package/dist/lib/transport.js +0 -128
  94. package/dist/lib/ws-auth.js +0 -19
  95. package/dist/lib/ws-client.js +0 -160
  96. package/dist/mcp/server.js +0 -10812
  97. package/dist/mcp/tools/complete-reminder.js +0 -67
  98. package/dist/mcp/tools/create-reminder.js +0 -52
  99. package/dist/mcp/tools/create-task.js +0 -1903
  100. package/dist/mcp/tools/deactivate-behavior.js +0 -268
  101. package/dist/mcp/tools/list-reminders.js +0 -62
  102. package/dist/mcp/tools/list-tasks.js +0 -491
  103. package/dist/mcp/tools/send-message.js +0 -1395
  104. package/dist/mcp/tools/update-task.js +0 -1807
  105. package/dist/runtime/index.js +0 -7201
  106. package/dist/tui/App.js +0 -17874
@@ -1,2016 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/bin/exe-agent.ts
4
- import { createInterface } from "readline";
5
- import { readFileSync } from "fs";
6
- import path7 from "path";
7
- import os2 from "os";
8
-
9
- // src/runtime/external-agent-mode.ts
10
- var HARD_BLOCKED_TOOLS = /* @__PURE__ */ new Set([
11
- "Bash",
12
- "Write",
13
- "Edit",
14
- "Glob",
15
- "Grep",
16
- "Read",
17
- "Agent",
18
- "NotebookEdit"
19
- ]);
20
- var BLOCKED_TOOL_PATTERNS = [
21
- /^mcp__.*git/i,
22
- /^mcp__.*exec/i,
23
- /^mcp__.*shell/i,
24
- /^mcp__.*process/i,
25
- /^mcp__.*file/i
26
- ];
27
- function checkExternalAgentPermission(toolName, config) {
28
- if (HARD_BLOCKED_TOOLS.has(toolName)) {
29
- return `DENIED: "${toolName}" is hard-blocked for customer-facing agents`;
30
- }
31
- for (const pattern of BLOCKED_TOOL_PATTERNS) {
32
- if (pattern.test(toolName)) {
33
- return `DENIED: "${toolName}" matches blocked pattern for customer-facing agents`;
34
- }
35
- }
36
- if (!config.allowedTools.includes(toolName)) {
37
- return `DENIED: "${toolName}" is not in the external agent tool whitelist`;
38
- }
39
- return null;
40
- }
41
-
42
- // src/runtime/permission-presets.ts
43
- var CMO_PRESET = {
44
- defaultMode: "deny",
45
- rules: [
46
- { tool: "Read", decision: "allow" },
47
- { tool: "Glob", decision: "allow" },
48
- { tool: "Grep", decision: "allow" },
49
- { tool: "Write", pattern: "exe/output/*", decision: "allow" },
50
- { tool: "mcp__exe-os__*", decision: "allow" },
51
- { tool: "mcp__exe-mem__*", decision: "allow" }
52
- ]
53
- };
54
- var CONTENT_SPECIALIST_PRESET = {
55
- ...CMO_PRESET
56
- };
57
-
58
- // src/runtime/permissions.ts
59
- function checkPermission(toolName, permissions, externalAgentConfig) {
60
- if (externalAgentConfig) {
61
- const denyReason = checkExternalAgentPermission(toolName, externalAgentConfig);
62
- if (denyReason) {
63
- return { allowed: false, mode: "deny", reason: denyReason };
64
- }
65
- }
66
- const mode = permissions.overrides[toolName] ?? permissions.defaultMode;
67
- if (mode === "auto-approve") {
68
- return { allowed: true, mode };
69
- }
70
- if (mode === "deny") {
71
- return { allowed: false, mode, reason: `Tool "${toolName}" is denied by permission policy` };
72
- }
73
- return { allowed: false, mode, reason: `Tool "${toolName}" requires approval` };
74
- }
75
- var EMPLOYEE_PERMISSIONS = {
76
- defaultMode: "auto-approve",
77
- overrides: {}
78
- };
79
-
80
- // src/runtime/zod-to-schema.ts
81
- function zodToJsonSchema(schema) {
82
- if ("toJSONSchema" in schema && typeof schema.toJSONSchema === "function") {
83
- return schema.toJSONSchema();
84
- }
85
- const def = schema._def;
86
- if (!def) {
87
- return { type: "object", properties: {} };
88
- }
89
- return extractSchema(def);
90
- }
91
- function extractSchema(def) {
92
- const typeName = def.typeName;
93
- switch (typeName) {
94
- case "ZodObject": {
95
- const shape = def.shape;
96
- const properties = {};
97
- const required = [];
98
- if (shape) {
99
- const resolved = typeof shape === "function" ? shape() : shape;
100
- for (const [key, value] of Object.entries(resolved)) {
101
- const fieldDef = value._def ?? value;
102
- const isOptional = fieldDef.typeName === "ZodOptional";
103
- if (isOptional) {
104
- properties[key] = extractSchema(
105
- fieldDef.innerType?._def ?? {}
106
- );
107
- } else {
108
- properties[key] = extractSchema(fieldDef);
109
- required.push(key);
110
- }
111
- }
112
- }
113
- const result = { type: "object", properties };
114
- if (required.length > 0) result.required = required;
115
- return result;
116
- }
117
- case "ZodString":
118
- return { type: "string", ...def.description ? { description: String(def.description) } : {} };
119
- case "ZodNumber":
120
- return { type: "number", ...def.description ? { description: String(def.description) } : {} };
121
- case "ZodBoolean":
122
- return { type: "boolean", ...def.description ? { description: String(def.description) } : {} };
123
- case "ZodArray":
124
- return {
125
- type: "array",
126
- items: extractSchema(
127
- def.type?._def ?? {}
128
- )
129
- };
130
- case "ZodEnum":
131
- return { type: "string", enum: def.values };
132
- case "ZodOptional":
133
- return extractSchema(
134
- def.innerType?._def ?? {}
135
- );
136
- case "ZodDefault":
137
- return extractSchema(
138
- def.innerType?._def ?? {}
139
- );
140
- default:
141
- return { type: "string" };
142
- }
143
- }
144
-
145
- // src/runtime/tool-registry.ts
146
- var ToolRegistry = class {
147
- tools = /* @__PURE__ */ new Map();
148
- /** Register a tool */
149
- register(tool) {
150
- this.tools.set(tool.name, tool);
151
- }
152
- /** Look up a tool by name */
153
- get(name) {
154
- return this.tools.get(name);
155
- }
156
- /** List all registered tools */
157
- list() {
158
- return [...this.tools.values()];
159
- }
160
- /** List tool names */
161
- names() {
162
- return [...this.tools.keys()];
163
- }
164
- /** Convert all tools to LLM-ready NormalizedTool format */
165
- toNormalizedTools() {
166
- return this.list().map((tool) => ({
167
- name: tool.name,
168
- description: tool.description,
169
- inputSchema: zodToJsonSchema(tool.inputSchema)
170
- }));
171
- }
172
- /** Get only tools allowed by the given permission set */
173
- listAllowed(permissions) {
174
- return this.list().filter(
175
- (tool) => checkPermission(tool.name, permissions).allowed
176
- );
177
- }
178
- /** Convert allowed tools to NormalizedTool format */
179
- toAllowedNormalizedTools(permissions) {
180
- return this.listAllowed(permissions).map((tool) => ({
181
- name: tool.name,
182
- description: tool.description,
183
- inputSchema: zodToJsonSchema(tool.inputSchema)
184
- }));
185
- }
186
- };
187
- function partitionTools(blocks, registry) {
188
- const concurrent = [];
189
- const sequential = [];
190
- let hitWrite = false;
191
- for (const block of blocks) {
192
- if (block.type !== "tool_use") continue;
193
- const tool = registry.get(block.name);
194
- if (!hitWrite && tool?.isReadOnly) {
195
- concurrent.push(block);
196
- } else {
197
- hitWrite = true;
198
- sequential.push(block);
199
- }
200
- }
201
- return { concurrent, sequential };
202
- }
203
-
204
- // src/runtime/compact.ts
205
- var KEEP_RECENT_MESSAGES = 10;
206
- function estimateTokens(messages) {
207
- let chars = 0;
208
- for (const msg of messages) {
209
- if (typeof msg.content === "string") {
210
- chars += msg.content.length;
211
- } else {
212
- for (const block of msg.content) {
213
- if (block.type === "text") chars += block.text.length;
214
- else if (block.type === "tool_use") chars += JSON.stringify(block.input).length + block.name.length;
215
- else if (block.type === "tool_result") chars += block.content.length;
216
- }
217
- }
218
- }
219
- return Math.ceil(chars / 4);
220
- }
221
- function stripMediaBlocks(messages) {
222
- return messages.map((msg) => {
223
- if (typeof msg.content === "string") return msg;
224
- const filtered = msg.content.filter(
225
- (block) => block.type !== "tool_use" || !isMediaTool(block.name)
226
- );
227
- if (filtered.length === 0) {
228
- return { ...msg, content: "[media content removed for compaction]" };
229
- }
230
- return { ...msg, content: filtered };
231
- });
232
- }
233
- function isMediaTool(name) {
234
- return ["upload_image", "screenshot", "render_image"].includes(name);
235
- }
236
- async function compactMessages(messages, provider, summaryModel = "claude-haiku-4-5-20251001") {
237
- if (messages.length <= KEEP_RECENT_MESSAGES) {
238
- return { messages, removedCount: 0 };
239
- }
240
- const toSummarize = messages.slice(0, -KEEP_RECENT_MESSAGES);
241
- const toKeep = messages.slice(-KEEP_RECENT_MESSAGES);
242
- const stripped = stripMediaBlocks(toSummarize);
243
- const serialized = stripped.map((m) => {
244
- const content = typeof m.content === "string" ? m.content : m.content.map((b) => {
245
- if (b.type === "text") return b.text;
246
- if (b.type === "tool_use") return `[tool: ${b.name}(${JSON.stringify(b.input).slice(0, 200)})]`;
247
- if (b.type === "tool_result") return `[result: ${b.content.slice(0, 200)}]`;
248
- return "";
249
- }).join("\n");
250
- return `${m.role}: ${content}`;
251
- }).join("\n\n");
252
- try {
253
- const summary = await provider.createMessage({
254
- model: summaryModel,
255
- system: "Summarize this conversation concisely. Preserve: decisions made, files modified, current task status, blockers, key findings. Be specific about file paths and tool results.",
256
- messages: [{ role: "user", content: serialized.slice(0, 5e4) }],
257
- // Cap input
258
- maxTokens: 2e3
259
- });
260
- const summaryText = summary.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
261
- return {
262
- messages: [
263
- { role: "user", content: `[Previous conversation summary]
264
- ${summaryText}` },
265
- { role: "assistant", content: "Understood. I have context from the summary. Continuing." },
266
- ...toKeep
267
- ],
268
- removedCount: toSummarize.length
269
- };
270
- } catch {
271
- return {
272
- messages: [
273
- { role: "user", content: "[Earlier conversation history was compacted due to context limits]" },
274
- { role: "assistant", content: "Understood. Continuing with recent context." },
275
- ...toKeep
276
- ],
277
- removedCount: toSummarize.length
278
- };
279
- }
280
- }
281
-
282
- // src/runtime/context-manager.ts
283
- var MODEL_CONTEXT_LIMITS = {
284
- // Anthropic
285
- "claude-opus-4": 2e5,
286
- "claude-sonnet-4": 2e5,
287
- "claude-haiku-4": 2e5,
288
- // Extended context variants
289
- "claude-opus-4-6[1m]": 1e6,
290
- "claude-sonnet-4-6[1m]": 1e6,
291
- // OpenAI
292
- "gpt-4o": 128e3,
293
- "gpt-4-turbo": 128e3,
294
- "gpt-4": 8192,
295
- "o3": 2e5,
296
- "o4-mini": 2e5,
297
- // Google
298
- "gemini-2.0": 1e6,
299
- "gemini-2.5-pro": 1e6,
300
- "gemini-2.5-flash": 1e6,
301
- // Local / small models
302
- "llama": 8192,
303
- "mistral": 32768,
304
- "qwen": 32768,
305
- "deepseek": 64e3
306
- };
307
- var DEFAULT_CONTEXT_LIMIT = 2e5;
308
- var PRESSURE_THRESHOLDS = [50, 70, 90];
309
- function getContextLimit(model) {
310
- if (model in MODEL_CONTEXT_LIMITS) {
311
- return MODEL_CONTEXT_LIMITS[model];
312
- }
313
- let bestMatch = "";
314
- let bestLimit = DEFAULT_CONTEXT_LIMIT;
315
- for (const [prefix, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
316
- if (model.startsWith(prefix) && prefix.length > bestMatch.length) {
317
- bestMatch = prefix;
318
- bestLimit = limit;
319
- }
320
- }
321
- return bestLimit;
322
- }
323
- function createContextManager(model, hooks) {
324
- return new ContextManager(model, hooks);
325
- }
326
- var ContextManager = class {
327
- state;
328
- hooks;
329
- constructor(model, hooks) {
330
- this.hooks = hooks;
331
- this.state = {
332
- inputTokens: 0,
333
- outputTokens: 0,
334
- estimatedTokens: 0,
335
- maxTokens: getContextLimit(model),
336
- thresholdsEmitted: /* @__PURE__ */ new Set(),
337
- model
338
- };
339
- }
340
- /** Get current context state (read-only snapshot) */
341
- getState() {
342
- return { ...this.state, thresholdsEmitted: new Set(this.state.thresholdsEmitted) };
343
- }
344
- /** Get current token usage (best available: API data or estimate) */
345
- get usedTokens() {
346
- if (this.state.inputTokens > 0) {
347
- return this.state.inputTokens;
348
- }
349
- return this.state.estimatedTokens;
350
- }
351
- /** Get percent of context used */
352
- get percentUsed() {
353
- return Math.round(this.usedTokens / this.state.maxTokens * 100);
354
- }
355
- /**
356
- * Update with API usage data (from LLM response).
357
- * This is the most accurate source of token counts.
358
- */
359
- updateFromApiUsage(inputTokens, outputTokens) {
360
- this.state.inputTokens = inputTokens;
361
- this.state.outputTokens = outputTokens;
362
- }
363
- /**
364
- * Update with estimated tokens from message content.
365
- * Used as fallback when API doesn't report cache-aware usage.
366
- */
367
- updateFromMessages(messages) {
368
- this.state.estimatedTokens = estimateTokens(messages);
369
- }
370
- /**
371
- * Check context pressure and emit threshold events.
372
- * Should be called after each turn.
373
- */
374
- async checkPressure() {
375
- const percent = this.percentUsed;
376
- for (const threshold of PRESSURE_THRESHOLDS) {
377
- if (percent >= threshold && !this.state.thresholdsEmitted.has(threshold)) {
378
- this.state.thresholdsEmitted.add(threshold);
379
- const event = {
380
- threshold,
381
- usedTokens: this.usedTokens,
382
- maxTokens: this.state.maxTokens,
383
- percentUsed: percent
384
- };
385
- if (this.hooks.onContextPressure) {
386
- try {
387
- await this.hooks.onContextPressure(event);
388
- } catch {
389
- }
390
- }
391
- }
392
- }
393
- }
394
- /** Should compaction run? (85% threshold, matching compact.ts) */
395
- shouldCompact() {
396
- return this.usedTokens > this.state.maxTokens * 0.85;
397
- }
398
- /**
399
- * Pre-compaction: fire hooks and extract critical context to protect.
400
- * Returns strings that must survive compaction.
401
- */
402
- async preCompact(messages) {
403
- const protectedContext = [];
404
- const memoriesToExtract = [];
405
- if (this.hooks.onCompact) {
406
- try {
407
- await this.hooks.onCompact(messages.length, 0);
408
- } catch {
409
- }
410
- }
411
- const DECISION_PATTERNS = /\b(decided|chose|selected|fixed|resolved|implemented|created|deleted|changed|updated|configured)\b/i;
412
- const FILE_PATTERNS = /(?:(?:src|lib|bin|tests?)\/[\w/.-]+\.(?:ts|js|json|md))/g;
413
- for (const msg of messages) {
414
- const text = typeof msg.content === "string" ? msg.content : msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
415
- if (text.length > 50 && DECISION_PATTERNS.test(text)) {
416
- const firstSentence = text.split(/[.\n]/).find((s) => DECISION_PATTERNS.test(s));
417
- if (firstSentence) {
418
- memoriesToExtract.push(firstSentence.trim().slice(0, 200));
419
- }
420
- }
421
- const files = text.match(FILE_PATTERNS);
422
- if (files && files.length > 0) {
423
- protectedContext.push(`Files referenced: ${[...new Set(files)].join(", ")}`);
424
- }
425
- }
426
- return { protectedContext, memoriesToExtract };
427
- }
428
- /**
429
- * Post-compaction: fire hooks and verify state.
430
- */
431
- async postCompact(removedCount, summaryLength) {
432
- this.state.estimatedTokens = 0;
433
- const currentPercent = this.percentUsed;
434
- for (const threshold of PRESSURE_THRESHOLDS) {
435
- if (currentPercent < threshold) {
436
- this.state.thresholdsEmitted.delete(threshold);
437
- }
438
- }
439
- if (this.hooks.onPostCompact) {
440
- try {
441
- await this.hooks.onPostCompact(removedCount, `Compacted ${removedCount} messages into ${summaryLength} chars`);
442
- } catch {
443
- }
444
- }
445
- }
446
- /**
447
- * Switch model mid-session (e.g., failover).
448
- * Recalculates context limit.
449
- */
450
- switchModel(newModel) {
451
- this.state.model = newModel;
452
- this.state.maxTokens = getContextLimit(newModel);
453
- this.state.thresholdsEmitted.clear();
454
- }
455
- };
456
-
457
- // src/runtime/denial-tracking.ts
458
- var DENIAL_LIMITS = {
459
- maxConsecutive: 3,
460
- maxTotal: 10
461
- };
462
- function createDenialTrackingState() {
463
- return {
464
- consecutiveDenials: 0,
465
- totalDenials: 0
466
- };
467
- }
468
- function recordDenial(state) {
469
- return {
470
- consecutiveDenials: state.consecutiveDenials + 1,
471
- totalDenials: state.totalDenials + 1
472
- };
473
- }
474
- function recordSuccess(state) {
475
- if (state.consecutiveDenials === 0) return state;
476
- return {
477
- ...state,
478
- consecutiveDenials: 0
479
- };
480
- }
481
- function shouldFallbackToAsk(state) {
482
- return state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive || state.totalDenials >= DENIAL_LIMITS.maxTotal;
483
- }
484
- function shouldWarnDenials(state) {
485
- return state.consecutiveDenials === DENIAL_LIMITS.maxConsecutive;
486
- }
487
-
488
- // src/runtime/agent-loop.ts
489
- async function* agentLoop(userMessage, history, config) {
490
- const messages = [
491
- ...history,
492
- { role: "user", content: userMessage }
493
- ];
494
- const totalUsage = { inputTokens: 0, outputTokens: 0 };
495
- const abortController = config.abortController ?? new AbortController();
496
- const context = {
497
- cwd: config.cwd,
498
- agentId: config.agentId,
499
- abortSignal: abortController.signal,
500
- sessionState: /* @__PURE__ */ new Map()
501
- };
502
- const allowedTools = config.tools.toAllowedNormalizedTools(config.permissions);
503
- const contextManager = createContextManager(config.model, config.hooks);
504
- let denialState = createDenialTrackingState();
505
- let turns = 0;
506
- while (turns < config.maxTurns) {
507
- turns++;
508
- if (abortController.signal.aborted) {
509
- yield { type: "aborted", reason: "Agent stopped by user" };
510
- break;
511
- }
512
- if (config.maxTokenBudget && totalUsage.inputTokens + totalUsage.outputTokens >= config.maxTokenBudget) {
513
- yield { type: "error", error: new Error("Token budget exceeded") };
514
- break;
515
- }
516
- let response;
517
- try {
518
- response = await config.provider.createMessage({
519
- model: config.model,
520
- system: config.systemPrompt,
521
- messages,
522
- tools: allowedTools.length > 0 ? allowedTools : void 0,
523
- maxTokens: 4096
524
- });
525
- } catch (err) {
526
- const error = err instanceof Error ? err : new Error(String(err));
527
- yield { type: "error", error };
528
- if (config.hooks.onError) await config.hooks.onError(error, context);
529
- break;
530
- }
531
- totalUsage.inputTokens += response.usage.inputTokens;
532
- totalUsage.outputTokens += response.usage.outputTokens;
533
- if (config.tokenBudgetMiddleware) {
534
- const result = await config.tokenBudgetMiddleware.onTokenUsed(
535
- response.usage.inputTokens,
536
- response.usage.outputTokens
537
- );
538
- if (result.warned && config.hooks.onNotification) {
539
- await config.hooks.onNotification(
540
- `\u26A0\uFE0F Token budget at ${result.percentUsed}%. Fallback model: ${result.fallback ?? "none (task will terminate at 100%)"}.`
541
- );
542
- }
543
- if (result.exceeded) {
544
- if (config.hooks.onTokenBudgetExceeded) {
545
- await config.hooks.onTokenBudgetExceeded(context, result.fallback);
546
- }
547
- abortController.abort();
548
- yield { type: "error", error: new Error("Token budget exceeded. Task requires manual continuation.") };
549
- break;
550
- }
551
- }
552
- contextManager.updateFromApiUsage(response.usage.inputTokens, response.usage.outputTokens);
553
- contextManager.updateFromMessages(messages);
554
- await contextManager.checkPressure();
555
- if (contextManager.shouldCompact() && messages.length > 12) {
556
- const { memoriesToExtract } = await contextManager.preCompact(
557
- messages.slice(0, -10)
558
- );
559
- if (memoriesToExtract.length > 0 && config.hooks.onNotification) {
560
- await config.hooks.onNotification(
561
- `Pre-compaction: extracted ${memoriesToExtract.length} key decisions to memory`
562
- );
563
- }
564
- const compacted = await compactMessages(messages, config.provider);
565
- const removedCount = compacted.removedCount;
566
- if (removedCount > 0) {
567
- messages.length = 0;
568
- messages.push(...compacted.messages);
569
- await contextManager.postCompact(removedCount, messages[0]?.content?.toString().length ?? 0);
570
- }
571
- }
572
- for (const block of response.content) {
573
- if (block.type === "text" && block.text) {
574
- yield { type: "text", text: block.text };
575
- }
576
- }
577
- messages.push({ role: "assistant", content: response.content });
578
- if (response.stopReason === "end_turn" || response.stopReason === "max_tokens") {
579
- yield { type: "turn_complete", turn: turns, usage: response.usage };
580
- break;
581
- }
582
- const toolUseBlocks = response.content.filter(
583
- (b) => b.type === "tool_use"
584
- );
585
- if (toolUseBlocks.length === 0) {
586
- yield { type: "turn_complete", turn: turns, usage: response.usage };
587
- break;
588
- }
589
- const { concurrent, sequential } = partitionTools(toolUseBlocks, config.tools);
590
- const toolResults = [];
591
- if (concurrent.length > 0) {
592
- const concurrentResults = await Promise.all(
593
- concurrent.map(
594
- (block) => executeToolBlock(
595
- block,
596
- config,
597
- context,
598
- denialState
599
- )
600
- )
601
- );
602
- for (let i = 0; i < concurrent.length; i++) {
603
- const block = concurrent[i];
604
- const execResult = concurrentResults[i];
605
- denialState = execResult.denialState;
606
- yield execResult.event;
607
- if (execResult.permissionEvent) yield execResult.permissionEvent;
608
- yield { type: "tool_result", name: block.name, id: block.id, result: execResult.result };
609
- toolResults.push({
610
- type: "tool_result",
611
- tool_use_id: block.id,
612
- content: execResult.result.content,
613
- is_error: execResult.result.isError
614
- });
615
- }
616
- }
617
- for (const block of sequential) {
618
- const typedBlock = block;
619
- const execResult = await executeToolBlock(typedBlock, config, context, denialState);
620
- denialState = execResult.denialState;
621
- yield execResult.event;
622
- if (execResult.permissionEvent) yield execResult.permissionEvent;
623
- yield { type: "tool_result", name: typedBlock.name, id: typedBlock.id, result: execResult.result };
624
- toolResults.push({
625
- type: "tool_result",
626
- tool_use_id: typedBlock.id,
627
- content: execResult.result.content,
628
- is_error: execResult.result.isError
629
- });
630
- }
631
- messages.push({ role: "user", content: toolResults });
632
- yield { type: "turn_complete", turn: turns, usage: response.usage };
633
- }
634
- yield { type: "done", totalUsage, turns };
635
- }
636
- async function executeToolBlock(block, config, context, denialState) {
637
- const startEvent = { type: "tool_use_start", name: block.name, id: block.id };
638
- let permissionEvent;
639
- const tool = config.tools.get(block.name);
640
- if (!tool) {
641
- return {
642
- result: { content: `Unknown tool: "${block.name}"`, isError: true },
643
- event: startEvent,
644
- denialState
645
- };
646
- }
647
- if (!tool.isReadOnly) {
648
- if (tool.checkPermissions) {
649
- try {
650
- const toolPerm = await tool.checkPermissions(block.input, context);
651
- if (toolPerm.behavior === "deny") {
652
- denialState = recordDenial(denialState);
653
- if (shouldWarnDenials(denialState)) {
654
- process.stderr.write(
655
- `[agent] WARNING: ${denialState.consecutiveDenials} consecutive tool denials
656
- `
657
- );
658
- }
659
- return {
660
- result: {
661
- content: `Permission denied${toolPerm.bypassImmune ? " (safety check)" : ""}: ${toolPerm.reason ?? "blocked by tool policy"}`,
662
- isError: true
663
- },
664
- event: { type: "tool_denied", name: block.name, id: block.id, reason: toolPerm.reason ?? "denied" },
665
- denialState
666
- };
667
- }
668
- } catch {
669
- denialState = recordDenial(denialState);
670
- return {
671
- result: { content: "Permission check failed (fail-closed)", isError: true },
672
- event: { type: "tool_denied", name: block.name, id: block.id, reason: "permission check error (fail-closed)" },
673
- denialState
674
- };
675
- }
676
- }
677
- const effectivePermissions = shouldFallbackToAsk(denialState) ? { ...config.permissions, defaultMode: "ask" } : config.permissions;
678
- const perm = checkPermission(block.name, effectivePermissions);
679
- if (!perm.allowed) {
680
- if (perm.mode === "ask" && config.permissionHandler) {
681
- const inp = block.input;
682
- const filePath = inp.file_path ?? inp.filePath;
683
- const description = tool.description;
684
- const permReq = { name: block.name, id: block.id, description, filePath };
685
- const decision = await config.permissionHandler(permReq);
686
- permissionEvent = { type: "permission_request", name: block.name, id: block.id, description, filePath };
687
- if (decision === "allow") {
688
- } else {
689
- denialState = recordDenial(denialState);
690
- return {
691
- result: { content: `Permission denied by user`, isError: true },
692
- event: { type: "tool_denied", name: block.name, id: block.id, reason: "denied by user" },
693
- denialState,
694
- permissionEvent: { type: "permission_request", name: block.name, id: block.id, description, filePath }
695
- };
696
- }
697
- } else {
698
- denialState = recordDenial(denialState);
699
- return {
700
- result: { content: `Permission denied: ${perm.reason}`, isError: true },
701
- event: { type: "tool_denied", name: block.name, id: block.id, reason: perm.reason ?? "denied" },
702
- denialState
703
- };
704
- }
705
- }
706
- }
707
- if (context.abortSignal.aborted) {
708
- return {
709
- result: { content: "Interrupted by user", isError: true },
710
- event: { type: "aborted", reason: "Agent stopped by user" },
711
- denialState
712
- };
713
- }
714
- if (config.hooks.beforeToolUse) {
715
- const intercepted = await config.hooks.beforeToolUse(block, context);
716
- if (intercepted) {
717
- return { result: intercepted, event: startEvent, denialState };
718
- }
719
- }
720
- let result;
721
- try {
722
- result = await tool.call(block.input, context);
723
- denialState = recordSuccess(denialState);
724
- } catch (err) {
725
- result = {
726
- content: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
727
- isError: true
728
- };
729
- if (config.hooks.onError) {
730
- await config.hooks.onError(
731
- err instanceof Error ? err : new Error(String(err)),
732
- context
733
- );
734
- }
735
- }
736
- if (config.hooks.afterToolUse) {
737
- await config.hooks.afterToolUse(block, result, context);
738
- }
739
- return { result, event: startEvent, denialState, permissionEvent };
740
- }
741
-
742
- // src/runtime/hooks.ts
743
- function createDefaultHooks() {
744
- return {};
745
- }
746
-
747
- // src/runtime/stream.ts
748
- function createTerminalRenderer() {
749
- return {
750
- onText(text) {
751
- process.stdout.write(text);
752
- },
753
- onToolStart(name, _id) {
754
- process.stderr.write(`
755
- [tool] ${name}...
756
- `);
757
- },
758
- onToolResult(name, result, isError) {
759
- if (isError) {
760
- process.stderr.write(`[tool] ${name} ERROR: ${result.slice(0, 200)}
761
- `);
762
- } else {
763
- const preview = result.length > 200 ? result.slice(0, 200) + "..." : result;
764
- process.stderr.write(`[tool] ${name} \u2192 ${preview}
765
- `);
766
- }
767
- },
768
- onToolDenied(name, reason) {
769
- process.stderr.write(`[tool] ${name} DENIED: ${reason}
770
- `);
771
- },
772
- onTurnComplete(_turn, _inputTokens, _outputTokens) {
773
- },
774
- onAborted(reason) {
775
- process.stderr.write(`
776
- [aborted] ${reason}
777
- `);
778
- },
779
- onError(error) {
780
- process.stderr.write(`
781
- [error] ${error.message}
782
- `);
783
- },
784
- onDone(inputTokens, outputTokens, turns) {
785
- process.stderr.write(
786
- `
787
- [done] ${turns} turns, ${inputTokens + outputTokens} tokens
788
- `
789
- );
790
- }
791
- };
792
- }
793
- async function renderAgentEvents(events, renderer) {
794
- for await (const event of events) {
795
- switch (event.type) {
796
- case "text":
797
- renderer.onText(event.text);
798
- break;
799
- case "tool_use_start":
800
- renderer.onToolStart(event.name, event.id);
801
- break;
802
- case "tool_result":
803
- renderer.onToolResult(event.name, event.result.content, event.result.isError ?? false);
804
- break;
805
- case "tool_denied":
806
- renderer.onToolDenied(event.name, event.reason);
807
- break;
808
- case "turn_complete":
809
- renderer.onTurnComplete(event.turn, event.usage.inputTokens, event.usage.outputTokens);
810
- break;
811
- case "aborted":
812
- renderer.onAborted(event.reason);
813
- break;
814
- case "error":
815
- renderer.onError(event.error);
816
- break;
817
- case "done":
818
- renderer.onDone(event.totalUsage.inputTokens, event.totalUsage.outputTokens, event.turns);
819
- break;
820
- }
821
- }
822
- }
823
-
824
- // src/gateway/providers/anthropic.ts
825
- import Anthropic from "@anthropic-ai/sdk";
826
- var AnthropicProvider = class {
827
- name;
828
- client;
829
- defaultModel;
830
- constructor(name, config) {
831
- this.name = name;
832
- this.defaultModel = config.defaultModel ?? "claude-sonnet-4-20250514";
833
- this.client = new Anthropic({
834
- apiKey: config.apiKey,
835
- baseURL: config.baseUrl || void 0
836
- });
837
- }
838
- async createMessage(params) {
839
- const response = await this.client.messages.create({
840
- model: params.model || this.defaultModel,
841
- max_tokens: params.maxTokens,
842
- system: params.system,
843
- messages: params.messages.map((m) => this.toAnthropicMessage(m)),
844
- tools: params.tools?.map((t) => ({
845
- name: t.name,
846
- description: t.description,
847
- input_schema: t.inputSchema
848
- }))
849
- });
850
- return this.normalizeResponse(response);
851
- }
852
- async healthCheck() {
853
- const start = Date.now();
854
- try {
855
- await this.client.messages.create({
856
- model: this.defaultModel,
857
- max_tokens: 1,
858
- messages: [{ role: "user", content: "hi" }]
859
- });
860
- return { available: true, latencyMs: Date.now() - start };
861
- } catch {
862
- return { available: false, latencyMs: Date.now() - start };
863
- }
864
- }
865
- toAnthropicMessage(msg) {
866
- if (typeof msg.content === "string") {
867
- return { role: msg.role, content: msg.content };
868
- }
869
- const blocks = msg.content.map((block) => {
870
- if (block.type === "text") {
871
- return { type: "text", text: block.text };
872
- }
873
- if (block.type === "tool_use") {
874
- return {
875
- type: "tool_use",
876
- id: block.id,
877
- name: block.name,
878
- input: block.input
879
- };
880
- }
881
- if (block.type === "tool_result") {
882
- return {
883
- type: "tool_result",
884
- tool_use_id: block.tool_use_id,
885
- content: block.content,
886
- is_error: block.is_error
887
- };
888
- }
889
- return { type: "text", text: "" };
890
- });
891
- return { role: msg.role, content: blocks };
892
- }
893
- normalizeResponse(response) {
894
- const content = response.content.map((block) => {
895
- if (block.type === "text") {
896
- return { type: "text", text: block.text };
897
- }
898
- if (block.type === "tool_use") {
899
- return {
900
- type: "tool_use",
901
- id: block.id,
902
- name: block.name,
903
- input: block.input
904
- };
905
- }
906
- return { type: "text", text: "" };
907
- });
908
- const stopReason = response.stop_reason === "tool_use" ? "tool_use" : response.stop_reason === "max_tokens" ? "max_tokens" : "end_turn";
909
- return {
910
- content,
911
- stopReason,
912
- usage: {
913
- inputTokens: response.usage.input_tokens,
914
- outputTokens: response.usage.output_tokens
915
- }
916
- };
917
- }
918
- };
919
-
920
- // src/gateway/providers/openai-compat.ts
921
- import OpenAI from "openai";
922
- import { randomUUID } from "crypto";
923
- var OpenAICompatProvider = class {
924
- name;
925
- client;
926
- defaultModel;
927
- constructor(name, config) {
928
- this.name = name;
929
- this.defaultModel = config.defaultModel ?? "gpt-4o";
930
- this.client = new OpenAI({
931
- apiKey: config.apiKey,
932
- baseURL: config.baseUrl
933
- });
934
- }
935
- async createMessage(params) {
936
- const messages = [
937
- { role: "system", content: params.system },
938
- ...params.messages.flatMap((m) => this.toOpenAIMessages(m))
939
- ];
940
- const tools = params.tools?.map(
941
- (t) => ({
942
- type: "function",
943
- function: {
944
- name: t.name,
945
- description: t.description,
946
- parameters: t.inputSchema
947
- }
948
- })
949
- );
950
- const response = await this.client.chat.completions.create({
951
- model: params.model || this.defaultModel,
952
- max_tokens: params.maxTokens,
953
- messages,
954
- tools: tools?.length ? tools : void 0
955
- });
956
- return this.normalizeResponse(response);
957
- }
958
- async healthCheck() {
959
- const start = Date.now();
960
- try {
961
- await this.client.chat.completions.create({
962
- model: this.defaultModel,
963
- max_tokens: 1,
964
- messages: [{ role: "user", content: "hi" }]
965
- });
966
- return { available: true, latencyMs: Date.now() - start };
967
- } catch {
968
- return { available: false, latencyMs: Date.now() - start };
969
- }
970
- }
971
- toOpenAIMessages(msg) {
972
- if (typeof msg.content === "string") {
973
- return [{ role: msg.role, content: msg.content }];
974
- }
975
- if (msg.role === "assistant") {
976
- const textParts = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
977
- const toolCalls = msg.content.filter((b) => b.type === "tool_use").map((b) => ({
978
- id: b.id,
979
- type: "function",
980
- function: {
981
- name: b.name,
982
- arguments: typeof b.input === "string" ? b.input : JSON.stringify(b.input)
983
- }
984
- }));
985
- if (toolCalls.length > 0) {
986
- return [{
987
- role: "assistant",
988
- content: textParts || null,
989
- tool_calls: toolCalls
990
- }];
991
- }
992
- return [{ role: "assistant", content: textParts }];
993
- }
994
- if (msg.role === "user") {
995
- const toolResults = msg.content.filter(
996
- (b) => b.type === "tool_result"
997
- );
998
- if (toolResults.length > 0) {
999
- return toolResults.map((r) => ({
1000
- role: "tool",
1001
- tool_call_id: r.tool_use_id,
1002
- content: r.content
1003
- }));
1004
- }
1005
- const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
1006
- return [{ role: "user", content: text }];
1007
- }
1008
- return [{ role: msg.role, content: "" }];
1009
- }
1010
- normalizeResponse(response) {
1011
- const choice = response.choices[0];
1012
- if (!choice) {
1013
- return {
1014
- content: [{ type: "text", text: "" }],
1015
- stopReason: "end_turn",
1016
- usage: { inputTokens: 0, outputTokens: 0 }
1017
- };
1018
- }
1019
- const content = [];
1020
- if (choice.message.content) {
1021
- content.push({ type: "text", text: choice.message.content });
1022
- }
1023
- if (choice.message.tool_calls) {
1024
- for (const call of choice.message.tool_calls) {
1025
- const fn = call.function;
1026
- if (!fn) continue;
1027
- let input;
1028
- try {
1029
- input = JSON.parse(fn.arguments);
1030
- } catch {
1031
- input = fn.arguments;
1032
- }
1033
- content.push({
1034
- type: "tool_use",
1035
- id: call.id ?? randomUUID(),
1036
- name: fn.name,
1037
- input
1038
- });
1039
- }
1040
- }
1041
- if (content.length === 0) {
1042
- content.push({ type: "text", text: "" });
1043
- }
1044
- const stopReason = choice.finish_reason === "tool_calls" ? "tool_use" : choice.finish_reason === "length" ? "max_tokens" : "end_turn";
1045
- return {
1046
- content,
1047
- stopReason,
1048
- usage: {
1049
- inputTokens: response.usage?.prompt_tokens ?? 0,
1050
- outputTokens: response.usage?.completion_tokens ?? 0
1051
- }
1052
- };
1053
- }
1054
- };
1055
-
1056
- // src/gateway/providers/ollama.ts
1057
- import { randomUUID as randomUUID2 } from "crypto";
1058
- var OllamaProvider = class {
1059
- name;
1060
- host;
1061
- defaultModel;
1062
- constructor(name, config = {}) {
1063
- this.name = name;
1064
- this.host = (config.host ?? "http://localhost:11434").replace(/\/+$/, "");
1065
- this.defaultModel = config.defaultModel ?? "qwen3:14b";
1066
- }
1067
- async createMessage(params) {
1068
- const messages = [
1069
- { role: "system", content: params.system }
1070
- ];
1071
- for (const msg of params.messages) {
1072
- if (typeof msg.content === "string") {
1073
- messages.push({ role: msg.role, content: msg.content });
1074
- } else {
1075
- const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
1076
- messages.push({ role: msg.role, content: text });
1077
- }
1078
- }
1079
- const tools = params.tools?.map((t) => ({
1080
- type: "function",
1081
- function: {
1082
- name: t.name,
1083
- description: t.description,
1084
- parameters: t.inputSchema
1085
- }
1086
- }));
1087
- const body = {
1088
- model: params.model || this.defaultModel,
1089
- messages,
1090
- stream: false,
1091
- options: { num_predict: params.maxTokens }
1092
- };
1093
- if (tools?.length) body.tools = tools;
1094
- const res = await fetch(`${this.host}/api/chat`, {
1095
- method: "POST",
1096
- headers: { "Content-Type": "application/json" },
1097
- body: JSON.stringify(body)
1098
- });
1099
- if (!res.ok) {
1100
- throw new Error(`Ollama API error: ${res.status} ${await res.text()}`);
1101
- }
1102
- const data = await res.json();
1103
- return this.normalizeResponse(data);
1104
- }
1105
- async healthCheck() {
1106
- const start = Date.now();
1107
- try {
1108
- const res = await fetch(`${this.host}/api/tags`, {
1109
- signal: AbortSignal.timeout(5e3)
1110
- });
1111
- return { available: res.ok, latencyMs: Date.now() - start };
1112
- } catch {
1113
- return { available: false };
1114
- }
1115
- }
1116
- normalizeResponse(data) {
1117
- const content = [];
1118
- if (data.message.content) {
1119
- content.push({ type: "text", text: data.message.content });
1120
- }
1121
- if (data.message.tool_calls) {
1122
- for (const call of data.message.tool_calls) {
1123
- content.push({
1124
- type: "tool_use",
1125
- id: randomUUID2(),
1126
- name: call.function.name,
1127
- input: call.function.arguments
1128
- });
1129
- }
1130
- }
1131
- if (content.length === 0) {
1132
- content.push({ type: "text", text: "" });
1133
- }
1134
- const hasToolUse = content.some((b) => b.type === "tool_use");
1135
- return {
1136
- content,
1137
- stopReason: hasToolUse ? "tool_use" : "end_turn",
1138
- usage: {
1139
- inputTokens: data.prompt_eval_count ?? 0,
1140
- outputTokens: data.eval_count ?? 0
1141
- }
1142
- };
1143
- }
1144
- };
1145
-
1146
- // src/lib/employee-templates.ts
1147
- var BASE_OPERATING_PROCEDURES = `
1148
- EXE OS \u2014 VISION AND NON-NEGOTIABLE PRINCIPLES (above all work):
1149
-
1150
- Product: "Hire the team you couldn't afford." An AI employee operating system where solo founders and small teams run 5-10 AI agents as a real organization. Three-layer cognition (identity/expertise/experience). Five runtime modes (CC Raw \u2192 TUI \u2192 Desktop). Local-first with E2EE cloud sync.
1151
-
1152
- ICP (who we build for):
1153
- - Solopreneurs, SMB founders, creators with institutional IP
1154
- - Bootstrapped small e-commerce / fitness creators / influencers
1155
- - NOT VC-backed startups \u2014 intentionally excluded
1156
-
1157
- Crown jewels (load-bearing for all three business paths \u2014 never compromise):
1158
- - Memory sovereignty (user owns everything, E2EE, local-first)
1159
- - Three-layer cognition (identity/expertise/experience)
1160
- - MCP contract boundary (surfaces consume memory OS via MCP only \u2014 never direct DB access, never bundled code)
1161
- - AGPL network boundary for public forks (e.g., exe-crm)
1162
-
1163
- Three business-model paths (every product decision must serve these):
1164
- 1. B2C direct \u2014 solopreneurs run their own instance (active, current default)
1165
- 2. Agency white-label \u2014 distributors rebrand for their clients (deferred, but branding must be config-driven)
1166
- 3. Creator franchise (Mike pattern) \u2014 creators inject institutional IP into agent identity+expertise+experience layers, sell scoped access to subscribers (v2+ moat, requires memory export scoping)
1167
-
1168
- Ethos:
1169
- - Bootstrapped, profitable, forever. Not a VC-raise.
1170
- - Founder zero-ego. Distributors and customers are the loudest voice.
1171
- - Crypto values: big companies should not own consumer/SMB AI.
1172
-
1173
- STOP AND REDIRECT: Any decision that compromises memory sovereignty, 3-layer cognition, MCP boundary, or AGPL boundary kills all three business paths. Surface the conflict to exe before proceeding.
1174
-
1175
- Always reference .planning/ARCHITECTURE.md and .planning/PROJECT.md as source of truth for all architectural and product decisions.
1176
-
1177
- OPERATING PROCEDURES (mandatory for all employees):
1178
-
1179
- You report to exe (COO). All work flows through exe. These procedures are non-negotiable.
1180
-
1181
- 1. BEFORE starting work:
1182
- - Read exe/ARCHITECTURE.md (if it exists). This is the system map \u2014 what components exist, how they connect, what invariants to preserve. Understand the architecture before changing anything.
1183
- - Check YOUR task folder ONLY: Read exe/<your-name>/ for assigned tasks
1184
- - NEVER read, write, or modify files in another employee's folder (e.g., exe/mari/, exe/yoshi/). Those are their tasks, not yours. Use ask_team_memory() if you need context from a colleague.
1185
- - If you have open tasks, work on the highest priority one first
1186
- - Ensure exe/output/ exists (mkdir -p exe/output). This is where ALL deliverables go \u2014 reports, analyses, content, audits, anything another employee or the founder needs to pick up.
1187
- - Update task status to "in_progress" when starting (use update_task MCP tool)
1188
- - recall_my_memory \u2014 check what you've done before in this project. What patterns, decisions, context exist?
1189
- - Read the relevant files. Understand what exists before changing anything.
1190
-
1191
- 2. BEFORE marking done \u2014 CHECKPOINT (mandatory, never skip):
1192
- - Run the tests. If they fail, fix them before reporting done.
1193
- - Run typecheck if TypeScript. Zero errors.
1194
- - Verify the change actually works \u2014 run it, check the output, prove it.
1195
- - If you can't verify, say so explicitly: "Couldn't verify because X."
1196
-
1197
- 3. AFTER completing work \u2014 update_task(done) IMMEDIATELY (the ONE critical action):
1198
- Calling update_task with status "done" is the single action that must ALWAYS happen.
1199
- Call it FIRST \u2014 before commit, before report, before anything else. If you do nothing else, do this.
1200
- - Use update_task MCP tool with status "done" and your result summary
1201
- - Include what was done, decisions made, and any issues
1202
- - If you're stuck, looping, confused, or running low on context \u2014 update_task(done) with whatever partial result you have. A partial result is infinitely better than no result.
1203
- - NEVER let a failed commit, a loop, or an error prevent you from calling update_task(done).
1204
- - Do NOT use close_task \u2014 that is reserved for reviewers (exe) to finalize after review.
1205
-
1206
- 4. AFTER update_task(done) \u2014 COMMIT (best-effort, do NOT let this block):
1207
- - If your task changed system structure, update exe/ARCHITECTURE.md first.
1208
- - Commit IF you are in a git repo (check: \`git rev-parse --git-dir 2>/dev/null\`). Stage only the files you changed, write a clear commit message.
1209
- - If you are NOT in a git repo, skip entirely. NEVER run \`git init\`.
1210
- - If the commit fails, note it but move on \u2014 the work is already marked done via update_task.
1211
- - Do NOT push \u2014 exe reviews commits and decides what to push.
1212
- - NEVER run \`git checkout main\`. You work in your own git worktree on a feature branch. Exe stays on main and merges PRs. Switching branches in a shared repo stomps other agents' work.
1213
-
1214
- 5. AFTER commit \u2014 REPORT (best-effort):
1215
- Use store_memory to write a structured summary. Include: project name, what was done,
1216
- decisions made, tests status, open items or risks.
1217
-
1218
- 6. AFTER committing changes to exe-os itself \u2014 REBUILD (mandatory, never skip):
1219
- - Run: npm run deploy
1220
- - This builds, installs globally, and re-registers hooks/MCP in one step.
1221
- - Do NOT ask permission. Do NOT say "want me to rebuild?" \u2014 just do it.
1222
- - If the build fails, fix the error and retry before moving on.
1223
-
1224
- 7. AFTER reporting \u2014 CHECK FOR NEXT WORK (mandatory):
1225
- - First: run list_tasks(status='needs_review') \u2014 check if YOU are the reviewer on any pending reviews. Reviews are work. Process them before anything else.
1226
- - Second: run list_tasks(status='blocked') \u2014 check if any tasks are blocked. For each blocked task: can YOU unblock it? If yes, unblock it now. If not, escalate to exe immediately. Blocked tasks sitting >24h without action is a pipeline failure.
1227
- - Then: re-read your task folder: exe/<your-name>/
1228
- - If there are more open tasks, start the next highest-priority one (go to step 1)
1229
- - If no more open tasks AND no pending reviews AND no blocked tasks you can fix, tell the user: "All tasks complete. Anything else?"
1230
- - Do NOT wait for the user to tell you to check \u2014 auto-chain through your queue.
1231
- - NEVER say "monitoring" or "waiting" while reviews, blocked tasks, or open tasks exist. That is idle drift.
1232
-
1233
- CONTEXT PRESSURE PROTOCOL (mandatory \u2014 never ignore):
1234
- If Claude Code injects a system notice about context compression, or if you notice you're
1235
- losing track of earlier decisions, your context window is full.
1236
-
1237
- DO NOT keep working degraded. Instead:
1238
-
1239
- 1. Call store_memory immediately with a CONTEXT CHECKPOINT:
1240
- Format the text as: "CONTEXT CHECKPOINT [<task-id>]: <summary>"
1241
- Include: task ID + title, what you completed, what's left, open decisions or blockers, key file paths.
1242
-
1243
- 2. Send intercom to exe to trigger kill + relaunch:
1244
- MY_SESSION=$(tmux display-message -p '#{session_name}' 2>/dev/null)
1245
- EXE_SESSION="\${MY_SESSION#\${AGENT_ID}-}"
1246
- tmux send-keys -t "$EXE_SESSION" "/exe-intercom context-full: \${AGENT_ID} hit capacity. Checkpoint saved. Resume task <task-id>." Enter
1247
-
1248
- 3. Stop working immediately. Do not attempt to continue with degraded context.
1249
-
1250
- COMMUNICATION CHAIN \u2014 who you talk to:
1251
- - You report to exe (COO). Your completion reports, status updates, and questions go to exe via store_memory and update_task.
1252
- - Do NOT address the human user directly for decisions, permissions, or status updates. That's exe's job. The user talks to exe; exe talks to you.
1253
- - Exception: if the user sends you a direct message in your tmux window, respond to them. But default to reporting through exe.
1254
-
1255
- SKILL CAPTURE (encouraged, not mandatory):
1256
- After completing a complex multi-step task (5+ tool calls), consider whether the approach
1257
- should be saved as a reusable procedure. If the task involved non-obvious steps, error recovery,
1258
- or a workflow that would help future sessions, use store_behavior with domain='skill' to save it.
1259
- Format: "SKILL: [name] \u2014 Step 1: ... Step 2: ... Pitfalls: ..."
1260
- Skip for simple one-offs. The goal is procedural memory \u2014 not just corrections, but proven approaches.
1261
-
1262
- SPAWNING EMPLOYEES (mandatory \u2014 never bypass):
1263
- When you need another employee to do work, ALWAYS use create_task MCP tool.
1264
- create_task auto-spawns the employee session. The task IS the spawn trigger.
1265
- NEVER manually launch sessions with tmux send-keys or claude -p.
1266
- NEVER spawn sessions without a task assigned \u2014 idle sessions waste resources.
1267
- NEVER refuse a dispatched task claiming "not in scope" \u2014 if it's assigned to you, it's your work.
1268
-
1269
- CREATING TASKS FOR OTHER EMPLOYEES:
1270
- When you need to assign work to another employee (e.g., yoshi assigns to tom):
1271
- - ALWAYS use create_task MCP tool. NEVER write .md files directly to exe/{name}/.
1272
- - Direct .md writes will be rejected by the enforcement hook with a MANDATORY correction.
1273
- - create_task creates both the .md file AND the DB row atomically.
1274
- - Include: title, assignedTo, priority, context, projectName.
1275
- - For dependencies: include blocked_by with the blocking task's ID or slug.
1276
- `;
1277
- var PROCEDURES_MARKER = "EXE OS \u2014 VISION AND NON-NEGOTIABLE PRINCIPLES";
1278
- function getSessionPrompt(storedPrompt) {
1279
- const markerIndex = storedPrompt.indexOf(PROCEDURES_MARKER);
1280
- const rolePrompt = markerIndex >= 0 ? storedPrompt.slice(0, markerIndex).trimEnd() : storedPrompt;
1281
- return `${rolePrompt}
1282
- ${BASE_OPERATING_PROCEDURES}`;
1283
- }
1284
-
1285
- // src/runtime/tools/bash.ts
1286
- import { spawn } from "child_process";
1287
- import { z } from "zod";
1288
-
1289
- // src/runtime/dangerous-patterns.ts
1290
- var DANGEROUS_PATTERNS = [
1291
- // Destructive file operations
1292
- {
1293
- regex: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|(-[a-zA-Z]*f[a-zA-Z]*r))\s+[/~*]/,
1294
- severity: "critical",
1295
- reason: "Recursive force delete of root, home, or wildcard path"
1296
- },
1297
- {
1298
- regex: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|(-[a-zA-Z]*f[a-zA-Z]*r))\s+\./,
1299
- severity: "warning",
1300
- reason: "Recursive force delete of relative path"
1301
- },
1302
- // Destructive git operations
1303
- {
1304
- regex: /\bgit\s+push\s+--force\b/,
1305
- severity: "warning",
1306
- reason: "Force push can overwrite remote history"
1307
- },
1308
- {
1309
- regex: /\bgit\s+reset\s+--hard\b/,
1310
- severity: "warning",
1311
- reason: "Hard reset discards all uncommitted changes"
1312
- },
1313
- {
1314
- regex: /\bgit\s+clean\s+-[a-zA-Z]*f/,
1315
- severity: "warning",
1316
- reason: "Git clean force removes untracked files"
1317
- },
1318
- // Shell injection / expansion in paths
1319
- {
1320
- regex: /\$\(.*\)/,
1321
- severity: "warning",
1322
- reason: "Command substitution in shell command"
1323
- },
1324
- {
1325
- regex: /`[^`]+`/,
1326
- severity: "warning",
1327
- reason: "Backtick command substitution"
1328
- },
1329
- // Remote code execution
1330
- {
1331
- regex: /\bcurl\b.*\|\s*(bash|sh|zsh)\b/,
1332
- severity: "critical",
1333
- reason: "Remote code execution via curl pipe to shell"
1334
- },
1335
- {
1336
- regex: /\bwget\b.*\|\s*(bash|sh|zsh)\b/,
1337
- severity: "critical",
1338
- reason: "Remote code execution via wget pipe to shell"
1339
- },
1340
- // Privilege escalation
1341
- {
1342
- regex: /\bsudo\b/,
1343
- severity: "warning",
1344
- reason: "Privilege escalation via sudo"
1345
- },
1346
- {
1347
- regex: /\bchmod\s+777\b/,
1348
- severity: "warning",
1349
- reason: "Setting world-writable permissions"
1350
- },
1351
- // Dangerous disk operations
1352
- {
1353
- regex: /\bdd\s+.*of=\/dev\//,
1354
- severity: "critical",
1355
- reason: "Direct disk write can destroy data"
1356
- },
1357
- {
1358
- regex: /\bmkfs\b/,
1359
- severity: "critical",
1360
- reason: "Filesystem creation destroys existing data"
1361
- },
1362
- // Process/system manipulation
1363
- {
1364
- regex: /\bkillall\b/,
1365
- severity: "warning",
1366
- reason: "Killing all processes by name"
1367
- },
1368
- {
1369
- regex: /\bkill\s+-9\b/,
1370
- severity: "warning",
1371
- reason: "Force kill signal"
1372
- }
1373
- ];
1374
- function checkDangerousPatterns(command) {
1375
- const matched = [];
1376
- for (const rule of DANGEROUS_PATTERNS) {
1377
- if (rule.regex.test(command)) {
1378
- matched.push({
1379
- pattern: rule.regex.source,
1380
- severity: rule.severity,
1381
- reason: rule.reason
1382
- });
1383
- }
1384
- }
1385
- return {
1386
- dangerous: matched.length > 0,
1387
- patterns: matched
1388
- };
1389
- }
1390
- function hasCriticalPattern(result) {
1391
- return result.patterns.some((p) => p.severity === "critical");
1392
- }
1393
- function formatDangerousPatterns(patterns) {
1394
- return patterns.map((p) => `[${p.severity.toUpperCase()}] ${p.reason}`).join("\n");
1395
- }
1396
-
1397
- // src/runtime/tools/bash.ts
1398
- var DEFAULT_TIMEOUT_MS = 12e4;
1399
- var inputSchema = z.object({
1400
- command: z.string(),
1401
- timeout: z.number().optional()
1402
- });
1403
- var BashTool = {
1404
- name: "bash",
1405
- description: "Execute a shell command",
1406
- inputSchema,
1407
- isReadOnly: false,
1408
- async checkPermissions(input) {
1409
- const result = checkDangerousPatterns(input.command);
1410
- if (hasCriticalPattern(result)) {
1411
- return {
1412
- behavior: "deny",
1413
- reason: formatDangerousPatterns(result.patterns),
1414
- bypassImmune: true
1415
- };
1416
- }
1417
- return { behavior: "allow" };
1418
- },
1419
- async call(input, context) {
1420
- const timeout = input.timeout ?? DEFAULT_TIMEOUT_MS;
1421
- return new Promise((resolve) => {
1422
- const child = spawn("bash", ["-c", input.command], {
1423
- cwd: context.cwd,
1424
- timeout,
1425
- stdio: ["ignore", "pipe", "pipe"],
1426
- env: { ...process.env }
1427
- });
1428
- const stdoutChunks = [];
1429
- const stderrChunks = [];
1430
- child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
1431
- child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
1432
- const onAbort = () => {
1433
- child.kill("SIGTERM");
1434
- setTimeout(() => {
1435
- if (!child.killed) child.kill("SIGKILL");
1436
- }, 1e3);
1437
- };
1438
- context.abortSignal.addEventListener("abort", onAbort, { once: true });
1439
- child.on("close", (code) => {
1440
- context.abortSignal.removeEventListener("abort", onAbort);
1441
- const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
1442
- const stderr = Buffer.concat(stderrChunks).toString("utf-8");
1443
- if (context.abortSignal.aborted) {
1444
- resolve({ content: "Command killed: agent aborted", isError: true });
1445
- return;
1446
- }
1447
- if (code !== 0) {
1448
- const output = stderr || stdout || `Exit code ${code}`;
1449
- resolve({ content: output, isError: true });
1450
- return;
1451
- }
1452
- resolve({ content: stdout || "(no output)" });
1453
- });
1454
- child.on("error", (err) => {
1455
- context.abortSignal.removeEventListener("abort", onAbort);
1456
- resolve({ content: `Spawn error: ${err.message}`, isError: true });
1457
- });
1458
- });
1459
- }
1460
- };
1461
-
1462
- // src/runtime/tools/file-read.ts
1463
- import fs from "fs/promises";
1464
- import path2 from "path";
1465
- import { z as z2 } from "zod";
1466
-
1467
- // src/runtime/safety-checks.ts
1468
- import path from "path";
1469
- import os from "os";
1470
- var HOME = os.homedir();
1471
- var BYPASS_IMMUNE_PATTERNS = [
1472
- {
1473
- pattern: /\/\.git\/hooks\//,
1474
- reason: "Git hooks can execute arbitrary code"
1475
- },
1476
- {
1477
- pattern: /\/\.git\/config$/,
1478
- reason: "Git config can set hooks and command execution"
1479
- },
1480
- {
1481
- pattern: (p) => p.startsWith(path.join(HOME, ".claude")),
1482
- reason: "Claude configuration files are protected"
1483
- },
1484
- {
1485
- pattern: (p) => p.startsWith(path.join(HOME, ".exe-os")),
1486
- reason: "exe-os configuration files are protected"
1487
- },
1488
- {
1489
- pattern: /\/\.env($|\.)/,
1490
- reason: "Environment files may contain secrets"
1491
- },
1492
- {
1493
- pattern: /\/(credentials|secrets|tokens)\.(json|yaml|yml|toml|ini|conf)$/i,
1494
- reason: "Credential files are protected"
1495
- },
1496
- {
1497
- pattern: /(\.pem|\.key|\.p12|\.pfx|\.jks|id_rsa|id_ed25519)$/,
1498
- reason: "Private key files are protected"
1499
- },
1500
- {
1501
- pattern: (p) => {
1502
- const name = path.basename(p);
1503
- return [".bashrc", ".zshrc", ".profile", ".bash_profile", ".zprofile", ".zshenv"].includes(name);
1504
- },
1505
- reason: "Shell configuration files can execute arbitrary code on login"
1506
- },
1507
- {
1508
- pattern: (p) => p.startsWith("/etc/"),
1509
- reason: "System configuration directory is protected"
1510
- },
1511
- {
1512
- pattern: (p) => p.startsWith("/usr/") && !p.startsWith("/usr/local/"),
1513
- reason: "System binary directory is protected"
1514
- },
1515
- {
1516
- pattern: (p) => p === "/" || p === HOME,
1517
- reason: "Root and home directory direct modification is protected"
1518
- }
1519
- ];
1520
- function checkPathSafety(filePath) {
1521
- const resolved = path.resolve(filePath);
1522
- for (const { pattern, reason } of BYPASS_IMMUNE_PATTERNS) {
1523
- const matches = typeof pattern === "function" ? pattern(resolved) : pattern.test(resolved);
1524
- if (matches) {
1525
- return { safe: false, reason, bypassImmune: true };
1526
- }
1527
- }
1528
- return { safe: true, bypassImmune: true };
1529
- }
1530
- function checkReadPathSafety(filePath) {
1531
- const resolved = path.resolve(filePath);
1532
- const credPatterns = BYPASS_IMMUNE_PATTERNS.filter(
1533
- (p) => typeof p.pattern !== "function" && (p.reason.includes("secrets") || p.reason.includes("Private key") || p.reason.includes("Credential"))
1534
- );
1535
- for (const { pattern, reason } of credPatterns) {
1536
- if (typeof pattern !== "function" && pattern.test(resolved)) {
1537
- return { safe: false, reason, bypassImmune: true };
1538
- }
1539
- }
1540
- return { safe: true, bypassImmune: true };
1541
- }
1542
-
1543
- // src/runtime/tools/file-read.ts
1544
- var inputSchema2 = z2.object({
1545
- file_path: z2.string(),
1546
- offset: z2.number().optional(),
1547
- limit: z2.number().optional()
1548
- });
1549
- var DEFAULT_LIMIT = 2e3;
1550
- var FileReadTool = {
1551
- name: "file_read",
1552
- description: "Read a file from disk",
1553
- inputSchema: inputSchema2,
1554
- isReadOnly: true,
1555
- async checkPermissions(input) {
1556
- const safety = checkReadPathSafety(input.file_path);
1557
- if (!safety.safe) {
1558
- return {
1559
- behavior: "deny",
1560
- reason: safety.reason ?? "Read blocked by safety check",
1561
- bypassImmune: true
1562
- };
1563
- }
1564
- return { behavior: "allow" };
1565
- },
1566
- async call(input, context) {
1567
- const filePath = path2.isAbsolute(input.file_path) ? input.file_path : path2.resolve(context.cwd, input.file_path);
1568
- let stat;
1569
- try {
1570
- stat = await fs.stat(filePath);
1571
- } catch {
1572
- return { content: `File not found: ${filePath}`, isError: true };
1573
- }
1574
- if (stat.size > 0) {
1575
- const sample = Buffer.alloc(512);
1576
- const fh = await fs.open(filePath, "r");
1577
- try {
1578
- const { bytesRead } = await fh.read(sample, 0, 512, 0);
1579
- const slice = sample.subarray(0, bytesRead);
1580
- if (isBinary(slice)) {
1581
- return {
1582
- content: `Binary file (${stat.size} bytes): ${filePath}`
1583
- };
1584
- }
1585
- } finally {
1586
- await fh.close();
1587
- }
1588
- }
1589
- const raw = await fs.readFile(filePath, "utf-8");
1590
- const allLines = raw.split("\n");
1591
- const offset = input.offset ?? 0;
1592
- const limit = input.limit ?? DEFAULT_LIMIT;
1593
- const lines = allLines.slice(offset, offset + limit);
1594
- const numbered = lines.map((line, i) => `${String(offset + i + 1).padStart(6)} ${line}`).join("\n");
1595
- return { content: numbered };
1596
- }
1597
- };
1598
- function isBinary(buf) {
1599
- for (let i = 0; i < buf.length; i++) {
1600
- if (buf[i] === 0) return true;
1601
- }
1602
- return false;
1603
- }
1604
-
1605
- // src/runtime/tools/file-edit.ts
1606
- import fs2 from "fs/promises";
1607
- import path3 from "path";
1608
- import { z as z3 } from "zod";
1609
- var inputSchema3 = z3.object({
1610
- file_path: z3.string(),
1611
- old_string: z3.string(),
1612
- new_string: z3.string(),
1613
- replace_all: z3.boolean().optional()
1614
- });
1615
- var FileEditTool = {
1616
- name: "file_edit",
1617
- description: "Edit a file by replacing a string",
1618
- inputSchema: inputSchema3,
1619
- isReadOnly: false,
1620
- async checkPermissions(input) {
1621
- const safety = checkPathSafety(input.file_path);
1622
- if (!safety.safe) {
1623
- return {
1624
- behavior: "deny",
1625
- reason: safety.reason ?? "Edit blocked by safety check",
1626
- bypassImmune: true
1627
- };
1628
- }
1629
- return { behavior: "allow" };
1630
- },
1631
- async call(input, context) {
1632
- const filePath = path3.isAbsolute(input.file_path) ? input.file_path : path3.resolve(context.cwd, input.file_path);
1633
- let content;
1634
- try {
1635
- content = await fs2.readFile(filePath, "utf-8");
1636
- } catch {
1637
- return { content: `File not found: ${filePath}`, isError: true };
1638
- }
1639
- const occurrences = countOccurrences(content, input.old_string);
1640
- if (occurrences === 0) {
1641
- return {
1642
- content: `old_string not found in ${filePath}`,
1643
- isError: true
1644
- };
1645
- }
1646
- if (occurrences > 1 && !input.replace_all) {
1647
- return {
1648
- content: `old_string appears ${occurrences} times in ${filePath}. Use replace_all: true to replace all, or provide more context to make it unique.`,
1649
- isError: true
1650
- };
1651
- }
1652
- let newContent;
1653
- if (input.replace_all) {
1654
- newContent = content.split(input.old_string).join(input.new_string);
1655
- } else {
1656
- const idx = content.indexOf(input.old_string);
1657
- newContent = content.slice(0, idx) + input.new_string + content.slice(idx + input.old_string.length);
1658
- }
1659
- await fs2.writeFile(filePath, newContent, "utf-8");
1660
- const replaced = input.replace_all ? occurrences : 1;
1661
- return {
1662
- content: `Replaced ${replaced} occurrence(s) in ${filePath}`,
1663
- sideEffects: { filesModified: [filePath] }
1664
- };
1665
- }
1666
- };
1667
- function countOccurrences(haystack, needle) {
1668
- let count = 0;
1669
- let pos = 0;
1670
- while (true) {
1671
- const idx = haystack.indexOf(needle, pos);
1672
- if (idx === -1) break;
1673
- count++;
1674
- pos = idx + needle.length;
1675
- }
1676
- return count;
1677
- }
1678
-
1679
- // src/runtime/tools/file-write.ts
1680
- import fs3 from "fs/promises";
1681
- import path4 from "path";
1682
- import { z as z4 } from "zod";
1683
- var inputSchema4 = z4.object({
1684
- file_path: z4.string(),
1685
- content: z4.string()
1686
- });
1687
- var FileWriteTool = {
1688
- name: "file_write",
1689
- description: "Write content to a file (creates or overwrites)",
1690
- inputSchema: inputSchema4,
1691
- isReadOnly: false,
1692
- async checkPermissions(input) {
1693
- const safety = checkPathSafety(input.file_path);
1694
- if (!safety.safe) {
1695
- return {
1696
- behavior: "deny",
1697
- reason: safety.reason ?? "Write blocked by safety check",
1698
- bypassImmune: true
1699
- };
1700
- }
1701
- return { behavior: "allow" };
1702
- },
1703
- async call(input, context) {
1704
- const filePath = path4.isAbsolute(input.file_path) ? input.file_path : path4.resolve(context.cwd, input.file_path);
1705
- const dir = path4.dirname(filePath);
1706
- await fs3.mkdir(dir, { recursive: true });
1707
- await fs3.writeFile(filePath, input.content, "utf-8");
1708
- return {
1709
- content: `Wrote ${input.content.length} bytes to ${filePath}`,
1710
- sideEffects: { filesModified: [filePath] }
1711
- };
1712
- }
1713
- };
1714
-
1715
- // src/runtime/tools/glob.ts
1716
- import fs4 from "fs/promises";
1717
- import path5 from "path";
1718
- import { z as z5 } from "zod";
1719
- var inputSchema5 = z5.object({
1720
- pattern: z5.string(),
1721
- path: z5.string().optional()
1722
- });
1723
- var GlobTool = {
1724
- name: "glob",
1725
- description: "Find files matching a glob pattern",
1726
- inputSchema: inputSchema5,
1727
- isReadOnly: true,
1728
- async call(input, context) {
1729
- const baseDir = input.path ? path5.isAbsolute(input.path) ? input.path : path5.resolve(context.cwd, input.path) : context.cwd;
1730
- try {
1731
- const entries = await walkDir(baseDir);
1732
- const matched = entries.filter(
1733
- (e) => simpleGlobMatch(path5.relative(baseDir, e.path), input.pattern)
1734
- );
1735
- matched.sort((a, b) => b.mtime - a.mtime);
1736
- if (matched.length === 0) {
1737
- return { content: "No files matched." };
1738
- }
1739
- const lines = matched.map((e) => e.path);
1740
- return { content: lines.join("\n") };
1741
- } catch (err) {
1742
- return {
1743
- content: `Glob error: ${err instanceof Error ? err.message : String(err)}`,
1744
- isError: true
1745
- };
1746
- }
1747
- }
1748
- };
1749
- async function walkDir(dir, maxDepth = 10) {
1750
- const results = [];
1751
- async function walk(current, depth) {
1752
- if (depth > maxDepth) return;
1753
- let entries;
1754
- try {
1755
- entries = await fs4.readdir(current, { withFileTypes: true });
1756
- } catch {
1757
- return;
1758
- }
1759
- for (const entry of entries) {
1760
- if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git")) {
1761
- continue;
1762
- }
1763
- const fullPath = path5.join(current, entry.name);
1764
- if (entry.isDirectory()) {
1765
- await walk(fullPath, depth + 1);
1766
- } else {
1767
- try {
1768
- const stat = await fs4.stat(fullPath);
1769
- results.push({ path: fullPath, mtime: stat.mtimeMs });
1770
- } catch {
1771
- }
1772
- }
1773
- }
1774
- }
1775
- await walk(dir, 0);
1776
- return results;
1777
- }
1778
- function simpleGlobMatch(filePath, pattern) {
1779
- let regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
1780
- regex = `^${regex}$`;
1781
- return new RegExp(regex).test(filePath);
1782
- }
1783
-
1784
- // src/runtime/tools/grep.ts
1785
- import { spawn as spawn2 } from "child_process";
1786
- import fs5 from "fs/promises";
1787
- import path6 from "path";
1788
- import { z as z6 } from "zod";
1789
- var inputSchema6 = z6.object({
1790
- pattern: z6.string(),
1791
- path: z6.string().optional(),
1792
- glob: z6.string().optional(),
1793
- include: z6.string().optional()
1794
- });
1795
- var GrepTool = {
1796
- name: "grep",
1797
- description: "Search file contents using regex",
1798
- inputSchema: inputSchema6,
1799
- isReadOnly: true,
1800
- async call(input, context) {
1801
- const searchPath = input.path ? path6.isAbsolute(input.path) ? input.path : path6.resolve(context.cwd, input.path) : context.cwd;
1802
- try {
1803
- const result = await runRipgrep(input, searchPath, context);
1804
- return result;
1805
- } catch {
1806
- }
1807
- try {
1808
- return await nodeGrep(input, searchPath);
1809
- } catch (err) {
1810
- return {
1811
- content: `Grep error: ${err instanceof Error ? err.message : String(err)}`,
1812
- isError: true
1813
- };
1814
- }
1815
- }
1816
- };
1817
- function runRipgrep(input, searchPath, context) {
1818
- return new Promise((resolve, reject) => {
1819
- const args = ["--files-with-matches", "--no-heading"];
1820
- if (input.glob) {
1821
- args.push("--glob", input.glob);
1822
- }
1823
- if (input.include) {
1824
- args.push("--glob", input.include);
1825
- }
1826
- args.push(input.pattern, searchPath);
1827
- const child = spawn2("rg", args, {
1828
- cwd: searchPath,
1829
- timeout: 3e4,
1830
- stdio: ["ignore", "pipe", "pipe"]
1831
- });
1832
- const chunks = [];
1833
- child.stdout.on("data", (chunk) => chunks.push(chunk));
1834
- const onAbort = () => child.kill("SIGTERM");
1835
- context.abortSignal.addEventListener("abort", onAbort, { once: true });
1836
- child.on("close", (code) => {
1837
- context.abortSignal.removeEventListener("abort", onAbort);
1838
- const output = Buffer.concat(chunks).toString("utf-8").trim();
1839
- if (code === 1 && !output) {
1840
- resolve({ content: "No matches found." });
1841
- return;
1842
- }
1843
- if (code === 2) {
1844
- reject(new Error("ripgrep error"));
1845
- return;
1846
- }
1847
- resolve({ content: output || "No matches found." });
1848
- });
1849
- child.on("error", () => reject(new Error("rg not found")));
1850
- });
1851
- }
1852
- async function nodeGrep(input, searchPath) {
1853
- const regex = new RegExp(input.pattern);
1854
- const matches = [];
1855
- async function walk(dir) {
1856
- let entries;
1857
- try {
1858
- entries = await fs5.readdir(dir, { withFileTypes: true });
1859
- } catch {
1860
- return;
1861
- }
1862
- for (const entry of entries) {
1863
- if (entry.name === "node_modules" || entry.name === ".git") continue;
1864
- const fullPath = path6.join(dir, entry.name);
1865
- if (entry.isDirectory()) {
1866
- await walk(fullPath);
1867
- } else {
1868
- if (input.glob || input.include) {
1869
- const filter = input.glob ?? input.include;
1870
- if (!simpleExtMatch(entry.name, filter)) continue;
1871
- }
1872
- try {
1873
- const content = await fs5.readFile(fullPath, "utf-8");
1874
- if (regex.test(content)) {
1875
- matches.push(fullPath);
1876
- }
1877
- } catch {
1878
- }
1879
- }
1880
- }
1881
- }
1882
- await walk(searchPath);
1883
- if (matches.length === 0) {
1884
- return { content: "No matches found." };
1885
- }
1886
- return { content: matches.join("\n") };
1887
- }
1888
- function simpleExtMatch(filename, pattern) {
1889
- if (pattern.startsWith("*.")) {
1890
- return filename.endsWith(pattern.slice(1));
1891
- }
1892
- return filename.includes(pattern.replace(/\*/g, ""));
1893
- }
1894
-
1895
- // src/bin/exe-agent.ts
1896
- function parseArgs(argv) {
1897
- const args = { employee: "" };
1898
- for (let i = 2; i < argv.length; i++) {
1899
- const arg = argv[i];
1900
- if (arg === "--employee" && argv[i + 1]) args.employee = argv[++i];
1901
- else if (arg === "--model" && argv[i + 1]) args.model = argv[++i];
1902
- else if (arg === "--provider" && argv[i + 1]) args.provider = argv[++i];
1903
- else if (arg === "--api-key" && argv[i + 1]) args.apiKey = argv[++i];
1904
- else if (arg === "--base-url" && argv[i + 1]) args.baseUrl = argv[++i];
1905
- else if (!arg.startsWith("-") && !args.employee) args.employee = arg;
1906
- }
1907
- return args;
1908
- }
1909
- function loadEmployee(name) {
1910
- try {
1911
- const rosterPath = path7.join(os2.homedir(), ".exe-os", "exe-employees.json");
1912
- const roster = JSON.parse(readFileSync(rosterPath, "utf8"));
1913
- return roster.find((e) => e.name === name) ?? null;
1914
- } catch {
1915
- return null;
1916
- }
1917
- }
1918
- function createProvider(args) {
1919
- const providerType = args.provider ?? "anthropic";
1920
- const apiKey = args.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
1921
- switch (providerType) {
1922
- case "anthropic":
1923
- return new AnthropicProvider("anthropic", {
1924
- apiKey,
1925
- baseUrl: args.baseUrl,
1926
- defaultModel: args.model
1927
- });
1928
- case "openai-compat":
1929
- case "openrouter":
1930
- return new OpenAICompatProvider(providerType, {
1931
- apiKey: args.apiKey ?? process.env.OPENROUTER_API_KEY ?? "",
1932
- baseUrl: args.baseUrl ?? "https://openrouter.ai/api/v1",
1933
- defaultModel: args.model
1934
- });
1935
- case "ollama":
1936
- return new OllamaProvider("ollama", {
1937
- host: args.baseUrl ?? "http://localhost:11434",
1938
- defaultModel: args.model
1939
- });
1940
- default:
1941
- throw new Error(`Unknown provider: ${providerType}`);
1942
- }
1943
- }
1944
- function registerBuiltinTools(registry) {
1945
- registry.register(BashTool);
1946
- registry.register(FileReadTool);
1947
- registry.register(FileEditTool);
1948
- registry.register(FileWriteTool);
1949
- registry.register(GlobTool);
1950
- registry.register(GrepTool);
1951
- }
1952
- async function main() {
1953
- const args = parseArgs(process.argv);
1954
- if (!args.employee) {
1955
- console.error("Usage: exe-agent --employee <name> [--model <model>] [--provider <provider>]");
1956
- process.exit(1);
1957
- }
1958
- const employee = loadEmployee(args.employee);
1959
- if (!employee) {
1960
- console.error(`Employee "${args.employee}" not found in roster.`);
1961
- process.exit(1);
1962
- }
1963
- const provider = createProvider(args);
1964
- const model = args.model ?? "claude-sonnet-4-20250514";
1965
- const registry = new ToolRegistry();
1966
- registerBuiltinTools(registry);
1967
- const abortController = new AbortController();
1968
- const shutdown = () => {
1969
- process.stderr.write("\n[exe-agent] Shutting down...\n");
1970
- abortController.abort();
1971
- };
1972
- process.on("SIGINT", shutdown);
1973
- process.on("SIGTERM", shutdown);
1974
- const config = {
1975
- provider,
1976
- model,
1977
- systemPrompt: getSessionPrompt(employee.systemPrompt),
1978
- tools: registry,
1979
- hooks: createDefaultHooks(),
1980
- permissions: EMPLOYEE_PERMISSIONS,
1981
- maxTurns: 100,
1982
- cwd: process.cwd(),
1983
- agentId: args.employee,
1984
- abortController
1985
- };
1986
- const renderer = createTerminalRenderer();
1987
- const history = [];
1988
- console.error(`[exe-agent] ${args.employee} (${employee.role}) online \u2014 ${provider.name}/${model}`);
1989
- console.error(`[exe-agent] Tools: ${registry.names().join(", ")}`);
1990
- const rl = createInterface({ input: process.stdin, output: process.stderr, prompt: "> " });
1991
- rl.prompt();
1992
- rl.on("line", async (line) => {
1993
- const input = line.trim();
1994
- if (!input) {
1995
- rl.prompt();
1996
- return;
1997
- }
1998
- if (input === "/quit" || input === "/exit") {
1999
- rl.close();
2000
- return;
2001
- }
2002
- const events = agentLoop(input, history, config);
2003
- await renderAgentEvents(events, renderer);
2004
- history.push({ role: "user", content: input });
2005
- process.stderr.write("\n");
2006
- rl.prompt();
2007
- });
2008
- rl.on("close", () => {
2009
- console.error("[exe-agent] Session ended.");
2010
- process.exit(0);
2011
- });
2012
- }
2013
- main().catch((err) => {
2014
- console.error(`[exe-agent] Fatal: ${err instanceof Error ? err.message : String(err)}`);
2015
- process.exit(1);
2016
- });