bosun 0.42.0 → 0.42.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/README.md +2 -0
- package/agent/agent-pool.mjs +34 -1
- package/agent/agent-work-report.mjs +89 -3
- package/agent/analyze-agent-work-helpers.mjs +14 -0
- package/agent/analyze-agent-work.mjs +23 -3
- package/agent/primary-agent.mjs +23 -1
- package/bosun-tui.mjs +4 -3
- package/bosun.schema.json +1 -1
- package/config/config.mjs +58 -0
- package/config/workspace-health.mjs +36 -6
- package/git/diff-stats.mjs +550 -124
- package/github/github-app-auth.mjs +9 -5
- package/infra/maintenance.mjs +13 -6
- package/infra/monitor.mjs +398 -10
- package/infra/runtime-accumulator.mjs +9 -1
- package/infra/session-tracker.mjs +163 -1
- package/infra/tui-bridge.mjs +415 -0
- package/infra/worktree-recovery-state.mjs +159 -0
- package/kanban/kanban-adapter.mjs +41 -8
- package/lib/repo-map.mjs +411 -0
- package/package.json +140 -137
- package/server/ui-server.mjs +953 -59
- package/shell/codex-config.mjs +34 -8
- package/task/task-cli.mjs +93 -19
- package/task/task-executor.mjs +397 -8
- package/task/task-store.mjs +194 -1
- package/telegram/telegram-bot.mjs +267 -18
- package/tools/vitest-runner.mjs +108 -0
- package/tui/app.mjs +252 -148
- package/tui/components/status-header.mjs +88 -131
- package/tui/lib/ws-bridge.mjs +125 -35
- package/tui/screens/agents-screen-helpers.mjs +219 -0
- package/tui/screens/agents.mjs +287 -270
- package/tui/screens/status.mjs +51 -189
- package/tui/screens/tasks.mjs +41 -253
- package/ui/app.js +52 -23
- package/ui/components/chat-view.js +263 -84
- package/ui/components/diff-viewer.js +324 -140
- package/ui/components/kanban-board.js +13 -9
- package/ui/components/session-list.js +111 -41
- package/ui/demo-defaults.js +481 -59
- package/ui/demo.html +32 -0
- package/ui/modules/session-api.js +320 -5
- package/ui/modules/stream-timeline.js +356 -0
- package/ui/modules/telegram.js +5 -2
- package/ui/modules/worktree-recovery.js +85 -0
- package/ui/styles.css +44 -0
- package/ui/tabs/chat.js +19 -4
- package/ui/tabs/dashboard.js +22 -0
- package/ui/tabs/infra.js +25 -0
- package/ui/tabs/tasks.js +119 -11
- package/voice/voice-auth-manager.mjs +10 -5
- package/workflow/workflow-engine.mjs +179 -1
- package/workflow/workflow-nodes.mjs +872 -16
- package/workflow/workflow-templates.mjs +4 -0
- package/workflow-templates/github.mjs +2 -1
- package/workflow-templates/planning.mjs +2 -1
- package/workflow-templates/sub-workflows.mjs +10 -0
- package/workflow-templates/task-batch.mjs +9 -8
- package/workflow-templates/task-execution.mjs +30 -12
- package/workflow-templates/task-lifecycle.mjs +59 -4
- package/workspace/shared-knowledge.mjs +409 -155
package/lib/repo-map.mjs
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { extname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_FILE_LIMIT = 12;
|
|
5
|
+
const DEFAULT_MAX_SYMBOLS = 4;
|
|
6
|
+
const QUERY_STOP_WORDS = new Set([
|
|
7
|
+
"about", "after", "again", "agent", "along", "also", "architect", "before", "being", "bosun",
|
|
8
|
+
"build", "changes", "check", "code", "create", "debug", "editor", "ensure", "feature", "files",
|
|
9
|
+
"from", "have", "implement", "implementation", "into", "large", "make", "mode", "plan", "phase",
|
|
10
|
+
"repo", "task", "tests", "that", "them", "then", "this", "update", "validate", "validation",
|
|
11
|
+
"with", "workflow", "worktree", "your",
|
|
12
|
+
]);
|
|
13
|
+
const IMPORTANT_PATHS = Object.freeze([
|
|
14
|
+
"package.json",
|
|
15
|
+
"AGENTS.md",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.mdx",
|
|
18
|
+
"README.txt",
|
|
19
|
+
"cli.mjs",
|
|
20
|
+
"setup.mjs",
|
|
21
|
+
"config/config.mjs",
|
|
22
|
+
"infra/monitor.mjs",
|
|
23
|
+
"workflow/workflow-nodes.mjs",
|
|
24
|
+
"workflow/workflow-engine.mjs",
|
|
25
|
+
"agent/primary-agent.mjs",
|
|
26
|
+
]);
|
|
27
|
+
const SOURCE_EXTENSIONS = new Set([".mjs", ".js", ".cjs", ".ts", ".tsx", ".jsx", ".json", ".md", ".yml", ".yaml"]);
|
|
28
|
+
|
|
29
|
+
function toPositiveInt(value, fallback) {
|
|
30
|
+
const numeric = Number(value);
|
|
31
|
+
return Number.isInteger(numeric) && numeric > 0 ? numeric : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function uniqueStrings(values) {
|
|
35
|
+
return [...new Set((Array.isArray(values) ? values : []).map((value) => String(value || "").trim()).filter(Boolean))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isTokenWordChar(char) {
|
|
39
|
+
if (!char) return false;
|
|
40
|
+
const code = char.charCodeAt(0);
|
|
41
|
+
return (code >= 48 && code <= 57)
|
|
42
|
+
|| code === 95
|
|
43
|
+
|| (code >= 97 && code <= 122)
|
|
44
|
+
|| (code >= 65 && code <= 90);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function trimTokenBoundaryPunctuation(token) {
|
|
48
|
+
const value = String(token || "");
|
|
49
|
+
let start = 0;
|
|
50
|
+
let end = value.length;
|
|
51
|
+
while (start < end && !isTokenWordChar(value[start])) start += 1;
|
|
52
|
+
while (end > start && !isTokenWordChar(value[end - 1])) end -= 1;
|
|
53
|
+
return value.slice(start, end);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function summarizePathSegment(segment) {
|
|
57
|
+
return String(segment || "")
|
|
58
|
+
.replace(/[-_]+/g, " ")
|
|
59
|
+
.replace(/\.m?js$/i, "")
|
|
60
|
+
.replace(/\.tsx?$/i, "")
|
|
61
|
+
.replace(/\.jsx?$/i, "")
|
|
62
|
+
.replace(/\.ya?ml$/i, "")
|
|
63
|
+
.replace(/\.json$/i, "")
|
|
64
|
+
.replace(/\s+/g, " ")
|
|
65
|
+
.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inferRepoMapEntry(pathValue) {
|
|
69
|
+
const path = String(pathValue || "").trim().replace(/\\/g, "/");
|
|
70
|
+
if (!path) return null;
|
|
71
|
+
const name = path.split("/").pop() || path;
|
|
72
|
+
const stem = summarizePathSegment(name);
|
|
73
|
+
const dir = path.includes("/") ? path.split("/").slice(0, -1).join("/") : "";
|
|
74
|
+
const dirHint = dir ? summarizePathSegment(dir.split("/").pop()) : "";
|
|
75
|
+
const symbols = [];
|
|
76
|
+
const lowerStem = stem.toLowerCase();
|
|
77
|
+
if (lowerStem) {
|
|
78
|
+
const compact = lowerStem
|
|
79
|
+
.split(" ")
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
82
|
+
.join("");
|
|
83
|
+
if (compact) {
|
|
84
|
+
symbols.push(compact);
|
|
85
|
+
if (!compact.startsWith("test")) symbols.push(`test${compact.charAt(0).toUpperCase()}${compact.slice(1)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const summaryParts = [];
|
|
89
|
+
if (dirHint) summaryParts.push(`${dirHint} module`);
|
|
90
|
+
if (stem) summaryParts.push(stem);
|
|
91
|
+
return {
|
|
92
|
+
path,
|
|
93
|
+
summary: summaryParts.join(" — "),
|
|
94
|
+
symbols: uniqueStrings(symbols).slice(0, DEFAULT_MAX_SYMBOLS),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeRepoMapFile(entry, maxSymbols = DEFAULT_MAX_SYMBOLS) {
|
|
99
|
+
if (!entry || typeof entry !== "object") return null;
|
|
100
|
+
const path = String(entry.path || entry.file || "").trim().replace(/\\/g, "/");
|
|
101
|
+
if (!path) return null;
|
|
102
|
+
return {
|
|
103
|
+
path,
|
|
104
|
+
summary: String(entry.summary || entry.description || "").trim(),
|
|
105
|
+
symbols: uniqueStrings(entry.symbols).slice(0, maxSymbols),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function normalizeRepoMap(repoMap, opts = {}) {
|
|
110
|
+
if (!repoMap || typeof repoMap !== "object") return null;
|
|
111
|
+
const maxSymbols = toPositiveInt(opts.maxSymbols, DEFAULT_MAX_SYMBOLS);
|
|
112
|
+
const rawRoot = String(repoMap.root || repoMap.repoRoot || "").trim();
|
|
113
|
+
const root = rawRoot ? rawRoot.replace(/\\/g, "/") : "";
|
|
114
|
+
const files = Array.isArray(repoMap.files)
|
|
115
|
+
? repoMap.files.map((entry) => normalizeRepoMapFile(entry, maxSymbols)).filter(Boolean)
|
|
116
|
+
: [];
|
|
117
|
+
if (!root && files.length === 0) return null;
|
|
118
|
+
return { root, files };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatRepoMap(repoMap, opts = {}) {
|
|
122
|
+
const normalized = normalizeRepoMap(repoMap, opts);
|
|
123
|
+
if (!normalized) return "";
|
|
124
|
+
const lines = [String(opts.title || "## Repo Map")];
|
|
125
|
+
if (normalized.root) lines.push(`- Root: ${normalized.root}`);
|
|
126
|
+
for (const file of normalized.files) {
|
|
127
|
+
const parts = [file.path];
|
|
128
|
+
if (file.symbols.length) parts.push(`symbols: ${file.symbols.join(", ")}`);
|
|
129
|
+
if (file.summary) parts.push(file.summary);
|
|
130
|
+
lines.push(`- ${parts.join(" — ")}`);
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveRootDir(options = {}) {
|
|
136
|
+
const explicit = String(options.rootDir || options.repoRoot || options.cwd || "").trim();
|
|
137
|
+
if (explicit) return explicit.replace(/\\/g, "/");
|
|
138
|
+
const fallback = String(process.cwd() || "").trim();
|
|
139
|
+
return fallback ? fallback.replace(/\\/g, "/") : "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function loadAgentIndex(rootDir) {
|
|
143
|
+
if (!rootDir) return null;
|
|
144
|
+
const filePath = resolve(rootDir, ".bosun", "context-index", "agent-index.json");
|
|
145
|
+
if (!existsSync(filePath)) return null;
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
148
|
+
const files = Array.isArray(parsed?.files) ? parsed.files : [];
|
|
149
|
+
const symbols = Array.isArray(parsed?.symbols) ? parsed.symbols : [];
|
|
150
|
+
return { filePath, files, symbols };
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildIndexMaps(index) {
|
|
157
|
+
const fileByPath = new Map();
|
|
158
|
+
const symbolsByPath = new Map();
|
|
159
|
+
if (!index) return { fileByPath, symbolsByPath };
|
|
160
|
+
for (const file of index.files || []) {
|
|
161
|
+
const path = String(file?.path || "").trim().replace(/\\/g, "/");
|
|
162
|
+
if (!path) continue;
|
|
163
|
+
fileByPath.set(path, file);
|
|
164
|
+
}
|
|
165
|
+
for (const symbol of index.symbols || []) {
|
|
166
|
+
const path = String(symbol?.path || "").trim().replace(/\\/g, "/");
|
|
167
|
+
if (!path) continue;
|
|
168
|
+
if (!symbolsByPath.has(path)) symbolsByPath.set(path, []);
|
|
169
|
+
symbolsByPath.get(path).push(symbol);
|
|
170
|
+
}
|
|
171
|
+
return { fileByPath, symbolsByPath };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function tokenizeQuery(...parts) {
|
|
175
|
+
return uniqueStrings(
|
|
176
|
+
parts
|
|
177
|
+
.map((part) => String(part || "").toLowerCase())
|
|
178
|
+
.flatMap((part) => part.match(/[a-z0-9][a-z0-9._/-]{2,}/g) || [])
|
|
179
|
+
.map((token) => trimTokenBoundaryPunctuation(token))
|
|
180
|
+
.filter((token) => token.length >= 3 && !QUERY_STOP_WORDS.has(token)),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function takeTopSymbols(symbols, tokens, maxSymbols) {
|
|
185
|
+
const normalizedTokens = Array.isArray(tokens) ? tokens : [];
|
|
186
|
+
const scored = (Array.isArray(symbols) ? symbols : []).map((symbol) => {
|
|
187
|
+
const name = String(symbol?.name || "").trim();
|
|
188
|
+
const signature = String(symbol?.signature || "").trim();
|
|
189
|
+
const lowerName = name.toLowerCase();
|
|
190
|
+
const lowerSignature = signature.toLowerCase();
|
|
191
|
+
let score = 0;
|
|
192
|
+
for (const token of normalizedTokens) {
|
|
193
|
+
if (lowerName.includes(token)) score += 30;
|
|
194
|
+
if (lowerSignature.includes(token)) score += 10;
|
|
195
|
+
}
|
|
196
|
+
return { name, score, line: Number(symbol?.line || 0) };
|
|
197
|
+
});
|
|
198
|
+
scored.sort((left, right) => right.score - left.score || left.line - right.line || left.name.localeCompare(right.name));
|
|
199
|
+
return uniqueStrings(scored.map((entry) => entry.name)).slice(0, maxSymbols);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildEntryFromIndex(path, maps, tokens, maxSymbols) {
|
|
203
|
+
const normalizedPath = String(path || "").trim().replace(/\\/g, "/");
|
|
204
|
+
if (!normalizedPath) return null;
|
|
205
|
+
const file = maps.fileByPath.get(normalizedPath);
|
|
206
|
+
const symbols = maps.symbolsByPath.get(normalizedPath) || [];
|
|
207
|
+
const matchedSymbols = takeTopSymbols(symbols, tokens, maxSymbols);
|
|
208
|
+
if (!file) {
|
|
209
|
+
const fallback = inferRepoMapEntry(normalizedPath);
|
|
210
|
+
if (!fallback) return null;
|
|
211
|
+
return {
|
|
212
|
+
...fallback,
|
|
213
|
+
symbols: matchedSymbols.length > 0 ? matchedSymbols : fallback.symbols.slice(0, maxSymbols),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
path: normalizedPath,
|
|
218
|
+
summary: String(file.summary || "").trim(),
|
|
219
|
+
symbols: matchedSymbols.length > 0 ? matchedSymbols : takeTopSymbols(symbols, [], maxSymbols),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function scoreIndexedFile(file, symbols, tokens) {
|
|
224
|
+
const path = String(file?.path || "").toLowerCase();
|
|
225
|
+
const summary = String(file?.summary || "").toLowerCase();
|
|
226
|
+
let score = 0;
|
|
227
|
+
for (const token of tokens) {
|
|
228
|
+
if (path.includes(token)) score += 24;
|
|
229
|
+
if (summary.includes(token)) score += 12;
|
|
230
|
+
for (const symbol of symbols) {
|
|
231
|
+
const name = String(symbol?.name || "").toLowerCase();
|
|
232
|
+
const signature = String(symbol?.signature || "").toLowerCase();
|
|
233
|
+
if (name.includes(token)) score += 20;
|
|
234
|
+
if (signature.includes(token)) score += 8;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (IMPORTANT_PATHS.includes(file?.path)) score += 10;
|
|
238
|
+
return score;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function pickEntriesFromIndex(index, tokens, fileLimit, maxSymbols) {
|
|
242
|
+
if (!index) return [];
|
|
243
|
+
const maps = buildIndexMaps(index);
|
|
244
|
+
const scored = [];
|
|
245
|
+
for (const file of index.files || []) {
|
|
246
|
+
const path = String(file?.path || "").trim().replace(/\\/g, "/");
|
|
247
|
+
if (!path) continue;
|
|
248
|
+
const symbols = maps.symbolsByPath.get(path) || [];
|
|
249
|
+
const score = scoreIndexedFile(file, symbols, tokens);
|
|
250
|
+
if (score <= 0) continue;
|
|
251
|
+
scored.push({ path, score });
|
|
252
|
+
}
|
|
253
|
+
scored.sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
254
|
+
return scored
|
|
255
|
+
.slice(0, fileLimit)
|
|
256
|
+
.map((entry) => buildEntryFromIndex(entry.path, maps, tokens, maxSymbols))
|
|
257
|
+
.filter(Boolean);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function pickImportantEntriesFromIndex(index, fileLimit, maxSymbols) {
|
|
261
|
+
if (!index) return [];
|
|
262
|
+
const maps = buildIndexMaps(index);
|
|
263
|
+
const picked = [];
|
|
264
|
+
for (const path of IMPORTANT_PATHS) {
|
|
265
|
+
const entry = buildEntryFromIndex(path, maps, [], maxSymbols);
|
|
266
|
+
if (entry) picked.push(entry);
|
|
267
|
+
}
|
|
268
|
+
if (picked.length >= fileLimit) return picked.slice(0, fileLimit);
|
|
269
|
+
|
|
270
|
+
const seenDirs = new Set(picked.map((entry) => entry.path.split("/")[0] || ""));
|
|
271
|
+
for (const file of index.files || []) {
|
|
272
|
+
const path = String(file?.path || "").trim().replace(/\\/g, "/");
|
|
273
|
+
if (!path || picked.some((entry) => entry.path === path)) continue;
|
|
274
|
+
const topDir = path.split("/")[0] || "";
|
|
275
|
+
if (seenDirs.has(topDir) && topDir) continue;
|
|
276
|
+
const entry = buildEntryFromIndex(path, maps, [], maxSymbols);
|
|
277
|
+
if (!entry) continue;
|
|
278
|
+
picked.push(entry);
|
|
279
|
+
if (topDir) seenDirs.add(topDir);
|
|
280
|
+
if (picked.length >= fileLimit) break;
|
|
281
|
+
}
|
|
282
|
+
return picked.slice(0, fileLimit);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function scanFilesystemEntries(rootDir, fileLimit, maxSymbols) {
|
|
286
|
+
if (!rootDir || !existsSync(rootDir)) return [];
|
|
287
|
+
const picked = [];
|
|
288
|
+
const pushEntry = (relPath) => {
|
|
289
|
+
const entry = inferRepoMapEntry(relPath);
|
|
290
|
+
if (!entry || picked.some((item) => item.path === entry.path)) return;
|
|
291
|
+
picked.push({ ...entry, symbols: entry.symbols.slice(0, maxSymbols) });
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
for (const relPath of IMPORTANT_PATHS) {
|
|
295
|
+
const absPath = resolve(rootDir, relPath);
|
|
296
|
+
if (existsSync(absPath)) pushEntry(relPath);
|
|
297
|
+
if (picked.length >= fileLimit) return picked;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const topEntries = readdirSync(rootDir, { withFileTypes: true })
|
|
301
|
+
.filter((entry) => !entry.name.startsWith(".") || entry.name === ".github")
|
|
302
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
303
|
+
|
|
304
|
+
for (const entry of topEntries) {
|
|
305
|
+
if (picked.length >= fileLimit) break;
|
|
306
|
+
if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
307
|
+
pushEntry(entry.name);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!entry.isDirectory()) continue;
|
|
311
|
+
const dirPath = join(rootDir, entry.name);
|
|
312
|
+
const child = readdirSync(dirPath, { withFileTypes: true })
|
|
313
|
+
.filter((item) => item.isFile() && SOURCE_EXTENSIONS.has(extname(item.name).toLowerCase()))
|
|
314
|
+
.sort((left, right) => left.name.localeCompare(right.name))[0];
|
|
315
|
+
if (child) pushEntry(join(entry.name, child.name));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return picked.slice(0, fileLimit);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function buildRepoMap(options = {}) {
|
|
322
|
+
const maxSymbols = toPositiveInt(options.repoMapMaxSymbols, DEFAULT_MAX_SYMBOLS);
|
|
323
|
+
const fileLimit = toPositiveInt(options.repoMapFileLimit, DEFAULT_FILE_LIMIT);
|
|
324
|
+
const explicit = normalizeRepoMap(options.repoMap, { maxSymbols });
|
|
325
|
+
const rootDir = resolveRootDir(options);
|
|
326
|
+
if (explicit) {
|
|
327
|
+
return {
|
|
328
|
+
root: explicit.root || rootDir,
|
|
329
|
+
files: explicit.files.slice(0, fileLimit),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const changedFiles = uniqueStrings(options.changedFiles).map((value) => value.replace(/\\/g, "/"));
|
|
334
|
+
const queryTokens = tokenizeQuery(
|
|
335
|
+
options.repoMapQuery,
|
|
336
|
+
options.query,
|
|
337
|
+
options.taskTitle,
|
|
338
|
+
options.taskDescription,
|
|
339
|
+
options.prompt,
|
|
340
|
+
options.userMessage,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const index = loadAgentIndex(rootDir);
|
|
344
|
+
const maps = buildIndexMaps(index);
|
|
345
|
+
|
|
346
|
+
let files = [];
|
|
347
|
+
if (changedFiles.length > 0) {
|
|
348
|
+
files = changedFiles
|
|
349
|
+
.map((path) => buildEntryFromIndex(path, maps, queryTokens, maxSymbols))
|
|
350
|
+
.filter(Boolean)
|
|
351
|
+
.slice(0, fileLimit);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (files.length === 0 && queryTokens.length > 0) {
|
|
355
|
+
files = pickEntriesFromIndex(index, queryTokens, fileLimit, maxSymbols);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (files.length === 0) {
|
|
359
|
+
files = pickImportantEntriesFromIndex(index, fileLimit, maxSymbols);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (files.length === 0) {
|
|
363
|
+
files = scanFilesystemEntries(rootDir, fileLimit, maxSymbols);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!rootDir && files.length === 0) return null;
|
|
367
|
+
return { root: rootDir, files };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function inferExecutionRole(options = {}, effectiveMode = "agent") {
|
|
371
|
+
const explicitRole = String(options.executionRole || "").trim().toLowerCase();
|
|
372
|
+
if (explicitRole) return explicitRole;
|
|
373
|
+
if (effectiveMode === "plan") return "architect";
|
|
374
|
+
const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
|
|
375
|
+
if (architectPlan) return "editor";
|
|
376
|
+
return "";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function buildArchitectEditorFrame(options = {}, effectiveMode = "agent") {
|
|
380
|
+
const executionRole = inferExecutionRole(options, effectiveMode);
|
|
381
|
+
const repoMapBlock = formatRepoMap(buildRepoMap(options), options);
|
|
382
|
+
const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
|
|
383
|
+
const lines = ["## Architect/Editor Execution"];
|
|
384
|
+
|
|
385
|
+
if (executionRole === "architect") {
|
|
386
|
+
lines.push(
|
|
387
|
+
"You are the architect phase.",
|
|
388
|
+
"Do not implement code changes in this phase.",
|
|
389
|
+
"Use the repo map to produce a compact structural plan that an editor can execute and validate.",
|
|
390
|
+
"Editor handoff: include ordered implementation steps, touched files, risks, and validation guidance.",
|
|
391
|
+
);
|
|
392
|
+
} else if (executionRole === "editor") {
|
|
393
|
+
lines.push(
|
|
394
|
+
"You are the editor phase.",
|
|
395
|
+
"Implement the approved plan with focused edits and verification.",
|
|
396
|
+
"Prefer the supplied repo map over broad rediscovery unless validation reveals drift.",
|
|
397
|
+
);
|
|
398
|
+
if (architectPlan) {
|
|
399
|
+
lines.push("", "## Architect Plan", architectPlan);
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
return repoMapBlock;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (repoMapBlock) {
|
|
406
|
+
lines.push("", repoMapBlock);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return lines.join("\n");
|
|
410
|
+
}
|
|
411
|
+
|