ccjk 13.3.5 → 13.3.7

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 (150) hide show
  1. package/dist/chunks/agent-teams.mjs +7 -5
  2. package/dist/chunks/agent.mjs +2 -2
  3. package/dist/chunks/agents.mjs +16 -16
  4. package/dist/chunks/api-cli.mjs +6 -6
  5. package/dist/chunks/api-providers.mjs +1 -1
  6. package/dist/chunks/api.mjs +4 -4
  7. package/dist/chunks/auto-bootstrap.mjs +1 -1
  8. package/dist/chunks/auto-fix.mjs +49 -4
  9. package/dist/chunks/auto-fixer.mjs +7 -5
  10. package/dist/chunks/auto-init.mjs +9 -7208
  11. package/dist/chunks/auto-memory-bridge.mjs +9 -3
  12. package/dist/chunks/auto-updater.mjs +9 -9
  13. package/dist/chunks/auto-upgrade.mjs +5 -3
  14. package/dist/chunks/banner.mjs +4 -3
  15. package/dist/chunks/boost.mjs +118 -62
  16. package/dist/chunks/ccjk-agents.mjs +3 -3
  17. package/dist/chunks/ccjk-all.mjs +7 -7
  18. package/dist/chunks/ccjk-config.mjs +2 -2
  19. package/dist/chunks/ccjk-hooks.mjs +4 -4
  20. package/dist/chunks/ccjk-mcp.mjs +5 -5
  21. package/dist/chunks/ccjk-setup.mjs +5 -5
  22. package/dist/chunks/ccjk-skills.mjs +5 -5
  23. package/dist/chunks/ccr.mjs +18 -16
  24. package/dist/chunks/ccu.mjs +2 -2
  25. package/dist/chunks/check-updates.mjs +8 -8
  26. package/dist/chunks/claude-code-config-manager.mjs +11 -8
  27. package/dist/chunks/claude-code-incremental-manager.mjs +7 -7
  28. package/dist/chunks/claude-config.mjs +1 -1
  29. package/dist/chunks/claude-wrapper.mjs +1 -1
  30. package/dist/chunks/cli-hook.mjs +15 -15
  31. package/dist/chunks/codex-config-switch.mjs +7 -7
  32. package/dist/chunks/codex-provider-manager.mjs +7 -7
  33. package/dist/chunks/codex-uninstaller.mjs +2 -2
  34. package/dist/chunks/codex.mjs +5 -5
  35. package/dist/chunks/commands.mjs +2 -2
  36. package/dist/chunks/commands2.mjs +3 -3
  37. package/dist/chunks/commit.mjs +2 -2
  38. package/dist/chunks/completion.mjs +2 -2
  39. package/dist/chunks/config-consolidator.mjs +2 -2
  40. package/dist/chunks/config-switch.mjs +8 -8
  41. package/dist/chunks/config.mjs +6 -5
  42. package/dist/chunks/config2.mjs +5 -5
  43. package/dist/chunks/config3.mjs +4 -4
  44. package/dist/chunks/constants.mjs +1 -1
  45. package/dist/chunks/context-opt.mjs +92 -90
  46. package/dist/chunks/context.mjs +659 -0
  47. package/dist/chunks/dashboard.mjs +14 -9
  48. package/dist/chunks/doctor.mjs +4 -4
  49. package/dist/chunks/eval.mjs +502 -0
  50. package/dist/chunks/evolution.mjs +46 -39
  51. package/dist/chunks/health-alerts.mjs +9 -9
  52. package/dist/chunks/help.mjs +1 -1
  53. package/dist/chunks/hook-installer.mjs +6 -3
  54. package/dist/chunks/index.mjs +23 -0
  55. package/dist/chunks/index10.mjs +634 -571
  56. package/dist/chunks/index11.mjs +1061 -569
  57. package/dist/chunks/index12.mjs +914 -1076
  58. package/dist/chunks/index13.mjs +136 -951
  59. package/dist/chunks/index14.mjs +209 -185
  60. package/dist/chunks/index2.mjs +19 -24
  61. package/dist/chunks/index3.mjs +19085 -12
  62. package/dist/chunks/index4.mjs +16 -19092
  63. package/dist/chunks/index5.mjs +7602 -16
  64. package/dist/chunks/index6.mjs +159 -7590
  65. package/dist/chunks/index7.mjs +1602 -171
  66. package/dist/chunks/index8.mjs +19 -1602
  67. package/dist/chunks/index9.mjs +612 -15
  68. package/dist/chunks/init.mjs +26 -19
  69. package/dist/chunks/installer.mjs +5 -5
  70. package/dist/chunks/installer2.mjs +2 -2
  71. package/dist/chunks/intent-engine.mjs +1 -1
  72. package/dist/chunks/interview.mjs +4 -4
  73. package/dist/chunks/manager.mjs +1 -1
  74. package/dist/chunks/marketplace.mjs +2 -2
  75. package/dist/chunks/mcp-cli.mjs +12 -12
  76. package/dist/chunks/mcp.mjs +8 -8
  77. package/dist/chunks/memory.mjs +8 -8
  78. package/dist/chunks/menu-hierarchical.mjs +24 -22
  79. package/dist/chunks/menu.mjs +27 -22
  80. package/dist/chunks/metrics-display.mjs +2 -2
  81. package/dist/chunks/migrator.mjs +1 -1
  82. package/dist/chunks/monitor.mjs +2 -2
  83. package/dist/chunks/notification.mjs +6 -6
  84. package/dist/chunks/onboarding-wizard.mjs +6 -5
  85. package/dist/chunks/onboarding.mjs +4 -4
  86. package/dist/chunks/package.mjs +1 -1
  87. package/dist/chunks/paradigm.mjs +2 -2
  88. package/dist/chunks/permission-manager.mjs +2 -2
  89. package/dist/chunks/permissions.mjs +3 -3
  90. package/dist/chunks/persistence-manager.mjs +19 -12
  91. package/dist/chunks/persistence.mjs +5 -3
  92. package/dist/chunks/plugin.mjs +2 -2
  93. package/dist/chunks/prompts.mjs +5 -5
  94. package/dist/chunks/providers.mjs +2 -2
  95. package/dist/chunks/quick-actions.mjs +7 -6
  96. package/dist/chunks/quick-provider.mjs +5 -4
  97. package/dist/chunks/quick-setup.mjs +20 -15
  98. package/dist/chunks/remote.mjs +15 -16
  99. package/dist/chunks/{convoy-manager.mjs → session-manager.mjs} +1129 -1095
  100. package/dist/chunks/session.mjs +2 -2
  101. package/dist/chunks/sessions.mjs +3 -3
  102. package/dist/chunks/silent-updater.mjs +1 -1
  103. package/dist/chunks/simple-config.mjs +2 -2
  104. package/dist/chunks/skill2.mjs +3 -3
  105. package/dist/chunks/skills-sync.mjs +5 -5
  106. package/dist/chunks/skills.mjs +3 -3
  107. package/dist/chunks/slash-commands.mjs +9 -8
  108. package/dist/chunks/smart-defaults.mjs +9 -5
  109. package/dist/chunks/startup.mjs +1 -1
  110. package/dist/chunks/stats.mjs +2 -2
  111. package/dist/chunks/status.mjs +37 -22
  112. package/dist/chunks/team.mjs +3 -3
  113. package/dist/chunks/thinking.mjs +4 -4
  114. package/dist/chunks/trace.mjs +2 -2
  115. package/dist/chunks/uninstall.mjs +9 -9
  116. package/dist/chunks/update.mjs +14 -11
  117. package/dist/chunks/upgrade-manager.mjs +3 -3
  118. package/dist/chunks/upgrade.mjs +25 -9
  119. package/dist/chunks/version-checker.mjs +4 -4
  120. package/dist/chunks/vim.mjs +3 -3
  121. package/dist/chunks/workflows.mjs +1 -1
  122. package/dist/chunks/wsl.mjs +1 -1
  123. package/dist/chunks/zero-config.mjs +4 -4
  124. package/dist/cli.mjs +60 -26
  125. package/dist/index.d.mts +4392 -4392
  126. package/dist/index.d.ts +4392 -4392
  127. package/dist/index.mjs +4314 -4314
  128. package/dist/shared/{ccjk.DcKLglJQ.mjs → ccjk.BIxuVL3_.mjs} +2 -2
  129. package/dist/shared/{ccjk.DJdmgr2d.mjs → ccjk.BJMRY2Ra.mjs} +5 -3
  130. package/dist/shared/{ccjk.B1TwPltj.mjs → ccjk.BOu1yav7.mjs} +3 -2
  131. package/dist/shared/{ccjk.mJpVRDZ8.mjs → ccjk.BWFpnOr3.mjs} +1 -1
  132. package/dist/shared/{ccjk.BfIpomdz.mjs → ccjk.CHUEFqmw.mjs} +3 -2
  133. package/dist/shared/{ccjk.CqdbaXqU.mjs → ccjk.CLUL0pAV.mjs} +9 -5
  134. package/dist/shared/{ccjk.Cot9p9_n.mjs → ccjk.Cjj8SVrn.mjs} +1 -1
  135. package/dist/shared/{ccjk.CfrpIIKy.mjs → ccjk.Crd_nEfj.mjs} +38 -20
  136. package/dist/shared/{ccjk.DCw2WnZU.mjs → ccjk.CvChMYvB.mjs} +1 -1
  137. package/dist/shared/{ccjk.CXzjn01x.mjs → ccjk.D8ZLYSZZ.mjs} +1 -1
  138. package/dist/shared/{ccjk.BrPUmTqm.mjs → ccjk.DJuyfrlL.mjs} +164 -82
  139. package/dist/shared/{ccjk.DHXfsrwn.mjs → ccjk.DRfdq6yl.mjs} +4 -4
  140. package/dist/shared/{ccjk.DXRAZcix.mjs → ccjk.DScm_NnL.mjs} +8 -4
  141. package/dist/shared/{ccjk.XsJWJuQP.mjs → ccjk.DfZKjHvG.mjs} +6 -128
  142. package/dist/shared/{ccjk.BFxsJM0k.mjs → ccjk.DwSebGy0.mjs} +4 -3
  143. package/dist/shared/ccjk.DxWqH-EF.mjs +170 -0
  144. package/dist/shared/{ccjk.Cwa_FiTX.mjs → ccjk.I6IuYdc_.mjs} +2 -2
  145. package/dist/shared/{ccjk.DpstNaeR.mjs → ccjk.KpFl2RDA.mjs} +3 -3
  146. package/dist/shared/{ccjk.dYDLfmph.mjs → ccjk._dESH4Rk.mjs} +1 -1
  147. package/dist/shared/{ccjk.BxSmJ8B7.mjs → ccjk.wLJHO0Af.mjs} +2 -1
  148. package/package.json +65 -67
  149. package/dist/chunks/index15.mjs +0 -218
  150. package/dist/shared/{ccjk.c-ETfBZ_.mjs → ccjk.eIn-g1yI.mjs} +96 -96
@@ -1,829 +1,810 @@
1
- import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
2
- import { glob } from 'tinyglobby';
3
- import { e as executionTracer } from '../shared/ccjk.BfIpomdz.mjs';
4
- import { f as fsParadigm } from '../shared/ccjk.BxSmJ8B7.mjs';
1
+ import { EventEmitter } from 'node:events';
2
+ import { n as nanoid } from '../shared/ccjk.BoApaI4j.mjs';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { x as K } from './main.mjs';
5
6
  import { j as join } from '../shared/ccjk.bQ7Dh1g4.mjs';
7
+ import { glob } from 'tinyglobby';
8
+ import { e as executionTracer } from '../shared/ccjk.CHUEFqmw.mjs';
9
+ import { f as fsParadigm } from '../shared/ccjk.wLJHO0Af.mjs';
6
10
  import 'node:child_process';
7
11
  import 'node:fs/promises';
8
- import { homedir } from 'node:os';
9
12
  import 'node:path';
10
- import { EventEmitter } from 'node:events';
11
- import { n as nanoid } from '../shared/ccjk.BoApaI4j.mjs';
12
- import { x as K } from './main.mjs';
13
13
 
14
- const DEPTH_TOKEN_BUDGETS = {
15
- L0: 100,
16
- // Abstract summary only
17
- L1: 2e3,
18
- // Overview
19
- L2: Number.POSITIVE_INFINITY
20
- // Full content
21
- };
22
- const PHASE_BUDGET_MULTIPLIERS = {
23
- exploring: 2,
24
- executing: 0.6,
25
- generating: 0.4,
26
- reviewing: 1.2,
27
- idle: 1
28
- };
29
- const CHARS_PER_TOKEN = 4;
30
- function estimateTokens(text) {
31
- return Math.ceil(text.length / CHARS_PER_TOKEN);
32
- }
33
- function truncateToDepth(content, depth) {
34
- if (depth === "L2") return content;
35
- const maxChars = DEPTH_TOKEN_BUDGETS[depth] * CHARS_PER_TOKEN;
36
- if (content.length <= maxChars) return content;
37
- if (depth === "L0") {
38
- const firstPara = content.split(/\n\n/)[0];
39
- return firstPara.length <= maxChars ? firstPara : content.slice(0, maxChars);
14
+ class GitBackedStateManager extends EventEmitter {
15
+ config;
16
+ worktrees = /* @__PURE__ */ new Map();
17
+ initialized = false;
18
+ pendingCommits = /* @__PURE__ */ new Map();
19
+ constructor(config = {}) {
20
+ super();
21
+ this.config = {
22
+ workspaceRoot: config.workspaceRoot ?? join(homedir(), ".ccjk", "workspace"),
23
+ useWorktrees: config.useWorktrees ?? true,
24
+ autoCommit: config.autoCommit ?? true,
25
+ compressionLevel: config.compressionLevel ?? 6,
26
+ verbose: config.verbose ?? false,
27
+ commitBatchInterval: config.commitBatchInterval ?? 1e3
28
+ };
40
29
  }
41
- return `${content.slice(0, maxChars)}
42
- ... (truncated, use L2 for full content)`;
43
- }
44
- class ContextLoader {
45
- cache = /* @__PURE__ */ new Map();
46
- defaultMaxSize = 1e5;
47
- // 100KB default
48
- defaultTokenBudget = 8e3;
49
- // ~32KB at 4 chars/token
50
30
  /**
51
- * Resolve effective depth from options.
52
- * If depth is explicit, use it. Otherwise derive from tokenBudget.
31
+ * Initialize the workspace with Git
53
32
  */
54
- resolveDepth(tokenBudget, explicitDepth) {
55
- if (explicitDepth) return explicitDepth;
56
- if (tokenBudget <= DEPTH_TOKEN_BUDGETS.L0) return "L0";
57
- if (tokenBudget <= DEPTH_TOKEN_BUDGETS.L1) return "L1";
58
- return "L2";
33
+ async initialize() {
34
+ if (this.initialized)
35
+ return;
36
+ const { workspaceRoot } = this.config;
37
+ try {
38
+ if (!existsSync(workspaceRoot)) {
39
+ mkdirSync(workspaceRoot, { recursive: true });
40
+ this.log(`Created workspace: ${workspaceRoot}`);
41
+ }
42
+ const gitDir = join(workspaceRoot, ".git");
43
+ if (!existsSync(gitDir)) {
44
+ await this.exec("git", ["init"], workspaceRoot);
45
+ this.log("Initialized Git repository");
46
+ const gitignore = `
47
+ # Sensitive data
48
+ *.env
49
+ *.key
50
+ *.pem
51
+ credentials.json
52
+
53
+ # Temporary files
54
+ *.tmp
55
+ *.log
56
+ *.swp
57
+
58
+ # OS files
59
+ .DS_Store
60
+ Thumbs.db
61
+ `;
62
+ writeFileSync(join(workspaceRoot, ".gitignore"), gitignore.trim());
63
+ await this.exec("git", ["add", ".gitignore"], workspaceRoot);
64
+ await this.exec("git", ["commit", "-m", "Initial commit"], workspaceRoot);
65
+ this.log("Created initial commit");
66
+ }
67
+ const agentsDir = join(workspaceRoot, "agents");
68
+ if (!existsSync(agentsDir)) {
69
+ mkdirSync(agentsDir, { recursive: true });
70
+ }
71
+ await this.loadExistingWorktrees();
72
+ this.initialized = true;
73
+ this.emit("initialized");
74
+ this.log("GitBackedStateManager initialized");
75
+ } catch (error) {
76
+ const err = error instanceof Error ? error : new Error(String(error));
77
+ this.emit("error", err);
78
+ throw err;
79
+ }
59
80
  }
60
81
  /**
61
- * Load context hierarchically with OpenViking-style tiered depth control.
62
- * Pass tokenBudget to automatically select L0/L1/L2 depth.
82
+ * Create isolated worktree for agent
63
83
  */
64
- async load(options = {}) {
65
- const {
66
- projectRoot = process.cwd(),
67
- maxSize = this.defaultMaxSize,
68
- tokenBudget,
69
- depth: explicitDepth,
70
- layers = ["project", "domain", "task", "execution"],
71
- task
72
- } = options;
73
- const phaseMultiplier = options.taskPhase ? PHASE_BUDGET_MULTIPLIERS[options.taskPhase] ?? 1 : 1;
74
- const effectiveBudget = Math.round((tokenBudget ?? this.defaultTokenBudget) * phaseMultiplier);
75
- const depth = this.resolveDepth(effectiveBudget, explicitDepth);
76
- const cacheKey = this.getCacheKey(projectRoot, layers, task?.id, depth);
77
- const cached = this.cache.get(cacheKey);
78
- if (cached && Date.now() - cached.loadedAt < 6e4) {
79
- executionTracer.logEvent("context-load", {
80
- source: "cache",
81
- layers: layers.length,
82
- size: cached.totalSize,
83
- tokens: cached.totalTokens,
84
- depth
85
- });
86
- return cached;
84
+ async createAgentWorktree(agentId) {
85
+ await this.ensureInitialized();
86
+ const { workspaceRoot, useWorktrees } = this.config;
87
+ if (this.worktrees.has(agentId)) {
88
+ return this.worktrees.get(agentId);
87
89
  }
88
- const context = {
89
- layers: /* @__PURE__ */ new Map(),
90
- totalSize: 0,
91
- totalTokens: 0,
92
- loadedAt: Date.now(),
93
- depth
94
- };
95
- for (const layer of layers) {
96
- const entries = await this.loadLayer(layer, projectRoot, task, depth);
97
- if (entries.length > 0) {
98
- context.layers.set(layer, entries);
99
- context.totalSize += entries.reduce((sum, e) => sum + e.content.length, 0);
100
- context.totalTokens += entries.reduce((sum, e) => sum + e.tokenEstimate, 0);
90
+ const agentPath = join(workspaceRoot, "agents", agentId);
91
+ if (useWorktrees) {
92
+ const branchName = `agent/${agentId}`;
93
+ try {
94
+ await this.exec("git", ["checkout", "--orphan", branchName], workspaceRoot);
95
+ await this.exec("git", ["reset", "--hard"], workspaceRoot);
96
+ await this.exec("git", ["checkout", "main"], workspaceRoot).catch(() => {
97
+ return this.exec("git", ["checkout", "master"], workspaceRoot);
98
+ });
99
+ if (!existsSync(agentPath)) {
100
+ await this.exec("git", ["worktree", "add", agentPath, branchName], workspaceRoot);
101
+ }
102
+ } catch {
103
+ if (!existsSync(agentPath)) {
104
+ mkdirSync(agentPath, { recursive: true });
105
+ }
101
106
  }
102
- if (context.totalSize > maxSize || context.totalTokens > effectiveBudget) {
103
- break;
107
+ } else {
108
+ if (!existsSync(agentPath)) {
109
+ mkdirSync(agentPath, { recursive: true });
104
110
  }
105
111
  }
106
- this.cache.set(cacheKey, context);
107
- executionTracer.logEvent("context-load", {
108
- source: "fresh",
109
- layers: context.layers.size,
110
- size: context.totalSize,
111
- tokens: context.totalTokens,
112
- depth
113
- });
114
- return context;
112
+ const stateFile = join(agentPath, "state.json");
113
+ const mailboxFile = join(agentPath, "mailbox.json");
114
+ if (!existsSync(stateFile)) {
115
+ writeFileSync(stateFile, JSON.stringify({ agentId, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
116
+ }
117
+ if (!existsSync(mailboxFile)) {
118
+ writeFileSync(mailboxFile, JSON.stringify({ inbox: [], outbox: [], archive: [] }, null, 2));
119
+ }
120
+ this.worktrees.set(agentId, agentPath);
121
+ this.emit("worktree:created", agentId, agentPath);
122
+ this.log(`Created worktree for agent: ${agentId}`);
123
+ return agentPath;
115
124
  }
116
125
  /**
117
- * Load a specific context layer
126
+ * Save agent state to Git
118
127
  */
119
- async loadLayer(layer, projectRoot, task, depth = "L2") {
120
- switch (layer) {
121
- case "project":
122
- return this.loadProjectContext(projectRoot, depth);
123
- case "domain":
124
- return this.loadDomainContext(projectRoot, task, depth);
125
- case "task":
126
- return this.loadTaskContext(task, depth);
127
- case "execution":
128
- return this.loadExecutionContext(task, depth);
129
- default:
130
- return [];
128
+ async saveState(agentId, state) {
129
+ await this.ensureInitialized();
130
+ const agentPath = await this.getOrCreateAgentPath(agentId);
131
+ const stateFile = join(agentPath, "state.json");
132
+ let existingState = {};
133
+ if (existsSync(stateFile)) {
134
+ try {
135
+ existingState = JSON.parse(readFileSync(stateFile, "utf-8"));
136
+ } catch {
137
+ }
131
138
  }
139
+ const newState = {
140
+ ...existingState,
141
+ ...state,
142
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
143
+ };
144
+ writeFileSync(stateFile, JSON.stringify(newState, null, 2));
145
+ let commitId = "";
146
+ if (this.config.autoCommit) {
147
+ commitId = await this.batchCommit(agentId, `Update state: ${agentId}`);
148
+ }
149
+ this.emit("state:saved", agentId, commitId);
150
+ this.log(`Saved state for agent: ${agentId}`);
151
+ return commitId;
132
152
  }
133
153
  /**
134
- * Load project-level context
135
- * - README.md
136
- * - CLAUDE.md
137
- * - package.json
138
- * - tsconfig.json
154
+ * Load agent state from Git
139
155
  */
140
- async loadProjectContext(projectRoot, depth = "L2") {
141
- const entries = [];
142
- const files = ["README.md", "CLAUDE.md", "package.json", "tsconfig.json"];
143
- for (const file of files) {
144
- const path = join(projectRoot, file);
145
- if (existsSync(path)) {
146
- const raw = readFileSync(path, "utf-8");
147
- const content = truncateToDepth(raw, depth);
148
- entries.push({
149
- layer: "project",
150
- source: file,
151
- content,
152
- priority: 100,
153
- depth,
154
- tokenEstimate: estimateTokens(content),
155
- metadata: { path }
156
- });
157
- }
156
+ async loadState(agentId) {
157
+ await this.ensureInitialized();
158
+ const agentPath = this.worktrees.get(agentId);
159
+ if (!agentPath) {
160
+ return null;
161
+ }
162
+ const stateFile = join(agentPath, "state.json");
163
+ if (!existsSync(stateFile)) {
164
+ return null;
165
+ }
166
+ try {
167
+ const state = JSON.parse(readFileSync(stateFile, "utf-8"));
168
+ this.emit("state:loaded", agentId);
169
+ this.log(`Loaded state for agent: ${agentId}`);
170
+ return state;
171
+ } catch {
172
+ return null;
158
173
  }
159
- return entries;
160
174
  }
161
175
  /**
162
- * Load domain-specific context
163
- * Based on task type/domain, load relevant files
176
+ * Rollback to previous state
164
177
  */
165
- async loadDomainContext(projectRoot, task, depth = "L2") {
166
- if (!task) return [];
167
- const entries = [];
168
- const structure = await fsParadigm.detect(projectRoot);
169
- const domain = this.inferDomain(task);
170
- if (!domain) {
171
- const sourceFiles = fsParadigm.getFilesByRole(structure, "source");
172
- for (const file of sourceFiles.slice(0, 3)) {
178
+ async rollback(agentId, commitId) {
179
+ await this.ensureInitialized();
180
+ const agentPath = this.worktrees.get(agentId);
181
+ if (!agentPath) {
182
+ throw new Error(`Agent not found: ${agentId}`);
183
+ }
184
+ try {
185
+ await this.exec("git", ["reset", "--hard", commitId], agentPath);
186
+ this.emit("state:rolled-back", agentId, commitId);
187
+ this.log(`Rolled back agent ${agentId} to commit ${commitId}`);
188
+ } catch (error) {
189
+ const err = error instanceof Error ? error : new Error(String(error));
190
+ this.emit("error", err);
191
+ throw err;
192
+ }
193
+ }
194
+ /**
195
+ * Get state history for agent
196
+ */
197
+ async getHistory(agentId, limit = 10) {
198
+ await this.ensureInitialized();
199
+ const agentPath = this.worktrees.get(agentId);
200
+ if (!agentPath) {
201
+ return [];
202
+ }
203
+ try {
204
+ const result = await this.exec(
205
+ "git",
206
+ ["log", `--max-count=${limit}`, "--format=%H|%s|%ct", "--", "state.json"],
207
+ agentPath
208
+ );
209
+ const snapshots = [];
210
+ for (const line of result.stdout.split("\n").filter(Boolean)) {
211
+ const [commitId, message, timestamp] = line.split("|");
212
+ const stateResult = await this.exec(
213
+ "git",
214
+ ["show", `${commitId}:state.json`],
215
+ agentPath
216
+ ).catch(() => ({ stdout: "{}" }));
217
+ let state;
173
218
  try {
174
- const raw = readFileSync(file, "utf-8");
175
- const content = truncateToDepth(raw, depth);
176
- entries.push({
177
- layer: "domain",
178
- source: file.replace(projectRoot, ""),
179
- content,
180
- priority: 75,
181
- depth,
182
- tokenEstimate: estimateTokens(content),
183
- metadata: { paradigm: structure.paradigm, path: file }
184
- });
219
+ state = JSON.parse(stateResult.stdout);
185
220
  } catch {
221
+ state = {};
186
222
  }
223
+ snapshots.push({
224
+ id: nanoid(),
225
+ agentId,
226
+ commitId,
227
+ timestamp: Number.parseInt(timestamp, 10) * 1e3,
228
+ message,
229
+ state
230
+ });
187
231
  }
188
- return entries;
232
+ return snapshots;
233
+ } catch {
234
+ return [];
189
235
  }
190
- const patterns = this.getDomainPatterns(domain, structure.paradigm);
191
- for (const pattern of patterns) {
192
- const files = await glob([pattern], {
193
- cwd: projectRoot,
194
- absolute: true,
195
- ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
196
- });
197
- for (const file of files.slice(0, 5)) {
198
- try {
199
- const raw = readFileSync(file, "utf-8");
200
- const content = truncateToDepth(raw, depth);
201
- entries.push({
202
- layer: "domain",
203
- source: file.replace(projectRoot, ""),
204
- content,
205
- priority: 80,
206
- depth,
207
- tokenEstimate: estimateTokens(content),
208
- metadata: { domain, paradigm: structure.paradigm, path: file }
209
- });
210
- } catch {
211
- }
236
+ }
237
+ /**
238
+ * Cleanup agent worktree
239
+ */
240
+ async cleanup(agentId) {
241
+ await this.ensureInitialized();
242
+ const agentPath = this.worktrees.get(agentId);
243
+ if (!agentPath) {
244
+ return;
245
+ }
246
+ try {
247
+ if (this.config.useWorktrees) {
248
+ await this.exec("git", ["worktree", "remove", agentPath, "--force"], this.config.workspaceRoot);
212
249
  }
250
+ this.worktrees.delete(agentId);
251
+ this.emit("worktree:removed", agentId);
252
+ this.log(`Cleaned up worktree for agent: ${agentId}`);
253
+ } catch (error) {
254
+ const err = error instanceof Error ? error : new Error(String(error));
255
+ this.emit("error", err);
256
+ throw err;
213
257
  }
214
- return entries;
215
258
  }
216
259
  /**
217
- * Load task-specific context
260
+ * Get all agent IDs
218
261
  */
219
- async loadTaskContext(task, depth = "L2") {
220
- if (!task) return [];
221
- const entries = [];
222
- if (task.description) {
223
- const content = truncateToDepth(task.description, depth);
224
- entries.push({
225
- layer: "task",
226
- source: "task-description",
227
- content,
228
- priority: 90,
229
- depth,
230
- tokenEstimate: estimateTokens(content),
231
- metadata: { taskId: task.id }
232
- });
233
- }
234
- if (task.input && depth !== "L0") {
235
- const content = truncateToDepth(JSON.stringify(task.input, null, 2), depth);
236
- entries.push({
237
- layer: "task",
238
- source: "task-input",
239
- content,
240
- priority: 85,
241
- depth,
242
- tokenEstimate: estimateTokens(content),
243
- metadata: { taskId: task.id }
244
- });
245
- }
246
- return entries;
262
+ getAgentIds() {
263
+ return Array.from(this.worktrees.keys());
247
264
  }
248
265
  /**
249
- * Load execution-specific context
250
- * - Recent errors
251
- * - Previous attempts
252
- * - Related task outputs
266
+ * Check if agent exists
253
267
  */
254
- async loadExecutionContext(task, depth = "L2") {
255
- if (!task) return [];
256
- const entries = [];
257
- if (task.output && depth !== "L0") {
258
- const content = truncateToDepth(JSON.stringify(task.output, null, 2), depth);
259
- entries.push({
260
- layer: "execution",
261
- source: "previous-output",
262
- content,
263
- priority: 70,
264
- depth,
265
- tokenEstimate: estimateTokens(content),
266
- metadata: { taskId: task.id }
267
- });
268
- }
269
- return entries;
268
+ hasAgent(agentId) {
269
+ return this.worktrees.has(agentId);
270
270
  }
271
271
  /**
272
- * Infer domain from task
272
+ * Get workspace root path
273
273
  */
274
- inferDomain(task) {
275
- const text = `${task.name} ${task.description || ""}`.toLowerCase();
276
- const domains = {
277
- api: ["api", "endpoint", "route", "controller"],
278
- ui: ["ui", "component", "view", "page", "frontend"],
279
- database: ["database", "db", "schema", "migration", "model"],
280
- auth: ["auth", "login", "user", "permission", "session"],
281
- test: ["test", "spec", "e2e", "unit", "integration"],
282
- config: ["config", "settings", "env", "setup"]
283
- };
284
- for (const [domain, keywords] of Object.entries(domains)) {
285
- if (keywords.some((kw) => text.includes(kw))) {
286
- return domain;
287
- }
288
- }
289
- return null;
274
+ getWorkspaceRoot() {
275
+ return this.config.workspaceRoot;
290
276
  }
291
277
  /**
292
- * Get file patterns for a domain (paradigm-aware)
278
+ * Get agent path
293
279
  */
294
- getDomainPatterns(domain, paradigm) {
295
- const basePatterns = {
296
- api: ["**/api/**/*.ts", "**/routes/**/*.ts", "**/controllers/**/*.ts"],
297
- ui: ["**/components/**/*.{tsx,vue}", "**/pages/**/*.{tsx,vue}", "**/views/**/*.{tsx,vue}"],
298
- database: ["**/models/**/*.ts", "**/schema/**/*.ts", "**/migrations/**/*.ts"],
299
- auth: ["**/auth/**/*.ts", "**/middleware/auth*.ts"],
300
- test: ["**/*.test.ts", "**/*.spec.ts"],
301
- config: ["**/config/**/*.ts", "**/*.config.ts"]
302
- };
303
- let patterns = basePatterns[domain] || [];
304
- if (paradigm === "monorepo") {
305
- patterns = patterns.map((p) => `packages/*/${p}`);
306
- } else if (paradigm === "fullstack") {
307
- if (domain === "ui") {
308
- patterns = patterns.map((p) => `client/${p}`);
309
- } else if (domain === "api") {
310
- patterns = patterns.map((p) => `server/${p}`);
311
- }
280
+ getAgentPath(agentId) {
281
+ return this.worktrees.get(agentId);
282
+ }
283
+ /**
284
+ * Get state history for agent
285
+ */
286
+ async getStateHistory(agentId, limit = 20) {
287
+ await this.ensureInitialized();
288
+ const agentPath = this.worktrees.get(agentId);
289
+ if (!agentPath) {
290
+ return { agentId, commits: [], totalCommits: 0 };
291
+ }
292
+ try {
293
+ const result = await this.exec(
294
+ "git",
295
+ ["log", `--max-count=${limit}`, "--format=%H|%s|%ct", "--", "state.json"],
296
+ agentPath
297
+ );
298
+ const commits = result.stdout.split("\n").filter(Boolean).map((line) => {
299
+ const [id, message, timestamp] = line.split("|");
300
+ return {
301
+ id,
302
+ message,
303
+ timestamp: Number.parseInt(timestamp, 10) * 1e3
304
+ };
305
+ });
306
+ const countResult = await this.exec(
307
+ "git",
308
+ ["rev-list", "--count", "HEAD", "--", "state.json"],
309
+ agentPath
310
+ ).catch(() => ({ stdout: "0" }));
311
+ return {
312
+ agentId,
313
+ commits,
314
+ totalCommits: Number.parseInt(countResult.stdout.trim(), 10) || commits.length
315
+ };
316
+ } catch {
317
+ return { agentId, commits: [], totalCommits: 0 };
312
318
  }
313
- return patterns;
314
319
  }
315
320
  /**
316
- * Get cache key
321
+ * Create a named snapshot of current state
317
322
  */
318
- getCacheKey(projectRoot, layers, taskId, depth = "L2") {
319
- return `${projectRoot}:${layers.join(",")}:${taskId || "none"}:${depth}`;
323
+ async createSnapshot(agentId, description) {
324
+ await this.ensureInitialized();
325
+ const agentPath = this.worktrees.get(agentId);
326
+ if (!agentPath) {
327
+ throw new Error(`Agent not found: ${agentId}`);
328
+ }
329
+ const commitId = await this.commit(agentId, `Snapshot: ${description}`);
330
+ if (!commitId) {
331
+ const result = await this.exec("git", ["rev-parse", "HEAD"], agentPath);
332
+ const currentCommitId = result.stdout.trim();
333
+ const state2 = await this.loadState(agentId);
334
+ return {
335
+ id: nanoid(),
336
+ agentId,
337
+ commitId: currentCommitId,
338
+ timestamp: Date.now(),
339
+ message: description,
340
+ state: state2 ?? {}
341
+ };
342
+ }
343
+ const state = await this.loadState(agentId);
344
+ return {
345
+ id: nanoid(),
346
+ agentId,
347
+ commitId,
348
+ timestamp: Date.now(),
349
+ message: description,
350
+ state: state ?? {}
351
+ };
320
352
  }
321
353
  /**
322
- * Clear cache
354
+ * Restore state from a snapshot
323
355
  */
324
- clearCache() {
325
- this.cache.clear();
356
+ async restoreSnapshot(agentId, snapshotId) {
357
+ await this.rollback(agentId, snapshotId);
326
358
  }
327
359
  /**
328
- * Format context for LLM consumption
360
+ * Sync with remote (if configured)
329
361
  */
330
- formatForLLM(context) {
331
- const sections = [];
332
- sections.push(`<!-- context depth=${context.depth} tokens\u2248${context.totalTokens} -->
333
- `);
334
- const layerOrder = ["project", "domain", "task", "execution"];
335
- for (const layer of layerOrder) {
336
- const entries = context.layers.get(layer);
337
- if (!entries || entries.length === 0) continue;
338
- sections.push(`# ${layer.toUpperCase()} CONTEXT
339
- `);
340
- const sorted = entries.sort((a, b) => b.priority - a.priority);
341
- for (const entry of sorted) {
342
- sections.push(`## ${entry.source} [${entry.depth}]
343
- `);
344
- sections.push(entry.content);
345
- sections.push("\n");
362
+ async syncWithRemote() {
363
+ await this.ensureInitialized();
364
+ try {
365
+ const remoteResult = await this.exec("git", ["remote"], this.config.workspaceRoot);
366
+ if (!remoteResult.stdout.trim()) {
367
+ this.log("No remote configured, skipping sync");
368
+ return;
346
369
  }
347
- }
348
- return sections.join("\n");
349
- }
350
- }
351
- const contextLoader = new ContextLoader();
352
- async function loadContextAtDepth(depth, options = {}) {
353
- return contextLoader.load({ ...options, depth });
354
- }
355
-
356
- const contextLoader$1 = {
357
- __proto__: null,
358
- ContextLoader: ContextLoader,
359
- contextLoader: contextLoader,
360
- loadContextAtDepth: loadContextAtDepth
361
- };
362
-
363
- function detectTaskPhase(history) {
364
- if (history.length === 0) return "idle";
365
- const recent = history.slice(-10);
366
- const recentText = recent.map((h) => h.content).join(" ").toLowerCase();
367
- if (recentText.includes("write") || recentText.includes("create") || recentText.includes("generating"))
368
- return "generating";
369
- if (recentText.includes("test") || recentText.includes("review") || recentText.includes("check"))
370
- return "reviewing";
371
- if (recentText.includes("edit") || recentText.includes("bash") || recentText.includes("run"))
372
- return "executing";
373
- if (recentText.includes("read") || recentText.includes("search") || recentText.includes("grep"))
374
- return "exploring";
375
- return "idle";
376
- }
377
- function extractFilePaths(content) {
378
- const matches = content.match(/(?:\/[\w./\-]+\.\w+|[\w./\-]+\.(?:ts|js|json|md|py|go|rs))/g);
379
- return matches ? [...new Set(matches)] : [];
380
- }
381
- class SessionIntelligence {
382
- session;
383
- constructor(session) {
384
- this.session = session;
385
- if (!session.intelligence) {
386
- session.intelligence = {
387
- taskPhase: "idle",
388
- filesTouched: [],
389
- keyDecisions: [],
390
- toolCallCount: 0,
391
- compressionWatermark: 0
392
- };
370
+ await this.exec("git", ["pull", "--rebase"], this.config.workspaceRoot);
371
+ await this.exec("git", ["push"], this.config.workspaceRoot);
372
+ this.log("Synced with remote");
373
+ } catch (error) {
374
+ this.log(`Sync failed: ${error}`);
393
375
  }
394
376
  }
395
- get state() {
396
- return this.session.intelligence;
377
+ // ========================================================================
378
+ // Private Methods
379
+ // ========================================================================
380
+ async ensureInitialized() {
381
+ if (!this.initialized) {
382
+ await this.initialize();
383
+ }
397
384
  }
398
- /**
399
- * Observe a new history entry and update intelligence state.
400
- * Call this after every message added to the session.
401
- */
402
- observe(entry) {
403
- const intel = this.state;
404
- if (!intel.userOriginalIntent && entry.role === "user" && entry.content.trim().length > 10) {
405
- intel.userOriginalIntent = entry.content.slice(0, 500);
385
+ async getOrCreateAgentPath(agentId) {
386
+ if (!this.worktrees.has(agentId)) {
387
+ await this.createAgentWorktree(agentId);
406
388
  }
407
- const files = extractFilePaths(entry.content);
408
- for (const f of files) {
409
- if (!intel.filesTouched.includes(f)) {
410
- intel.filesTouched.push(f);
389
+ return this.worktrees.get(agentId);
390
+ }
391
+ async loadExistingWorktrees() {
392
+ const agentsDir = join(this.config.workspaceRoot, "agents");
393
+ if (!existsSync(agentsDir)) {
394
+ return;
395
+ }
396
+ try {
397
+ const { readdirSync, statSync } = await import('node:fs');
398
+ const entries = readdirSync(agentsDir);
399
+ for (const entry of entries) {
400
+ const entryPath = join(agentsDir, entry);
401
+ if (statSync(entryPath).isDirectory()) {
402
+ this.worktrees.set(entry, entryPath);
403
+ this.log(`Loaded existing worktree: ${entry}`);
404
+ }
411
405
  }
406
+ } catch {
412
407
  }
413
- if (entry.role === "assistant" && /\b(bash|read|write|edit|grep|glob)\b/i.test(entry.content)) {
414
- intel.toolCallCount++;
408
+ }
409
+ async batchCommit(agentId, message) {
410
+ const existing = this.pendingCommits.get(agentId);
411
+ if (existing) {
412
+ clearTimeout(existing);
415
413
  }
416
- intel.taskPhase = detectTaskPhase(this.session.history);
414
+ return new Promise((resolve) => {
415
+ const timeout = setTimeout(async () => {
416
+ this.pendingCommits.delete(agentId);
417
+ const commitId = await this.commit(agentId, message);
418
+ resolve(commitId);
419
+ }, this.config.commitBatchInterval);
420
+ this.pendingCommits.set(agentId, timeout);
421
+ });
417
422
  }
418
- /**
419
- * Record a key decision explicitly.
420
- */
421
- recordDecision(decision) {
422
- const intel = this.state;
423
- if (!intel.keyDecisions.includes(decision)) {
424
- intel.keyDecisions.push(decision.slice(0, 200));
425
- if (intel.keyDecisions.length > 20) {
426
- intel.keyDecisions.shift();
423
+ async commit(agentId, message) {
424
+ const agentPath = this.worktrees.get(agentId);
425
+ if (!agentPath) {
426
+ return "";
427
+ }
428
+ try {
429
+ await this.exec("git", ["add", "-A"], agentPath);
430
+ const statusResult = await this.exec("git", ["status", "--porcelain"], agentPath);
431
+ if (!statusResult.stdout.trim()) {
432
+ return "";
427
433
  }
434
+ await this.exec("git", ["commit", "-m", message], agentPath);
435
+ const result = await this.exec("git", ["rev-parse", "HEAD"], agentPath);
436
+ return result.stdout.trim();
437
+ } catch {
438
+ return "";
428
439
  }
429
440
  }
430
- /**
431
- * Mark that history up to index N has been compressed.
432
- */
433
- markCompressed(upToIndex) {
434
- this.state.compressionWatermark = upToIndex;
441
+ async exec(cmd, args, cwd) {
442
+ const result = await K(cmd, args, { nodeOptions: { cwd } });
443
+ return {
444
+ stdout: result.stdout,
445
+ stderr: result.stderr
446
+ };
435
447
  }
436
- /**
437
- * Generate a system prompt injection that preserves intent across compaction.
438
- * Inject this whenever context is about to be compressed.
439
- */
440
- getSystemPromptInjection() {
441
- const intel = this.state;
442
- const parts = ["<!-- session-intelligence -->"];
443
- if (intel.userOriginalIntent) {
444
- parts.push(`Original user intent: ${intel.userOriginalIntent}`);
445
- }
446
- if (intel.keyDecisions.length > 0) {
447
- parts.push(`Key decisions: ${intel.keyDecisions.join("; ")}`);
448
- }
449
- if (intel.filesTouched.length > 0) {
450
- parts.push(`Files touched: ${intel.filesTouched.slice(-20).join(", ")}`);
448
+ log(message) {
449
+ if (this.config.verbose) {
450
+ console.log(`[GitBackedState] ${message}`);
451
451
  }
452
- parts.push(`Task phase: ${intel.taskPhase} | Tool calls: ${intel.toolCallCount}`);
453
- parts.push("<!-- /session-intelligence -->");
454
- return parts.join("\n");
455
452
  }
456
- /**
457
- * Returns true if the session has enough context to be worth preserving.
458
- */
459
- hasSignificantContext() {
460
- const intel = this.state;
461
- return !!intel.userOriginalIntent || intel.toolCallCount > 3 || intel.filesTouched.length > 0;
453
+ }
454
+ let globalStateManager = null;
455
+ function getGlobalStateManager(config) {
456
+ if (!globalStateManager) {
457
+ globalStateManager = new GitBackedStateManager(config);
462
458
  }
459
+ return globalStateManager;
463
460
  }
464
461
 
465
- class GitBackedStateManager extends EventEmitter {
466
- config;
467
- worktrees = /* @__PURE__ */ new Map();
462
+ const gitBackedState = {
463
+ __proto__: null,
464
+ GitBackedStateManager: GitBackedStateManager,
465
+ getGlobalStateManager: getGlobalStateManager
466
+ };
467
+
468
+ class ConvoyManager extends EventEmitter {
469
+ stateManager;
470
+ convoys = /* @__PURE__ */ new Map();
468
471
  initialized = false;
469
- pendingCommits = /* @__PURE__ */ new Map();
470
- constructor(config = {}) {
472
+ constructor(stateManager) {
471
473
  super();
472
- this.config = {
473
- workspaceRoot: config.workspaceRoot ?? join(homedir(), ".ccjk", "workspace"),
474
- useWorktrees: config.useWorktrees ?? true,
475
- autoCommit: config.autoCommit ?? true,
476
- compressionLevel: config.compressionLevel ?? 6,
477
- verbose: config.verbose ?? false,
478
- commitBatchInterval: config.commitBatchInterval ?? 1e3
479
- };
474
+ this.stateManager = stateManager ?? getGlobalStateManager();
480
475
  }
481
476
  /**
482
- * Initialize the workspace with Git
477
+ * Initialize convoy manager
483
478
  */
484
479
  async initialize() {
485
480
  if (this.initialized)
486
481
  return;
487
- const { workspaceRoot } = this.config;
488
- try {
489
- if (!existsSync(workspaceRoot)) {
490
- mkdirSync(workspaceRoot, { recursive: true });
491
- this.log(`Created workspace: ${workspaceRoot}`);
492
- }
493
- const gitDir = join(workspaceRoot, ".git");
494
- if (!existsSync(gitDir)) {
495
- await this.exec("git", ["init"], workspaceRoot);
496
- this.log("Initialized Git repository");
497
- const gitignore = `
498
- # Sensitive data
499
- *.env
500
- *.key
501
- *.pem
502
- credentials.json
503
-
504
- # Temporary files
505
- *.tmp
506
- *.log
507
- *.swp
508
-
509
- # OS files
510
- .DS_Store
511
- Thumbs.db
512
- `;
513
- writeFileSync(join(workspaceRoot, ".gitignore"), gitignore.trim());
514
- await this.exec("git", ["add", ".gitignore"], workspaceRoot);
515
- await this.exec("git", ["commit", "-m", "Initial commit"], workspaceRoot);
516
- this.log("Created initial commit");
517
- }
518
- const agentsDir = join(workspaceRoot, "agents");
519
- if (!existsSync(agentsDir)) {
520
- mkdirSync(agentsDir, { recursive: true });
521
- }
522
- await this.loadExistingWorktrees();
523
- this.initialized = true;
524
- this.emit("initialized");
525
- this.log("GitBackedStateManager initialized");
526
- } catch (error) {
527
- const err = error instanceof Error ? error : new Error(String(error));
528
- this.emit("error", err);
529
- throw err;
482
+ await this.stateManager.initialize();
483
+ await this.loadConvoys();
484
+ this.initialized = true;
485
+ }
486
+ /**
487
+ * Create new convoy
488
+ */
489
+ async create(name, options = {}) {
490
+ await this.ensureInitialized();
491
+ const convoy = {
492
+ id: `cv-${nanoid(6)}`,
493
+ name,
494
+ description: options.description ?? "",
495
+ tasks: [],
496
+ status: "pending",
497
+ progress: 0,
498
+ totalTasks: 0,
499
+ completedTasks: 0,
500
+ failedTasks: 0,
501
+ cancelledTasks: 0,
502
+ skippedTasks: 0,
503
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
504
+ createdBy: options.createdBy ?? "system",
505
+ notifications: {
506
+ onComplete: options.notifyOnComplete ?? false,
507
+ onFailure: options.notifyOnFailure ?? true,
508
+ onProgress: options.notifyOnProgress ?? false,
509
+ notifyHuman: options.notifyHuman ?? false
510
+ },
511
+ metadata: options.metadata,
512
+ tags: options.tags ?? []
513
+ };
514
+ this.convoys.set(convoy.id, convoy);
515
+ await this.persistConvoy(convoy);
516
+ this.emit("convoy:created", convoy);
517
+ return convoy;
518
+ }
519
+ /**
520
+ * Add task to convoy
521
+ */
522
+ async addTask(convoyId, title, options = {}) {
523
+ await this.ensureInitialized();
524
+ const convoy = this.convoys.get(convoyId);
525
+ if (!convoy) {
526
+ throw new Error(`Convoy not found: ${convoyId}`);
527
+ }
528
+ const task = {
529
+ id: `task-${nanoid(6)}`,
530
+ title,
531
+ description: options.description ?? "",
532
+ status: "pending",
533
+ assignedTo: options.assignedTo,
534
+ dependsOn: options.dependsOn ?? [],
535
+ metadata: options.metadata
536
+ };
537
+ convoy.tasks.push(task);
538
+ convoy.totalTasks = convoy.tasks.length;
539
+ this.updateProgress(convoy);
540
+ await this.persistConvoy(convoy);
541
+ return task;
542
+ }
543
+ /**
544
+ * Add multiple tasks to convoy
545
+ */
546
+ async addTasks(convoyId, tasks) {
547
+ const addedTasks = [];
548
+ for (const { title, options } of tasks) {
549
+ const task = await this.addTask(convoyId, title, options);
550
+ addedTasks.push(task);
530
551
  }
552
+ return addedTasks;
531
553
  }
532
554
  /**
533
- * Create isolated worktree for agent
555
+ * Start convoy execution
534
556
  */
535
- async createAgentWorktree(agentId) {
557
+ async start(convoyId) {
536
558
  await this.ensureInitialized();
537
- const { workspaceRoot, useWorktrees } = this.config;
538
- if (this.worktrees.has(agentId)) {
539
- return this.worktrees.get(agentId);
559
+ const convoy = this.convoys.get(convoyId);
560
+ if (!convoy) {
561
+ throw new Error(`Convoy not found: ${convoyId}`);
540
562
  }
541
- const agentPath = join(workspaceRoot, "agents", agentId);
542
- if (useWorktrees) {
543
- const branchName = `agent/${agentId}`;
544
- try {
545
- await this.exec("git", ["checkout", "--orphan", branchName], workspaceRoot);
546
- await this.exec("git", ["reset", "--hard"], workspaceRoot);
547
- await this.exec("git", ["checkout", "main"], workspaceRoot).catch(() => {
548
- return this.exec("git", ["checkout", "master"], workspaceRoot);
549
- });
550
- if (!existsSync(agentPath)) {
551
- await this.exec("git", ["worktree", "add", agentPath, branchName], workspaceRoot);
552
- }
553
- } catch {
554
- if (!existsSync(agentPath)) {
555
- mkdirSync(agentPath, { recursive: true });
556
- }
557
- }
558
- } else {
559
- if (!existsSync(agentPath)) {
560
- mkdirSync(agentPath, { recursive: true });
561
- }
563
+ if (convoy.status !== "pending" && convoy.status !== "paused") {
564
+ throw new Error(`Cannot start convoy in status: ${convoy.status}`);
562
565
  }
563
- const stateFile = join(agentPath, "state.json");
564
- const mailboxFile = join(agentPath, "mailbox.json");
565
- if (!existsSync(stateFile)) {
566
- writeFileSync(stateFile, JSON.stringify({ agentId, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
566
+ convoy.status = "in_progress";
567
+ convoy.startedAt = convoy.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
568
+ await this.persistConvoy(convoy);
569
+ this.emit("convoy:started", convoy);
570
+ }
571
+ /**
572
+ * Start a task
573
+ */
574
+ async startTask(convoyId, taskId, assignedTo) {
575
+ await this.ensureInitialized();
576
+ const convoy = this.convoys.get(convoyId);
577
+ if (!convoy) {
578
+ throw new Error(`Convoy not found: ${convoyId}`);
567
579
  }
568
- if (!existsSync(mailboxFile)) {
569
- writeFileSync(mailboxFile, JSON.stringify({ inbox: [], outbox: [], archive: [] }, null, 2));
580
+ const task = convoy.tasks.find((t) => t.id === taskId);
581
+ if (!task) {
582
+ throw new Error(`Task not found: ${taskId}`);
570
583
  }
571
- this.worktrees.set(agentId, agentPath);
572
- this.emit("worktree:created", agentId, agentPath);
573
- this.log(`Created worktree for agent: ${agentId}`);
574
- return agentPath;
584
+ const unmetDeps = this.getUnmetDependencies(convoy, task);
585
+ if (unmetDeps.length > 0) {
586
+ throw new Error(`Task has unmet dependencies: ${unmetDeps.join(", ")}`);
587
+ }
588
+ task.status = "in_progress";
589
+ task.startedAt = (/* @__PURE__ */ new Date()).toISOString();
590
+ if (assignedTo) {
591
+ task.assignedTo = assignedTo;
592
+ }
593
+ if (convoy.status === "pending") {
594
+ convoy.status = "in_progress";
595
+ convoy.startedAt = (/* @__PURE__ */ new Date()).toISOString();
596
+ }
597
+ await this.persistConvoy(convoy);
598
+ this.emit("task:started", convoy, task);
575
599
  }
576
600
  /**
577
- * Save agent state to Git
601
+ * Complete a task
578
602
  */
579
- async saveState(agentId, state) {
603
+ async completeTask(convoyId, taskId, result) {
580
604
  await this.ensureInitialized();
581
- const agentPath = await this.getOrCreateAgentPath(agentId);
582
- const stateFile = join(agentPath, "state.json");
583
- let existingState = {};
584
- if (existsSync(stateFile)) {
585
- try {
586
- existingState = JSON.parse(readFileSync(stateFile, "utf-8"));
587
- } catch {
588
- }
605
+ const convoy = this.convoys.get(convoyId);
606
+ if (!convoy) {
607
+ throw new Error(`Convoy not found: ${convoyId}`);
589
608
  }
590
- const newState = {
591
- ...existingState,
592
- ...state,
593
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
594
- };
595
- writeFileSync(stateFile, JSON.stringify(newState, null, 2));
596
- let commitId = "";
597
- if (this.config.autoCommit) {
598
- commitId = await this.batchCommit(agentId, `Update state: ${agentId}`);
609
+ const task = convoy.tasks.find((t) => t.id === taskId);
610
+ if (!task) {
611
+ throw new Error(`Task not found: ${taskId}`);
612
+ }
613
+ task.status = "completed";
614
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
615
+ task.result = result;
616
+ convoy.completedTasks++;
617
+ this.updateProgress(convoy);
618
+ this.checkConvoyCompletion(convoy);
619
+ await this.persistConvoy(convoy);
620
+ this.emit("task:completed", convoy, task);
621
+ if (convoy.notifications.onProgress) {
622
+ this.emit("convoy:progress", convoy, convoy.progress);
599
623
  }
600
- this.emit("state:saved", agentId, commitId);
601
- this.log(`Saved state for agent: ${agentId}`);
602
- return commitId;
603
624
  }
604
625
  /**
605
- * Load agent state from Git
626
+ * Fail a task
606
627
  */
607
- async loadState(agentId) {
628
+ async failTask(convoyId, taskId, error) {
608
629
  await this.ensureInitialized();
609
- const agentPath = this.worktrees.get(agentId);
610
- if (!agentPath) {
611
- return null;
630
+ const convoy = this.convoys.get(convoyId);
631
+ if (!convoy) {
632
+ throw new Error(`Convoy not found: ${convoyId}`);
612
633
  }
613
- const stateFile = join(agentPath, "state.json");
614
- if (!existsSync(stateFile)) {
615
- return null;
634
+ const task = convoy.tasks.find((t) => t.id === taskId);
635
+ if (!task) {
636
+ throw new Error(`Task not found: ${taskId}`);
616
637
  }
617
- try {
618
- const state = JSON.parse(readFileSync(stateFile, "utf-8"));
619
- this.emit("state:loaded", agentId);
620
- this.log(`Loaded state for agent: ${agentId}`);
621
- return state;
622
- } catch {
623
- return null;
638
+ task.status = "failed";
639
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
640
+ task.error = error;
641
+ convoy.failedTasks++;
642
+ this.updateProgress(convoy);
643
+ await this.skipDependentTasks(convoy, taskId);
644
+ this.checkConvoyCompletion(convoy);
645
+ await this.persistConvoy(convoy);
646
+ this.emit("task:failed", convoy, task);
647
+ if (convoy.notifications.onFailure) {
648
+ this.emit("convoy:failed", convoy);
624
649
  }
625
650
  }
626
651
  /**
627
- * Rollback to previous state
652
+ * Cancel a task
628
653
  */
629
- async rollback(agentId, commitId) {
654
+ async cancelTask(convoyId, taskId) {
630
655
  await this.ensureInitialized();
631
- const agentPath = this.worktrees.get(agentId);
632
- if (!agentPath) {
633
- throw new Error(`Agent not found: ${agentId}`);
656
+ const convoy = this.convoys.get(convoyId);
657
+ if (!convoy) {
658
+ throw new Error(`Convoy not found: ${convoyId}`);
634
659
  }
635
- try {
636
- await this.exec("git", ["reset", "--hard", commitId], agentPath);
637
- this.emit("state:rolled-back", agentId, commitId);
638
- this.log(`Rolled back agent ${agentId} to commit ${commitId}`);
639
- } catch (error) {
640
- const err = error instanceof Error ? error : new Error(String(error));
641
- this.emit("error", err);
642
- throw err;
660
+ const task = convoy.tasks.find((t) => t.id === taskId);
661
+ if (!task) {
662
+ throw new Error(`Task not found: ${taskId}`);
643
663
  }
664
+ task.status = "cancelled";
665
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
666
+ convoy.cancelledTasks++;
667
+ this.updateProgress(convoy);
668
+ await this.skipDependentTasks(convoy, taskId);
669
+ this.checkConvoyCompletion(convoy);
670
+ await this.persistConvoy(convoy);
671
+ this.emit("task:cancelled", convoy, task);
644
672
  }
645
673
  /**
646
- * Get state history for agent
674
+ * Pause convoy
647
675
  */
648
- async getHistory(agentId, limit = 10) {
676
+ async pause(convoyId) {
649
677
  await this.ensureInitialized();
650
- const agentPath = this.worktrees.get(agentId);
651
- if (!agentPath) {
652
- return [];
678
+ const convoy = this.convoys.get(convoyId);
679
+ if (!convoy) {
680
+ throw new Error(`Convoy not found: ${convoyId}`);
653
681
  }
654
- try {
655
- const result = await this.exec(
656
- "git",
657
- ["log", `--max-count=${limit}`, "--format=%H|%s|%ct", "--", "state.json"],
658
- agentPath
659
- );
660
- const snapshots = [];
661
- for (const line of result.stdout.split("\n").filter(Boolean)) {
662
- const [commitId, message, timestamp] = line.split("|");
663
- const stateResult = await this.exec(
664
- "git",
665
- ["show", `${commitId}:state.json`],
666
- agentPath
667
- ).catch(() => ({ stdout: "{}" }));
668
- let state;
669
- try {
670
- state = JSON.parse(stateResult.stdout);
671
- } catch {
672
- state = {};
673
- }
674
- snapshots.push({
675
- id: nanoid(),
676
- agentId,
677
- commitId,
678
- timestamp: Number.parseInt(timestamp, 10) * 1e3,
679
- message,
680
- state
681
- });
682
- }
683
- return snapshots;
684
- } catch {
685
- return [];
682
+ if (convoy.status !== "in_progress") {
683
+ throw new Error(`Cannot pause convoy in status: ${convoy.status}`);
686
684
  }
685
+ convoy.status = "paused";
686
+ await this.persistConvoy(convoy);
687
+ this.emit("convoy:paused", convoy);
687
688
  }
688
689
  /**
689
- * Cleanup agent worktree
690
+ * Resume convoy
690
691
  */
691
- async cleanup(agentId) {
692
+ async resume(convoyId) {
693
+ await this.ensureInitialized();
694
+ const convoy = this.convoys.get(convoyId);
695
+ if (!convoy) {
696
+ throw new Error(`Convoy not found: ${convoyId}`);
697
+ }
698
+ if (convoy.status !== "paused") {
699
+ throw new Error(`Cannot resume convoy in status: ${convoy.status}`);
700
+ }
701
+ convoy.status = "in_progress";
702
+ await this.persistConvoy(convoy);
703
+ this.emit("convoy:resumed", convoy);
704
+ }
705
+ /**
706
+ * Cancel convoy
707
+ */
708
+ async cancel(convoyId) {
692
709
  await this.ensureInitialized();
693
- const agentPath = this.worktrees.get(agentId);
694
- if (!agentPath) {
695
- return;
710
+ const convoy = this.convoys.get(convoyId);
711
+ if (!convoy) {
712
+ throw new Error(`Convoy not found: ${convoyId}`);
696
713
  }
697
- try {
698
- if (this.config.useWorktrees) {
699
- await this.exec("git", ["worktree", "remove", agentPath, "--force"], this.config.workspaceRoot);
714
+ convoy.status = "cancelled";
715
+ convoy.completedAt = (/* @__PURE__ */ new Date()).toISOString();
716
+ for (const task of convoy.tasks) {
717
+ if (task.status === "pending" || task.status === "in_progress") {
718
+ task.status = "cancelled";
719
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
720
+ convoy.cancelledTasks++;
700
721
  }
701
- this.worktrees.delete(agentId);
702
- this.emit("worktree:removed", agentId);
703
- this.log(`Cleaned up worktree for agent: ${agentId}`);
704
- } catch (error) {
705
- const err = error instanceof Error ? error : new Error(String(error));
706
- this.emit("error", err);
707
- throw err;
708
722
  }
723
+ this.updateProgress(convoy);
724
+ await this.persistConvoy(convoy);
725
+ this.emit("convoy:cancelled", convoy);
709
726
  }
710
727
  /**
711
- * Get all agent IDs
728
+ * Get convoy by ID
712
729
  */
713
- getAgentIds() {
714
- return Array.from(this.worktrees.keys());
730
+ get(convoyId) {
731
+ return this.convoys.get(convoyId);
715
732
  }
716
733
  /**
717
- * Check if agent exists
734
+ * List all convoys
718
735
  */
719
- hasAgent(agentId) {
720
- return this.worktrees.has(agentId);
736
+ list(filter) {
737
+ let convoys = Array.from(this.convoys.values());
738
+ if (filter) {
739
+ if (filter.status) {
740
+ convoys = convoys.filter((c) => c.status === filter.status);
741
+ }
742
+ if (filter.createdBy) {
743
+ convoys = convoys.filter((c) => c.createdBy === filter.createdBy);
744
+ }
745
+ if (filter.tags && filter.tags.length > 0) {
746
+ convoys = convoys.filter(
747
+ (c) => filter.tags.some((tag) => c.tags.includes(tag))
748
+ );
749
+ }
750
+ }
751
+ return convoys.sort(
752
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
753
+ );
721
754
  }
722
755
  /**
723
- * Get workspace root path
756
+ * Get active convoys
724
757
  */
725
- getWorkspaceRoot() {
726
- return this.config.workspaceRoot;
758
+ getActive() {
759
+ return this.list({ status: "in_progress" });
727
760
  }
728
761
  /**
729
- * Get agent path
762
+ * Get next available tasks (no unmet dependencies)
730
763
  */
731
- getAgentPath(agentId) {
732
- return this.worktrees.get(agentId);
764
+ getNextTasks(convoyId) {
765
+ const convoy = this.convoys.get(convoyId);
766
+ if (!convoy)
767
+ return [];
768
+ return convoy.tasks.filter((task) => {
769
+ if (task.status !== "pending")
770
+ return false;
771
+ return this.getUnmetDependencies(convoy, task).length === 0;
772
+ });
733
773
  }
734
774
  /**
735
- * Get state history for agent
775
+ * Get convoy summary
736
776
  */
737
- async getStateHistory(agentId, limit = 20) {
738
- await this.ensureInitialized();
739
- const agentPath = this.worktrees.get(agentId);
740
- if (!agentPath) {
741
- return { agentId, commits: [], totalCommits: 0 };
742
- }
743
- try {
744
- const result = await this.exec(
745
- "git",
746
- ["log", `--max-count=${limit}`, "--format=%H|%s|%ct", "--", "state.json"],
747
- agentPath
748
- );
749
- const commits = result.stdout.split("\n").filter(Boolean).map((line) => {
750
- const [id, message, timestamp] = line.split("|");
751
- return {
752
- id,
753
- message,
754
- timestamp: Number.parseInt(timestamp, 10) * 1e3
755
- };
756
- });
757
- const countResult = await this.exec(
758
- "git",
759
- ["rev-list", "--count", "HEAD", "--", "state.json"],
760
- agentPath
761
- ).catch(() => ({ stdout: "0" }));
762
- return {
763
- agentId,
764
- commits,
765
- totalCommits: Number.parseInt(countResult.stdout.trim(), 10) || commits.length
766
- };
767
- } catch {
768
- return { agentId, commits: [], totalCommits: 0 };
777
+ getSummary(convoyId) {
778
+ const convoy = this.convoys.get(convoyId);
779
+ if (!convoy)
780
+ return "Convoy not found";
781
+ const lines = [
782
+ `Convoy: ${convoy.name} (${convoy.id})`,
783
+ `Status: ${convoy.status}`,
784
+ `Progress: ${convoy.progress}%`,
785
+ `Tasks: ${convoy.completedTasks}/${convoy.totalTasks} completed`
786
+ ];
787
+ if (convoy.failedTasks > 0) {
788
+ lines.push(`Failed: ${convoy.failedTasks}`);
769
789
  }
770
- }
771
- /**
772
- * Create a named snapshot of current state
773
- */
774
- async createSnapshot(agentId, description) {
775
- await this.ensureInitialized();
776
- const agentPath = this.worktrees.get(agentId);
777
- if (!agentPath) {
778
- throw new Error(`Agent not found: ${agentId}`);
790
+ if (convoy.cancelledTasks > 0) {
791
+ lines.push(`Cancelled: ${convoy.cancelledTasks}`);
779
792
  }
780
- const commitId = await this.commit(agentId, `Snapshot: ${description}`);
781
- if (!commitId) {
782
- const result = await this.exec("git", ["rev-parse", "HEAD"], agentPath);
783
- const currentCommitId = result.stdout.trim();
784
- const state2 = await this.loadState(agentId);
785
- return {
786
- id: nanoid(),
787
- agentId,
788
- commitId: currentCommitId,
789
- timestamp: Date.now(),
790
- message: description,
791
- state: state2 ?? {}
792
- };
793
+ if (convoy.skippedTasks > 0) {
794
+ lines.push(`Skipped: ${convoy.skippedTasks}`);
793
795
  }
794
- const state = await this.loadState(agentId);
795
- return {
796
- id: nanoid(),
797
- agentId,
798
- commitId,
799
- timestamp: Date.now(),
800
- message: description,
801
- state: state ?? {}
802
- };
803
- }
804
- /**
805
- * Restore state from a snapshot
806
- */
807
- async restoreSnapshot(agentId, snapshotId) {
808
- await this.rollback(agentId, snapshotId);
796
+ return lines.join("\n");
809
797
  }
810
798
  /**
811
- * Sync with remote (if configured)
799
+ * Delete convoy
812
800
  */
813
- async syncWithRemote() {
801
+ async delete(convoyId) {
814
802
  await this.ensureInitialized();
815
- try {
816
- const remoteResult = await this.exec("git", ["remote"], this.config.workspaceRoot);
817
- if (!remoteResult.stdout.trim()) {
818
- this.log("No remote configured, skipping sync");
819
- return;
820
- }
821
- await this.exec("git", ["pull", "--rebase"], this.config.workspaceRoot);
822
- await this.exec("git", ["push"], this.config.workspaceRoot);
823
- this.log("Synced with remote");
824
- } catch (error) {
825
- this.log(`Sync failed: ${error}`);
826
- }
803
+ const convoy = this.convoys.get(convoyId);
804
+ if (!convoy)
805
+ return false;
806
+ this.convoys.delete(convoyId);
807
+ return true;
827
808
  }
828
809
  // ========================================================================
829
810
  // Private Methods
@@ -833,505 +814,558 @@ Thumbs.db
833
814
  await this.initialize();
834
815
  }
835
816
  }
836
- async getOrCreateAgentPath(agentId) {
837
- if (!this.worktrees.has(agentId)) {
838
- await this.createAgentWorktree(agentId);
839
- }
840
- return this.worktrees.get(agentId);
817
+ updateProgress(convoy) {
818
+ const processed = convoy.completedTasks + convoy.failedTasks + convoy.cancelledTasks + convoy.skippedTasks;
819
+ convoy.progress = convoy.totalTasks > 0 ? Math.round(processed / convoy.totalTasks * 100) : 0;
841
820
  }
842
- async loadExistingWorktrees() {
843
- const agentsDir = join(this.config.workspaceRoot, "agents");
844
- if (!existsSync(agentsDir)) {
845
- return;
846
- }
847
- try {
848
- const { readdirSync, statSync } = await import('node:fs');
849
- const entries = readdirSync(agentsDir);
850
- for (const entry of entries) {
851
- const entryPath = join(agentsDir, entry);
852
- if (statSync(entryPath).isDirectory()) {
853
- this.worktrees.set(entry, entryPath);
854
- this.log(`Loaded existing worktree: ${entry}`);
821
+ checkConvoyCompletion(convoy) {
822
+ const processed = convoy.completedTasks + convoy.failedTasks + convoy.cancelledTasks + convoy.skippedTasks;
823
+ if (processed === convoy.totalTasks) {
824
+ convoy.completedAt = (/* @__PURE__ */ new Date()).toISOString();
825
+ if (convoy.failedTasks === 0 && convoy.cancelledTasks === 0) {
826
+ convoy.status = "completed";
827
+ if (convoy.notifications.onComplete) {
828
+ this.emit("convoy:completed", convoy);
829
+ }
830
+ } else {
831
+ convoy.status = "failed";
832
+ if (convoy.notifications.onFailure) {
833
+ this.emit("convoy:failed", convoy);
855
834
  }
856
835
  }
857
- } catch {
858
836
  }
859
837
  }
860
- async batchCommit(agentId, message) {
861
- const existing = this.pendingCommits.get(agentId);
862
- if (existing) {
863
- clearTimeout(existing);
864
- }
865
- return new Promise((resolve) => {
866
- const timeout = setTimeout(async () => {
867
- this.pendingCommits.delete(agentId);
868
- const commitId = await this.commit(agentId, message);
869
- resolve(commitId);
870
- }, this.config.commitBatchInterval);
871
- this.pendingCommits.set(agentId, timeout);
838
+ getUnmetDependencies(convoy, task) {
839
+ return task.dependsOn.filter((depId) => {
840
+ const depTask = convoy.tasks.find((t) => t.id === depId);
841
+ return !depTask || depTask.status !== "completed";
872
842
  });
873
843
  }
874
- async commit(agentId, message) {
875
- const agentPath = this.worktrees.get(agentId);
876
- if (!agentPath) {
877
- return "";
878
- }
879
- try {
880
- await this.exec("git", ["add", "-A"], agentPath);
881
- const statusResult = await this.exec("git", ["status", "--porcelain"], agentPath);
882
- if (!statusResult.stdout.trim()) {
883
- return "";
884
- }
885
- await this.exec("git", ["commit", "-m", message], agentPath);
886
- const result = await this.exec("git", ["rev-parse", "HEAD"], agentPath);
887
- return result.stdout.trim();
888
- } catch {
889
- return "";
844
+ async skipDependentTasks(convoy, failedTaskId) {
845
+ for (const task of convoy.tasks) {
846
+ if (task.status === "pending" && task.dependsOn.includes(failedTaskId)) {
847
+ task.status = "skipped";
848
+ task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
849
+ task.error = `Skipped due to failed dependency: ${failedTaskId}`;
850
+ convoy.skippedTasks++;
851
+ this.emit("task:skipped", convoy, task);
852
+ await this.skipDependentTasks(convoy, task.id);
853
+ }
890
854
  }
891
855
  }
892
- async exec(cmd, args, cwd) {
893
- const result = await K(cmd, args, { nodeOptions: { cwd } });
894
- return {
895
- stdout: result.stdout,
896
- stderr: result.stderr
897
- };
898
- }
899
- log(message) {
900
- if (this.config.verbose) {
901
- console.log(`[GitBackedState] ${message}`);
856
+ async loadConvoys() {
857
+ for (const agentId of this.stateManager.getAgentIds()) {
858
+ if (agentId.startsWith("convoy-")) {
859
+ const state = await this.stateManager.loadState(agentId);
860
+ if (state) {
861
+ const convoy = state;
862
+ this.convoys.set(convoy.id, convoy);
863
+ }
864
+ }
902
865
  }
903
866
  }
867
+ async persistConvoy(convoy) {
868
+ await this.stateManager.createAgentWorktree(`convoy-${convoy.id}`);
869
+ await this.stateManager.saveState(`convoy-${convoy.id}`, convoy);
870
+ }
904
871
  }
905
- let globalStateManager = null;
906
- function getGlobalStateManager(config) {
907
- if (!globalStateManager) {
908
- globalStateManager = new GitBackedStateManager(config);
872
+ let globalConvoyManager = null;
873
+ function getGlobalConvoyManager() {
874
+ if (!globalConvoyManager) {
875
+ globalConvoyManager = new ConvoyManager();
909
876
  }
910
- return globalStateManager;
877
+ return globalConvoyManager;
911
878
  }
912
879
 
913
- const gitBackedState = {
880
+ const convoyManager = {
914
881
  __proto__: null,
915
- GitBackedStateManager: GitBackedStateManager,
916
- getGlobalStateManager: getGlobalStateManager
882
+ ConvoyManager: ConvoyManager,
883
+ getGlobalConvoyManager: getGlobalConvoyManager
917
884
  };
918
885
 
919
- class ConvoyManager extends EventEmitter {
920
- stateManager;
921
- convoys = /* @__PURE__ */ new Map();
922
- initialized = false;
923
- constructor(stateManager) {
924
- super();
925
- this.stateManager = stateManager ?? getGlobalStateManager();
926
- }
927
- /**
928
- * Initialize convoy manager
929
- */
930
- async initialize() {
931
- if (this.initialized)
932
- return;
933
- await this.stateManager.initialize();
934
- await this.loadConvoys();
935
- this.initialized = true;
886
+ const DEPTH_TOKEN_BUDGETS = {
887
+ L0: 100,
888
+ // Abstract summary only
889
+ L1: 2e3,
890
+ // Overview
891
+ L2: Number.POSITIVE_INFINITY
892
+ // Full content
893
+ };
894
+ const PHASE_BUDGET_MULTIPLIERS = {
895
+ exploring: 2,
896
+ executing: 0.6,
897
+ generating: 0.4,
898
+ reviewing: 1.2,
899
+ idle: 1
900
+ };
901
+ const CHARS_PER_TOKEN = 4;
902
+ function estimateTokens(text) {
903
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
904
+ }
905
+ function truncateToDepth(content, depth) {
906
+ if (depth === "L2")
907
+ return content;
908
+ const maxChars = DEPTH_TOKEN_BUDGETS[depth] * CHARS_PER_TOKEN;
909
+ if (content.length <= maxChars)
910
+ return content;
911
+ if (depth === "L0") {
912
+ const firstPara = content.split(/\n\n/)[0];
913
+ return firstPara.length <= maxChars ? firstPara : content.slice(0, maxChars);
936
914
  }
915
+ return `${content.slice(0, maxChars)}
916
+ ... (truncated, use L2 for full content)`;
917
+ }
918
+ class ContextLoader {
919
+ cache = /* @__PURE__ */ new Map();
920
+ defaultMaxSize = 1e5;
921
+ // 100KB default
922
+ defaultTokenBudget = 8e3;
923
+ // ~32KB at 4 chars/token
937
924
  /**
938
- * Create new convoy
925
+ * Resolve effective depth from options.
926
+ * If depth is explicit, use it. Otherwise derive from tokenBudget.
939
927
  */
940
- async create(name, options = {}) {
941
- await this.ensureInitialized();
942
- const convoy = {
943
- id: `cv-${nanoid(6)}`,
944
- name,
945
- description: options.description ?? "",
946
- tasks: [],
947
- status: "pending",
948
- progress: 0,
949
- totalTasks: 0,
950
- completedTasks: 0,
951
- failedTasks: 0,
952
- cancelledTasks: 0,
953
- skippedTasks: 0,
954
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
955
- createdBy: options.createdBy ?? "system",
956
- notifications: {
957
- onComplete: options.notifyOnComplete ?? false,
958
- onFailure: options.notifyOnFailure ?? true,
959
- onProgress: options.notifyOnProgress ?? false,
960
- notifyHuman: options.notifyHuman ?? false
961
- },
962
- metadata: options.metadata,
963
- tags: options.tags ?? []
964
- };
965
- this.convoys.set(convoy.id, convoy);
966
- await this.persistConvoy(convoy);
967
- this.emit("convoy:created", convoy);
968
- return convoy;
928
+ resolveDepth(tokenBudget, explicitDepth) {
929
+ if (explicitDepth)
930
+ return explicitDepth;
931
+ if (tokenBudget <= DEPTH_TOKEN_BUDGETS.L0)
932
+ return "L0";
933
+ if (tokenBudget <= DEPTH_TOKEN_BUDGETS.L1)
934
+ return "L1";
935
+ return "L2";
969
936
  }
970
937
  /**
971
- * Add task to convoy
938
+ * Load context hierarchically with OpenViking-style tiered depth control.
939
+ * Pass tokenBudget to automatically select L0/L1/L2 depth.
972
940
  */
973
- async addTask(convoyId, title, options = {}) {
974
- await this.ensureInitialized();
975
- const convoy = this.convoys.get(convoyId);
976
- if (!convoy) {
977
- throw new Error(`Convoy not found: ${convoyId}`);
941
+ async load(options = {}) {
942
+ const {
943
+ projectRoot = process.cwd(),
944
+ maxSize = this.defaultMaxSize,
945
+ tokenBudget,
946
+ depth: explicitDepth,
947
+ layers = ["project", "domain", "task", "execution"],
948
+ task
949
+ } = options;
950
+ const phaseMultiplier = options.taskPhase ? PHASE_BUDGET_MULTIPLIERS[options.taskPhase] ?? 1 : 1;
951
+ const effectiveBudget = Math.round((tokenBudget ?? this.defaultTokenBudget) * phaseMultiplier);
952
+ const depth = this.resolveDepth(effectiveBudget, explicitDepth);
953
+ const cacheKey = this.getCacheKey(projectRoot, layers, task?.id, depth);
954
+ const cached = this.cache.get(cacheKey);
955
+ if (cached && Date.now() - cached.loadedAt < 6e4) {
956
+ executionTracer.logEvent("context-load", {
957
+ source: "cache",
958
+ layers: layers.length,
959
+ size: cached.totalSize,
960
+ tokens: cached.totalTokens,
961
+ depth
962
+ });
963
+ return cached;
978
964
  }
979
- const task = {
980
- id: `task-${nanoid(6)}`,
981
- title,
982
- description: options.description ?? "",
983
- status: "pending",
984
- assignedTo: options.assignedTo,
985
- dependsOn: options.dependsOn ?? [],
986
- metadata: options.metadata
965
+ const context = {
966
+ layers: /* @__PURE__ */ new Map(),
967
+ totalSize: 0,
968
+ totalTokens: 0,
969
+ loadedAt: Date.now(),
970
+ depth
987
971
  };
988
- convoy.tasks.push(task);
989
- convoy.totalTasks = convoy.tasks.length;
990
- this.updateProgress(convoy);
991
- await this.persistConvoy(convoy);
992
- return task;
993
- }
994
- /**
995
- * Add multiple tasks to convoy
996
- */
997
- async addTasks(convoyId, tasks) {
998
- const addedTasks = [];
999
- for (const { title, options } of tasks) {
1000
- const task = await this.addTask(convoyId, title, options);
1001
- addedTasks.push(task);
1002
- }
1003
- return addedTasks;
1004
- }
1005
- /**
1006
- * Start convoy execution
1007
- */
1008
- async start(convoyId) {
1009
- await this.ensureInitialized();
1010
- const convoy = this.convoys.get(convoyId);
1011
- if (!convoy) {
1012
- throw new Error(`Convoy not found: ${convoyId}`);
1013
- }
1014
- if (convoy.status !== "pending" && convoy.status !== "paused") {
1015
- throw new Error(`Cannot start convoy in status: ${convoy.status}`);
972
+ for (const layer of layers) {
973
+ const entries = await this.loadLayer(layer, projectRoot, task, depth);
974
+ if (entries.length > 0) {
975
+ context.layers.set(layer, entries);
976
+ context.totalSize += entries.reduce((sum, e) => sum + e.content.length, 0);
977
+ context.totalTokens += entries.reduce((sum, e) => sum + e.tokenEstimate, 0);
978
+ }
979
+ if (context.totalSize > maxSize || context.totalTokens > effectiveBudget) {
980
+ break;
981
+ }
1016
982
  }
1017
- convoy.status = "in_progress";
1018
- convoy.startedAt = convoy.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
1019
- await this.persistConvoy(convoy);
1020
- this.emit("convoy:started", convoy);
983
+ this.cache.set(cacheKey, context);
984
+ executionTracer.logEvent("context-load", {
985
+ source: "fresh",
986
+ layers: context.layers.size,
987
+ size: context.totalSize,
988
+ tokens: context.totalTokens,
989
+ depth
990
+ });
991
+ return context;
1021
992
  }
1022
993
  /**
1023
- * Start a task
994
+ * Load a specific context layer
1024
995
  */
1025
- async startTask(convoyId, taskId, assignedTo) {
1026
- await this.ensureInitialized();
1027
- const convoy = this.convoys.get(convoyId);
1028
- if (!convoy) {
1029
- throw new Error(`Convoy not found: ${convoyId}`);
1030
- }
1031
- const task = convoy.tasks.find((t) => t.id === taskId);
1032
- if (!task) {
1033
- throw new Error(`Task not found: ${taskId}`);
1034
- }
1035
- const unmetDeps = this.getUnmetDependencies(convoy, task);
1036
- if (unmetDeps.length > 0) {
1037
- throw new Error(`Task has unmet dependencies: ${unmetDeps.join(", ")}`);
1038
- }
1039
- task.status = "in_progress";
1040
- task.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1041
- if (assignedTo) {
1042
- task.assignedTo = assignedTo;
1043
- }
1044
- if (convoy.status === "pending") {
1045
- convoy.status = "in_progress";
1046
- convoy.startedAt = (/* @__PURE__ */ new Date()).toISOString();
996
+ async loadLayer(layer, projectRoot, task, depth = "L2") {
997
+ switch (layer) {
998
+ case "project":
999
+ return this.loadProjectContext(projectRoot, depth);
1000
+ case "domain":
1001
+ return this.loadDomainContext(projectRoot, task, depth);
1002
+ case "task":
1003
+ return this.loadTaskContext(task, depth);
1004
+ case "execution":
1005
+ return this.loadExecutionContext(task, depth);
1006
+ default:
1007
+ return [];
1047
1008
  }
1048
- await this.persistConvoy(convoy);
1049
- this.emit("task:started", convoy, task);
1050
1009
  }
1051
1010
  /**
1052
- * Complete a task
1011
+ * Load project-level context
1012
+ * - README.md
1013
+ * - CLAUDE.md
1014
+ * - package.json
1015
+ * - tsconfig.json
1053
1016
  */
1054
- async completeTask(convoyId, taskId, result) {
1055
- await this.ensureInitialized();
1056
- const convoy = this.convoys.get(convoyId);
1057
- if (!convoy) {
1058
- throw new Error(`Convoy not found: ${convoyId}`);
1059
- }
1060
- const task = convoy.tasks.find((t) => t.id === taskId);
1061
- if (!task) {
1062
- throw new Error(`Task not found: ${taskId}`);
1063
- }
1064
- task.status = "completed";
1065
- task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1066
- task.result = result;
1067
- convoy.completedTasks++;
1068
- this.updateProgress(convoy);
1069
- this.checkConvoyCompletion(convoy);
1070
- await this.persistConvoy(convoy);
1071
- this.emit("task:completed", convoy, task);
1072
- if (convoy.notifications.onProgress) {
1073
- this.emit("convoy:progress", convoy, convoy.progress);
1017
+ async loadProjectContext(projectRoot, depth = "L2") {
1018
+ const entries = [];
1019
+ const files = ["README.md", "CLAUDE.md", "package.json", "tsconfig.json"];
1020
+ for (const file of files) {
1021
+ const path = join(projectRoot, file);
1022
+ if (existsSync(path)) {
1023
+ const raw = readFileSync(path, "utf-8");
1024
+ const content = truncateToDepth(raw, depth);
1025
+ entries.push({
1026
+ layer: "project",
1027
+ source: file,
1028
+ content,
1029
+ priority: 100,
1030
+ depth,
1031
+ tokenEstimate: estimateTokens(content),
1032
+ metadata: { path }
1033
+ });
1034
+ }
1074
1035
  }
1036
+ return entries;
1075
1037
  }
1076
1038
  /**
1077
- * Fail a task
1039
+ * Load domain-specific context
1040
+ * Based on task type/domain, load relevant files
1078
1041
  */
1079
- async failTask(convoyId, taskId, error) {
1080
- await this.ensureInitialized();
1081
- const convoy = this.convoys.get(convoyId);
1082
- if (!convoy) {
1083
- throw new Error(`Convoy not found: ${convoyId}`);
1084
- }
1085
- const task = convoy.tasks.find((t) => t.id === taskId);
1086
- if (!task) {
1087
- throw new Error(`Task not found: ${taskId}`);
1042
+ async loadDomainContext(projectRoot, task, depth = "L2") {
1043
+ if (!task)
1044
+ return [];
1045
+ const entries = [];
1046
+ const structure = await fsParadigm.detect(projectRoot);
1047
+ const domain = this.inferDomain(task);
1048
+ if (!domain) {
1049
+ const sourceFiles = fsParadigm.getFilesByRole(structure, "source");
1050
+ for (const file of sourceFiles.slice(0, 3)) {
1051
+ try {
1052
+ const raw = readFileSync(file, "utf-8");
1053
+ const content = truncateToDepth(raw, depth);
1054
+ entries.push({
1055
+ layer: "domain",
1056
+ source: file.replace(projectRoot, ""),
1057
+ content,
1058
+ priority: 75,
1059
+ depth,
1060
+ tokenEstimate: estimateTokens(content),
1061
+ metadata: { paradigm: structure.paradigm, path: file }
1062
+ });
1063
+ } catch {
1064
+ }
1065
+ }
1066
+ return entries;
1088
1067
  }
1089
- task.status = "failed";
1090
- task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1091
- task.error = error;
1092
- convoy.failedTasks++;
1093
- this.updateProgress(convoy);
1094
- await this.skipDependentTasks(convoy, taskId);
1095
- this.checkConvoyCompletion(convoy);
1096
- await this.persistConvoy(convoy);
1097
- this.emit("task:failed", convoy, task);
1098
- if (convoy.notifications.onFailure) {
1099
- this.emit("convoy:failed", convoy);
1068
+ const patterns = this.getDomainPatterns(domain, structure.paradigm);
1069
+ for (const pattern of patterns) {
1070
+ const files = await glob([pattern], {
1071
+ cwd: projectRoot,
1072
+ absolute: true,
1073
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"]
1074
+ });
1075
+ for (const file of files.slice(0, 5)) {
1076
+ try {
1077
+ const raw = readFileSync(file, "utf-8");
1078
+ const content = truncateToDepth(raw, depth);
1079
+ entries.push({
1080
+ layer: "domain",
1081
+ source: file.replace(projectRoot, ""),
1082
+ content,
1083
+ priority: 80,
1084
+ depth,
1085
+ tokenEstimate: estimateTokens(content),
1086
+ metadata: { domain, paradigm: structure.paradigm, path: file }
1087
+ });
1088
+ } catch {
1089
+ }
1090
+ }
1100
1091
  }
1092
+ return entries;
1101
1093
  }
1102
1094
  /**
1103
- * Cancel a task
1095
+ * Load task-specific context
1104
1096
  */
1105
- async cancelTask(convoyId, taskId) {
1106
- await this.ensureInitialized();
1107
- const convoy = this.convoys.get(convoyId);
1108
- if (!convoy) {
1109
- throw new Error(`Convoy not found: ${convoyId}`);
1097
+ async loadTaskContext(task, depth = "L2") {
1098
+ if (!task)
1099
+ return [];
1100
+ const entries = [];
1101
+ if (task.description) {
1102
+ const content = truncateToDepth(task.description, depth);
1103
+ entries.push({
1104
+ layer: "task",
1105
+ source: "task-description",
1106
+ content,
1107
+ priority: 90,
1108
+ depth,
1109
+ tokenEstimate: estimateTokens(content),
1110
+ metadata: { taskId: task.id }
1111
+ });
1110
1112
  }
1111
- const task = convoy.tasks.find((t) => t.id === taskId);
1112
- if (!task) {
1113
- throw new Error(`Task not found: ${taskId}`);
1113
+ if (task.input && depth !== "L0") {
1114
+ const content = truncateToDepth(JSON.stringify(task.input, null, 2), depth);
1115
+ entries.push({
1116
+ layer: "task",
1117
+ source: "task-input",
1118
+ content,
1119
+ priority: 85,
1120
+ depth,
1121
+ tokenEstimate: estimateTokens(content),
1122
+ metadata: { taskId: task.id }
1123
+ });
1114
1124
  }
1115
- task.status = "cancelled";
1116
- task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1117
- convoy.cancelledTasks++;
1118
- this.updateProgress(convoy);
1119
- await this.skipDependentTasks(convoy, taskId);
1120
- this.checkConvoyCompletion(convoy);
1121
- await this.persistConvoy(convoy);
1122
- this.emit("task:cancelled", convoy, task);
1125
+ return entries;
1123
1126
  }
1124
1127
  /**
1125
- * Pause convoy
1128
+ * Load execution-specific context
1129
+ * - Recent errors
1130
+ * - Previous attempts
1131
+ * - Related task outputs
1126
1132
  */
1127
- async pause(convoyId) {
1128
- await this.ensureInitialized();
1129
- const convoy = this.convoys.get(convoyId);
1130
- if (!convoy) {
1131
- throw new Error(`Convoy not found: ${convoyId}`);
1132
- }
1133
- if (convoy.status !== "in_progress") {
1134
- throw new Error(`Cannot pause convoy in status: ${convoy.status}`);
1133
+ async loadExecutionContext(task, depth = "L2") {
1134
+ if (!task)
1135
+ return [];
1136
+ const entries = [];
1137
+ if (task.output && depth !== "L0") {
1138
+ const content = truncateToDepth(JSON.stringify(task.output, null, 2), depth);
1139
+ entries.push({
1140
+ layer: "execution",
1141
+ source: "previous-output",
1142
+ content,
1143
+ priority: 70,
1144
+ depth,
1145
+ tokenEstimate: estimateTokens(content),
1146
+ metadata: { taskId: task.id }
1147
+ });
1135
1148
  }
1136
- convoy.status = "paused";
1137
- await this.persistConvoy(convoy);
1138
- this.emit("convoy:paused", convoy);
1149
+ return entries;
1139
1150
  }
1140
1151
  /**
1141
- * Resume convoy
1152
+ * Infer domain from task
1142
1153
  */
1143
- async resume(convoyId) {
1144
- await this.ensureInitialized();
1145
- const convoy = this.convoys.get(convoyId);
1146
- if (!convoy) {
1147
- throw new Error(`Convoy not found: ${convoyId}`);
1148
- }
1149
- if (convoy.status !== "paused") {
1150
- throw new Error(`Cannot resume convoy in status: ${convoy.status}`);
1154
+ inferDomain(task) {
1155
+ const text = `${task.name} ${task.description || ""}`.toLowerCase();
1156
+ const domains = {
1157
+ api: ["api", "endpoint", "route", "controller"],
1158
+ ui: ["ui", "component", "view", "page", "frontend"],
1159
+ database: ["database", "db", "schema", "migration", "model"],
1160
+ auth: ["auth", "login", "user", "permission", "session"],
1161
+ test: ["test", "spec", "e2e", "unit", "integration"],
1162
+ config: ["config", "settings", "env", "setup"]
1163
+ };
1164
+ for (const [domain, keywords] of Object.entries(domains)) {
1165
+ if (keywords.some((kw) => text.includes(kw))) {
1166
+ return domain;
1167
+ }
1151
1168
  }
1152
- convoy.status = "in_progress";
1153
- await this.persistConvoy(convoy);
1154
- this.emit("convoy:resumed", convoy);
1169
+ return null;
1155
1170
  }
1156
1171
  /**
1157
- * Cancel convoy
1172
+ * Get file patterns for a domain (paradigm-aware)
1158
1173
  */
1159
- async cancel(convoyId) {
1160
- await this.ensureInitialized();
1161
- const convoy = this.convoys.get(convoyId);
1162
- if (!convoy) {
1163
- throw new Error(`Convoy not found: ${convoyId}`);
1164
- }
1165
- convoy.status = "cancelled";
1166
- convoy.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1167
- for (const task of convoy.tasks) {
1168
- if (task.status === "pending" || task.status === "in_progress") {
1169
- task.status = "cancelled";
1170
- task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1171
- convoy.cancelledTasks++;
1174
+ getDomainPatterns(domain, paradigm) {
1175
+ const basePatterns = {
1176
+ api: ["**/api/**/*.ts", "**/routes/**/*.ts", "**/controllers/**/*.ts"],
1177
+ ui: ["**/components/**/*.{tsx,vue}", "**/pages/**/*.{tsx,vue}", "**/views/**/*.{tsx,vue}"],
1178
+ database: ["**/models/**/*.ts", "**/schema/**/*.ts", "**/migrations/**/*.ts"],
1179
+ auth: ["**/auth/**/*.ts", "**/middleware/auth*.ts"],
1180
+ test: ["**/*.test.ts", "**/*.spec.ts"],
1181
+ config: ["**/config/**/*.ts", "**/*.config.ts"]
1182
+ };
1183
+ let patterns = basePatterns[domain] || [];
1184
+ if (paradigm === "monorepo") {
1185
+ patterns = patterns.map((p) => `packages/*/${p}`);
1186
+ } else if (paradigm === "fullstack") {
1187
+ if (domain === "ui") {
1188
+ patterns = patterns.map((p) => `client/${p}`);
1189
+ } else if (domain === "api") {
1190
+ patterns = patterns.map((p) => `server/${p}`);
1172
1191
  }
1173
1192
  }
1174
- this.updateProgress(convoy);
1175
- await this.persistConvoy(convoy);
1176
- this.emit("convoy:cancelled", convoy);
1193
+ return patterns;
1177
1194
  }
1178
1195
  /**
1179
- * Get convoy by ID
1196
+ * Get cache key
1180
1197
  */
1181
- get(convoyId) {
1182
- return this.convoys.get(convoyId);
1198
+ getCacheKey(projectRoot, layers, taskId, depth = "L2") {
1199
+ return `${projectRoot}:${layers.join(",")}:${taskId || "none"}:${depth}`;
1183
1200
  }
1184
1201
  /**
1185
- * List all convoys
1202
+ * Clear cache
1186
1203
  */
1187
- list(filter) {
1188
- let convoys = Array.from(this.convoys.values());
1189
- if (filter) {
1190
- if (filter.status) {
1191
- convoys = convoys.filter((c) => c.status === filter.status);
1192
- }
1193
- if (filter.createdBy) {
1194
- convoys = convoys.filter((c) => c.createdBy === filter.createdBy);
1195
- }
1196
- if (filter.tags && filter.tags.length > 0) {
1197
- convoys = convoys.filter(
1198
- (c) => filter.tags.some((tag) => c.tags.includes(tag))
1199
- );
1200
- }
1201
- }
1202
- return convoys.sort(
1203
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
1204
- );
1204
+ clearCache() {
1205
+ this.cache.clear();
1205
1206
  }
1206
1207
  /**
1207
- * Get active convoys
1208
+ * Format context for LLM consumption
1208
1209
  */
1209
- getActive() {
1210
- return this.list({ status: "in_progress" });
1210
+ formatForLLM(context) {
1211
+ const sections = [];
1212
+ sections.push(`<!-- context depth=${context.depth} tokens\u2248${context.totalTokens} -->
1213
+ `);
1214
+ const layerOrder = ["project", "domain", "task", "execution"];
1215
+ for (const layer of layerOrder) {
1216
+ const entries = context.layers.get(layer);
1217
+ if (!entries || entries.length === 0)
1218
+ continue;
1219
+ sections.push(`# ${layer.toUpperCase()} CONTEXT
1220
+ `);
1221
+ const sorted = entries.sort((a, b) => b.priority - a.priority);
1222
+ for (const entry of sorted) {
1223
+ sections.push(`## ${entry.source} [${entry.depth}]
1224
+ `);
1225
+ sections.push(entry.content);
1226
+ sections.push("\n");
1227
+ }
1228
+ }
1229
+ return sections.join("\n");
1211
1230
  }
1212
- /**
1213
- * Get next available tasks (no unmet dependencies)
1214
- */
1215
- getNextTasks(convoyId) {
1216
- const convoy = this.convoys.get(convoyId);
1217
- if (!convoy)
1218
- return [];
1219
- return convoy.tasks.filter((task) => {
1220
- if (task.status !== "pending")
1221
- return false;
1222
- return this.getUnmetDependencies(convoy, task).length === 0;
1223
- });
1231
+ }
1232
+ const contextLoader = new ContextLoader();
1233
+ async function loadContextAtDepth(depth, options = {}) {
1234
+ return contextLoader.load({ ...options, depth });
1235
+ }
1236
+
1237
+ const contextLoader$1 = {
1238
+ __proto__: null,
1239
+ ContextLoader: ContextLoader,
1240
+ contextLoader: contextLoader,
1241
+ loadContextAtDepth: loadContextAtDepth
1242
+ };
1243
+
1244
+ function detectTaskPhase(history) {
1245
+ if (history.length === 0)
1246
+ return "idle";
1247
+ const recent = history.slice(-10);
1248
+ const recentText = recent.map((h) => h.content).join(" ").toLowerCase();
1249
+ if (recentText.includes("write") || recentText.includes("create") || recentText.includes("generating"))
1250
+ return "generating";
1251
+ if (recentText.includes("test") || recentText.includes("review") || recentText.includes("check"))
1252
+ return "reviewing";
1253
+ if (recentText.includes("edit") || recentText.includes("bash") || recentText.includes("run"))
1254
+ return "executing";
1255
+ if (recentText.includes("read") || recentText.includes("search") || recentText.includes("grep"))
1256
+ return "exploring";
1257
+ return "idle";
1258
+ }
1259
+ function extractFilePaths(content) {
1260
+ const matches = content.match(/\/[\w./\-]+\.\w+|[\w./\-]+\.(?:ts|js|json|md|py|go|rs)/g);
1261
+ return matches ? [...new Set(matches)] : [];
1262
+ }
1263
+ class SessionIntelligence {
1264
+ static instance = null;
1265
+ session;
1266
+ constructor(session) {
1267
+ this.session = session;
1268
+ if (!session.intelligence) {
1269
+ session.intelligence = {
1270
+ taskPhase: "idle",
1271
+ filesTouched: [],
1272
+ keyDecisions: [],
1273
+ toolCallCount: 0,
1274
+ compressionWatermark: 0
1275
+ };
1276
+ }
1277
+ }
1278
+ get state() {
1279
+ return this.session.intelligence;
1224
1280
  }
1225
1281
  /**
1226
- * Get convoy summary
1282
+ * Observe a new history entry and update intelligence state.
1283
+ * Call this after every message added to the session.
1227
1284
  */
1228
- getSummary(convoyId) {
1229
- const convoy = this.convoys.get(convoyId);
1230
- if (!convoy)
1231
- return "Convoy not found";
1232
- const lines = [
1233
- `Convoy: ${convoy.name} (${convoy.id})`,
1234
- `Status: ${convoy.status}`,
1235
- `Progress: ${convoy.progress}%`,
1236
- `Tasks: ${convoy.completedTasks}/${convoy.totalTasks} completed`
1237
- ];
1238
- if (convoy.failedTasks > 0) {
1239
- lines.push(`Failed: ${convoy.failedTasks}`);
1285
+ observe(entry) {
1286
+ const intel = this.state;
1287
+ if (!intel.userOriginalIntent && entry.role === "user" && entry.content.trim().length > 10) {
1288
+ intel.userOriginalIntent = entry.content.slice(0, 500);
1240
1289
  }
1241
- if (convoy.cancelledTasks > 0) {
1242
- lines.push(`Cancelled: ${convoy.cancelledTasks}`);
1290
+ const files = extractFilePaths(entry.content);
1291
+ for (const f of files) {
1292
+ if (!intel.filesTouched.includes(f)) {
1293
+ intel.filesTouched.push(f);
1294
+ }
1243
1295
  }
1244
- if (convoy.skippedTasks > 0) {
1245
- lines.push(`Skipped: ${convoy.skippedTasks}`);
1296
+ if (entry.role === "assistant" && /\b(bash|read|write|edit|grep|glob)\b/i.test(entry.content)) {
1297
+ intel.toolCallCount++;
1246
1298
  }
1247
- return lines.join("\n");
1299
+ intel.taskPhase = detectTaskPhase(this.session.history);
1248
1300
  }
1249
1301
  /**
1250
- * Delete convoy
1302
+ * Record a key decision explicitly.
1251
1303
  */
1252
- async delete(convoyId) {
1253
- await this.ensureInitialized();
1254
- const convoy = this.convoys.get(convoyId);
1255
- if (!convoy)
1256
- return false;
1257
- this.convoys.delete(convoyId);
1258
- return true;
1259
- }
1260
- // ========================================================================
1261
- // Private Methods
1262
- // ========================================================================
1263
- async ensureInitialized() {
1264
- if (!this.initialized) {
1265
- await this.initialize();
1266
- }
1267
- }
1268
- updateProgress(convoy) {
1269
- const processed = convoy.completedTasks + convoy.failedTasks + convoy.cancelledTasks + convoy.skippedTasks;
1270
- convoy.progress = convoy.totalTasks > 0 ? Math.round(processed / convoy.totalTasks * 100) : 0;
1271
- }
1272
- checkConvoyCompletion(convoy) {
1273
- const processed = convoy.completedTasks + convoy.failedTasks + convoy.cancelledTasks + convoy.skippedTasks;
1274
- if (processed === convoy.totalTasks) {
1275
- convoy.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1276
- if (convoy.failedTasks === 0 && convoy.cancelledTasks === 0) {
1277
- convoy.status = "completed";
1278
- if (convoy.notifications.onComplete) {
1279
- this.emit("convoy:completed", convoy);
1280
- }
1281
- } else {
1282
- convoy.status = "failed";
1283
- if (convoy.notifications.onFailure) {
1284
- this.emit("convoy:failed", convoy);
1285
- }
1304
+ recordDecision(decision) {
1305
+ const intel = this.state;
1306
+ if (!intel.keyDecisions.includes(decision)) {
1307
+ intel.keyDecisions.push(decision.slice(0, 200));
1308
+ if (intel.keyDecisions.length > 20) {
1309
+ intel.keyDecisions.shift();
1286
1310
  }
1287
1311
  }
1288
1312
  }
1289
- getUnmetDependencies(convoy, task) {
1290
- return task.dependsOn.filter((depId) => {
1291
- const depTask = convoy.tasks.find((t) => t.id === depId);
1292
- return !depTask || depTask.status !== "completed";
1293
- });
1313
+ /**
1314
+ * Mark that history up to index N has been compressed.
1315
+ */
1316
+ markCompressed(upToIndex) {
1317
+ this.state.compressionWatermark = upToIndex;
1294
1318
  }
1295
- async skipDependentTasks(convoy, failedTaskId) {
1296
- for (const task of convoy.tasks) {
1297
- if (task.status === "pending" && task.dependsOn.includes(failedTaskId)) {
1298
- task.status = "skipped";
1299
- task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1300
- task.error = `Skipped due to failed dependency: ${failedTaskId}`;
1301
- convoy.skippedTasks++;
1302
- this.emit("task:skipped", convoy, task);
1303
- await this.skipDependentTasks(convoy, task.id);
1304
- }
1319
+ /**
1320
+ * Generate a system prompt injection that preserves intent across compaction.
1321
+ * Inject this whenever context is about to be compressed.
1322
+ */
1323
+ getSystemPromptInjection() {
1324
+ const intel = this.state;
1325
+ const parts = ["<!-- session-intelligence -->"];
1326
+ if (intel.userOriginalIntent) {
1327
+ parts.push(`Original user intent: ${intel.userOriginalIntent}`);
1328
+ }
1329
+ if (intel.keyDecisions.length > 0) {
1330
+ parts.push(`Key decisions: ${intel.keyDecisions.join("; ")}`);
1331
+ }
1332
+ if (intel.filesTouched.length > 0) {
1333
+ parts.push(`Files touched: ${intel.filesTouched.slice(-20).join(", ")}`);
1305
1334
  }
1335
+ parts.push(`Task phase: ${intel.taskPhase} | Tool calls: ${intel.toolCallCount}`);
1336
+ parts.push("<!-- /session-intelligence -->");
1337
+ return parts.join("\n");
1306
1338
  }
1307
- async loadConvoys() {
1308
- for (const agentId of this.stateManager.getAgentIds()) {
1309
- if (agentId.startsWith("convoy-")) {
1310
- const state = await this.stateManager.loadState(agentId);
1311
- if (state) {
1312
- const convoy = state;
1313
- this.convoys.set(convoy.id, convoy);
1314
- }
1315
- }
1339
+ /**
1340
+ * Returns true if the session has enough context to be worth preserving.
1341
+ */
1342
+ hasSignificantContext() {
1343
+ const intel = this.state;
1344
+ return !!intel.userOriginalIntent || intel.toolCallCount > 3 || intel.filesTouched.length > 0;
1345
+ }
1346
+ static getInstance() {
1347
+ if (!SessionIntelligence.instance) {
1348
+ SessionIntelligence.instance = new SessionIntelligence({
1349
+ id: "session-intelligence-singleton",
1350
+ createdAt: /* @__PURE__ */ new Date(),
1351
+ lastUsedAt: /* @__PURE__ */ new Date(),
1352
+ history: []
1353
+ });
1316
1354
  }
1355
+ return SessionIntelligence.instance;
1317
1356
  }
1318
- async persistConvoy(convoy) {
1319
- await this.stateManager.createAgentWorktree(`convoy-${convoy.id}`);
1320
- await this.stateManager.saveState(`convoy-${convoy.id}`, convoy);
1357
+ recordMessage(role, content) {
1358
+ const entry = {
1359
+ timestamp: /* @__PURE__ */ new Date(),
1360
+ role,
1361
+ content
1362
+ };
1363
+ this.session.history.push(entry);
1364
+ this.observe(entry);
1321
1365
  }
1322
- }
1323
- let globalConvoyManager = null;
1324
- function getGlobalConvoyManager() {
1325
- if (!globalConvoyManager) {
1326
- globalConvoyManager = new ConvoyManager();
1366
+ updatePhase(phase) {
1367
+ this.state.taskPhase = phase;
1327
1368
  }
1328
- return globalConvoyManager;
1329
1369
  }
1330
1370
 
1331
- const convoyManager = {
1332
- __proto__: null,
1333
- ConvoyManager: ConvoyManager,
1334
- getGlobalConvoyManager: getGlobalConvoyManager
1335
- };
1336
-
1337
- export { SessionIntelligence as S, getGlobalStateManager as a, contextLoader$1 as b, contextLoader as c, gitBackedState as d, convoyManager as e, getGlobalConvoyManager as g };
1371
+ export { SessionIntelligence as S, getGlobalStateManager as a, gitBackedState as b, contextLoader as c, convoyManager as d, contextLoader$1 as e, getGlobalConvoyManager as g };