bosun 0.42.1 → 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.
@@ -9,6 +9,7 @@ import { loadConfig } from "../config/config.mjs";
9
9
  import { ensureCodexConfig, printConfigSummary } from "../shell/codex-config.mjs";
10
10
  import { ensureRepoConfigs, printRepoConfigSummary } from "../config/repo-config.mjs";
11
11
  import { resolveRepoRoot } from "../config/repo-root.mjs";
12
+ import { buildArchitectEditorFrame } from "../lib/repo-map.mjs";
12
13
  import { getAgentToolConfig, getEffectiveTools } from "./agent-tool-config.mjs";
13
14
  import { getSessionTracker } from "../infra/session-tracker.mjs";
14
15
  import { getEntry, getEntryContent, resolveAgentProfileLibraryMetadata } from "../infra/library-manager.mjs";
@@ -191,133 +192,9 @@ function appendAttachmentsToPrompt(message, attachments) {
191
192
  return { message: `${message}${lines.join("\n")}`, appended: true };
192
193
  }
193
194
 
194
- function normalizeRepoMap(repoMap) {
195
- if (!repoMap || typeof repoMap !== "object") return null;
196
- const root = String(repoMap.root || repoMap.repoRoot || "").trim();
197
- const files = Array.isArray(repoMap.files)
198
- ? repoMap.files
199
- .filter((entry) => entry && typeof entry === "object")
200
- .map((entry) => ({
201
- path: String(entry.path || entry.file || "").trim(),
202
- summary: String(entry.summary || entry.description || "").trim(),
203
- symbols: Array.isArray(entry.symbols)
204
- ? entry.symbols.map((symbol) => String(symbol || "").trim()).filter(Boolean)
205
- : [],
206
- }))
207
- .filter((entry) => entry.path)
208
- : [];
209
- if (!root && files.length === 0) return null;
210
- return { root, files };
211
- }
212
-
213
- function formatRepoMap(repoMap) {
214
- const normalized = normalizeRepoMap(repoMap);
215
- if (!normalized) return "";
216
- const lines = ["## Repo Map"];
217
- if (normalized.root) lines.push(`- Root: ${normalized.root}`);
218
- for (const file of normalized.files) {
219
- const parts = [file.path];
220
- if (file.symbols.length) parts.push(`symbols: ${file.symbols.join(", ")}`);
221
- if (file.summary) parts.push(file.summary);
222
- lines.push(`- ${parts.join(" — ")}`);
223
- }
224
- return lines.join("\n");
225
- }
226
-
227
- function summarizePathSegment(segment) {
228
- return String(segment || "")
229
- .replace(/[-_]+/g, " ")
230
- .replace(/\.m?js$/i, "")
231
- .replace(/\s+/g, " ")
232
- .trim();
233
- }
234
-
235
- function inferRepoMapEntry(pathValue) {
236
- const path = String(pathValue || "").trim().replace(/\\/g, "/");
237
- if (!path) return null;
238
- const name = path.split("/").pop() || path;
239
- const stem = summarizePathSegment(name);
240
- const dir = path.includes("/") ? path.split("/").slice(0, -1).join("/") : "";
241
- const dirHint = dir ? summarizePathSegment(dir.split("/").pop()) : "";
242
- const symbols = [];
243
- const lowerStem = stem.toLowerCase();
244
- if (lowerStem) {
245
- const compact = lowerStem
246
- .split(" ")
247
- .filter(Boolean)
248
- .map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
249
- .join("");
250
- if (compact) {
251
- symbols.push(compact);
252
- if (!compact.startsWith("test")) symbols.push(`test${compact.charAt(0).toUpperCase()}${compact.slice(1)}`);
253
- }
254
- }
255
- const summaryParts = [];
256
- if (dirHint) summaryParts.push(`${dirHint} module`);
257
- if (stem) summaryParts.push(stem);
258
- return {
259
- path,
260
- summary: summaryParts.join(" — "),
261
- symbols: [...new Set(symbols)].slice(0, 3),
262
- };
263
- }
264
195
 
265
- function deriveRepoMap(options = {}) {
266
- const explicit = normalizeRepoMap(options.repoMap);
267
- if (explicit) return explicit;
268
- const changedFiles = Array.isArray(options.changedFiles)
269
- ? options.changedFiles.map((value) => String(value || "").trim()).filter(Boolean)
270
- : [];
271
- if (!changedFiles.length) return null;
272
- const root = String(options.repoRoot || options.cwd || resolveRepoRoot() || "").trim();
273
- const files = changedFiles
274
- .map((pathValue) => inferRepoMapEntry(pathValue))
275
- .filter(Boolean)
276
- .slice(0, Number(options.repoMapFileLimit) > 0 ? Number(options.repoMapFileLimit) : 12);
277
- if (!root && files.length === 0) return null;
278
- return { root, files };
279
- }
280
196
 
281
- function inferExecutionRole(options = {}, effectiveMode = "agent") {
282
- const explicitRole = String(options.executionRole || "").trim().toLowerCase();
283
- if (explicitRole) return explicitRole;
284
- if (effectiveMode === "plan") return "architect";
285
- const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
286
- if (architectPlan) return "editor";
287
- return "";
288
- }
289
- function buildArchitectEditorFrame(options = {}, effectiveMode = "agent") {
290
- const executionRole = inferExecutionRole(options, effectiveMode);
291
- const repoMapBlock = formatRepoMap(deriveRepoMap(options));
292
- const architectPlan = String(options.architectPlan || options.planSummary || "").trim();
293
- const lines = ["## Architect/Editor Execution"];
294
-
295
- if (executionRole === "architect") {
296
- lines.push(
297
- "You are the architect phase.",
298
- "Do not implement code changes in this phase.",
299
- "Use the repo map to produce a compact structural plan that an editor can execute and validate.",
300
- "Editor handoff: include ordered implementation steps, touched files, risks, and validation guidance.",
301
- );
302
- } else if (executionRole === "editor") {
303
- lines.push(
304
- "You are the editor phase.",
305
- "Implement the approved plan with focused edits and verification.",
306
- "Prefer the supplied repo map over broad rediscovery unless validation reveals drift.",
307
- );
308
- if (architectPlan) {
309
- lines.push("", "## Architect Plan", architectPlan);
310
- }
311
- } else {
312
- return repoMapBlock;
313
- }
314
-
315
- if (repoMapBlock) {
316
- lines.push("", repoMapBlock);
317
- }
318
197
 
319
- return lines.join("\n");
320
- }
321
198
 
322
199
  function summarizeContextCompressionItems(items) {
323
200
  if (!Array.isArray(items) || items.length === 0) return null;
@@ -1190,8 +1067,9 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
1190
1067
  const messageWithAttachments = attachments.length && !attachmentsAppended
1191
1068
  ? appendAttachmentsToPrompt(userMessage, attachments).message
1192
1069
  : userMessage;
1070
+ const architectEditorFrame = buildArchitectEditorFrame(options, effectiveMode);
1193
1071
  const toolContract = buildPrimaryToolCapabilityContract(options);
1194
- const messageWithToolContract = [selectedProfile.block, toolContract, messageWithAttachments]
1072
+ const messageWithToolContract = [selectedProfile.block, architectEditorFrame, toolContract, messageWithAttachments]
1195
1073
  .filter(Boolean)
1196
1074
  .join("\n\n");
1197
1075
  const framedMessage = modePrefix ? modePrefix + messageWithToolContract : messageWithToolContract;
@@ -1680,3 +1558,5 @@ export async function execSdkCommand(command, args = "", adapterName, options =
1680
1558
  }
1681
1559
 
1682
1560
 
1561
+
1562
+
@@ -264,6 +264,8 @@ export class SessionTracker {
264
264
  lastActivityAt: Date.now(),
265
265
  metadata: {},
266
266
  insights: buildSessionInsights({ messages: [] }),
267
+ trajectory: { version: 1, replayable: true, steps: [] },
268
+ summary: null,
267
269
  });
268
270
  const session = this.#sessions.get(taskId);
269
271
  this.#markDirty(taskId);
@@ -319,6 +321,7 @@ export class SessionTracker {
319
321
  if (Number.isFinite(maxMessages) && maxMessages > 0) {
320
322
  while (session.messages.length > maxMessages) session.messages.shift();
321
323
  }
324
+ this.#appendTrajectoryStep(session, event);
322
325
  this.#refreshDerivedState(session);
323
326
  this.#markDirty(taskId);
324
327
  emitSessionEvent(session, msg);
@@ -352,6 +355,7 @@ export class SessionTracker {
352
355
  if (Number.isFinite(maxMessages) && maxMessages > 0) {
353
356
  while (session.messages.length > maxMessages) session.messages.shift();
354
357
  }
358
+ this.#appendTrajectoryStep(session, event);
355
359
  this.#refreshDerivedState(session);
356
360
  this.#markDirty(taskId);
357
361
  emitSessionEvent(session, msg);
@@ -369,6 +373,7 @@ export class SessionTracker {
369
373
  if (Number.isFinite(maxMessages) && maxMessages > 0) {
370
374
  while (session.messages.length > maxMessages) session.messages.shift();
371
375
  }
376
+ this.#appendTrajectoryStep(session, event);
372
377
  this.#refreshDerivedState(session);
373
378
  this.#markDirty(taskId);
374
379
  emitSessionEvent(session, msg);
@@ -619,6 +624,8 @@ export class SessionTracker {
619
624
  metadata,
620
625
  maxMessages: resolvedMax,
621
626
  insights: buildSessionInsights({ messages: [] }),
627
+ trajectory: { version: 1, replayable: true, steps: [] },
628
+ summary: null,
622
629
  };
623
630
  this.#sessions.set(id, session);
624
631
  this.#markDirty(id);
@@ -797,6 +804,13 @@ export class SessionTracker {
797
804
  this.#flushDirty();
798
805
  }
799
806
 
807
+ /**
808
+ * Flush all dirty sessions to disk immediately (alias for flush).
809
+ */
810
+ flushNow() {
811
+ this.#flushDirty();
812
+ }
813
+
800
814
  /**
801
815
  * Stop all timers and flush pending writes (for cleanup).
802
816
  */
@@ -1003,6 +1017,84 @@ export class SessionTracker {
1003
1017
  return true;
1004
1018
  }
1005
1019
 
1020
+ #appendTrajectoryStep(session, event) {
1021
+ if (!session) return;
1022
+ if (!session.trajectory) {
1023
+ session.trajectory = { version: 1, replayable: true, steps: [] };
1024
+ }
1025
+ const step = this.#extractTrajectoryStep(event, session);
1026
+ if (step) {
1027
+ session.trajectory.steps.push(step);
1028
+ }
1029
+ }
1030
+
1031
+ #extractTrajectoryStep(event, session) {
1032
+ const ts = new Date().toISOString();
1033
+ const id = `step-${Date.now()}-${randomToken(6)}`;
1034
+
1035
+ // String event
1036
+ if (typeof event === "string") {
1037
+ return { id, kind: "system", summary: event.trim().slice(0, 200), timestamp: ts };
1038
+ }
1039
+
1040
+ // Direct message format (role/content)
1041
+ if (event?.role && event?.content !== undefined) {
1042
+ const content = String(event.content).slice(0, 200);
1043
+ const timestamp = event.timestamp || ts;
1044
+ if (event.role === "user") return { id, kind: "user_message", summary: content, timestamp };
1045
+ if (event.role === "assistant") return { id, kind: "assistant", summary: content, timestamp };
1046
+ return { id, kind: event.role, summary: content, timestamp };
1047
+ }
1048
+
1049
+ // SDK item.started events
1050
+ if (event?.type === "item.started" && event?.item) {
1051
+ const item = event.item;
1052
+ if (item.type === "command_execution") {
1053
+ return { id, kind: "tool_call", summary: `Ran ${item.command || "unknown"}`, timestamp: ts };
1054
+ }
1055
+ if (item.type === "reasoning") {
1056
+ return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
1057
+ }
1058
+ if (item.type === "function_call" || item.type === "mcp_tool_call") {
1059
+ return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
1060
+ }
1061
+ return null;
1062
+ }
1063
+
1064
+ // SDK item.completed events
1065
+ if (event?.type === "item.completed" && event?.item) {
1066
+ const item = event.item;
1067
+ if (item.type === "reasoning") {
1068
+ return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
1069
+ }
1070
+ if (item.type === "function_call" || item.type === "mcp_tool_call") {
1071
+ return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
1072
+ }
1073
+ if (item.type === "command_execution") {
1074
+ const cmd = item.command || "";
1075
+ const hasPriorStart = (session?.trajectory?.steps || []).some(
1076
+ (s) => s.kind === "tool_call" && s.summary === `Ran ${cmd}`,
1077
+ );
1078
+ if (hasPriorStart) {
1079
+ return { id, kind: "tool_result", summary: `${cmd} (exit ${item.exit_code ?? "?"})`, timestamp: ts };
1080
+ }
1081
+ return { id, kind: "command", summary: cmd, timestamp: ts };
1082
+ }
1083
+ if (item.type === "agent_message") {
1084
+ return { id, kind: "assistant", summary: item.text || "", timestamp: ts };
1085
+ }
1086
+ return null;
1087
+ }
1088
+
1089
+ // Assistant message events
1090
+ if (event?.type === "assistant.message") {
1091
+ const content = event?.data?.content || event?.content || "";
1092
+ return { id, kind: "agent_message", summary: content.slice(0, 200), timestamp: ts };
1093
+ }
1094
+
1095
+ return null;
1096
+ }
1097
+
1006
1098
  #refreshDerivedState(session) {
1007
1099
  if (!session) return;
1008
1100
  try {
@@ -1013,6 +1105,22 @@ export class SessionTracker {
1013
1105
  } catch {
1014
1106
  // Inspector insights are best-effort only.
1015
1107
  }
1108
+ try {
1109
+ const steps = session.trajectory?.steps || [];
1110
+ const totalSteps = steps.length;
1111
+ const isFailed = session.status === "failed";
1112
+ const isLong = totalSteps > 12;
1113
+ const failedOrLongRun = isFailed || isLong;
1114
+ const resumable = failedOrLongRun;
1115
+ const shortSteps = steps.slice(-12).map((s) => ({ kind: s.kind, summary: s.summary }));
1116
+ const latestStep =
1117
+ steps.length > 0
1118
+ ? { kind: steps[steps.length - 1].kind, summary: steps[steps.length - 1].summary }
1119
+ : null;
1120
+ session.summary = { failedOrLongRun, resumable, totalSteps, shortSteps, latestStep };
1121
+ } catch {
1122
+ // Summary computation is best-effort only.
1123
+ }
1016
1124
  }
1017
1125
 
1018
1126
  #ensureDir() {
@@ -1052,6 +1160,8 @@ export class SessionTracker {
1052
1160
  messages: session.messages || [],
1053
1161
  metadata: session.metadata || {},
1054
1162
  insights: session.insights || null,
1163
+ trajectory: session.trajectory || null,
1164
+ summary: session.summary || null,
1055
1165
  };
1056
1166
  writeFileSync(filePath, JSON.stringify(data, null, 2));
1057
1167
  } catch (err) {
@@ -1131,6 +1241,8 @@ export class SessionTracker {
1131
1241
  lastActivityAt: lastActive || Date.now(),
1132
1242
  metadata: data.metadata || {},
1133
1243
  insights: data.insights || buildSessionInsights({ messages: data.messages || [] }),
1244
+ trajectory: data.trajectory || { version: 1, replayable: true, steps: [] },
1245
+ summary: data.summary || null,
1134
1246
  });
1135
1247
  const restored = this.#sessions.get(id);
1136
1248
  if (restored && isTerminalSessionStatus(restored.status) && !restored.accumulatedAt) {