opencode-orchestrator 1.2.70 → 1.3.3

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/dist/index.js CHANGED
@@ -1903,10 +1903,14 @@ var init_plugin_hooks = __esm({
1903
1903
  PLUGIN_HOOKS = {
1904
1904
  /** Intercepts user messages before sending to LLM */
1905
1905
  CHAT_MESSAGE: "chat.message",
1906
- /** Runs after LLM finishes responding */
1907
- ASSISTANT_DONE: "assistant.done",
1906
+ /** Runs before any tool call */
1907
+ TOOL_EXECUTE_BEFORE: "tool.execute.before",
1908
1908
  /** Runs after any tool call completes */
1909
- TOOL_EXECUTE_AFTER: "tool.execute.after"
1909
+ TOOL_EXECUTE_AFTER: "tool.execute.after",
1910
+ /** Preserves custom compaction context */
1911
+ EXPERIMENTAL_SESSION_COMPACTING: "experimental.session.compacting",
1912
+ /** Injects dynamic system prompt additions */
1913
+ EXPERIMENTAL_CHAT_SYSTEM_TRANSFORM: "experimental.chat.system.transform"
1910
1914
  };
1911
1915
  }
1912
1916
  });
@@ -7966,10 +7970,10 @@ function mergeDefs(...defs) {
7966
7970
  function cloneDef(schema) {
7967
7971
  return mergeDefs(schema._zod.def);
7968
7972
  }
7969
- function getElementAtPath(obj, path10) {
7970
- if (!path10)
7973
+ function getElementAtPath(obj, path11) {
7974
+ if (!path11)
7971
7975
  return obj;
7972
- return path10.reduce((acc, key) => acc?.[key], obj);
7976
+ return path11.reduce((acc, key) => acc?.[key], obj);
7973
7977
  }
7974
7978
  function promiseAllObject(promisesObj) {
7975
7979
  const keys = Object.keys(promisesObj);
@@ -8378,11 +8382,11 @@ function explicitlyAborted(x, startIndex = 0) {
8378
8382
  }
8379
8383
  return false;
8380
8384
  }
8381
- function prefixIssues(path10, issues) {
8385
+ function prefixIssues(path11, issues) {
8382
8386
  return issues.map((iss) => {
8383
8387
  var _a3;
8384
8388
  (_a3 = iss).path ?? (_a3.path = []);
8385
- iss.path.unshift(path10);
8389
+ iss.path.unshift(path11);
8386
8390
  return iss;
8387
8391
  });
8388
8392
  }
@@ -8529,16 +8533,16 @@ function flattenError(error95, mapper = (issue3) => issue3.message) {
8529
8533
  }
8530
8534
  function formatError(error95, mapper = (issue3) => issue3.message) {
8531
8535
  const fieldErrors = { _errors: [] };
8532
- const processError = (error96, path10 = []) => {
8536
+ const processError = (error96, path11 = []) => {
8533
8537
  for (const issue3 of error96.issues) {
8534
8538
  if (issue3.code === "invalid_union" && issue3.errors.length) {
8535
- issue3.errors.map((issues) => processError({ issues }, [...path10, ...issue3.path]));
8539
+ issue3.errors.map((issues) => processError({ issues }, [...path11, ...issue3.path]));
8536
8540
  } else if (issue3.code === "invalid_key") {
8537
- processError({ issues: issue3.issues }, [...path10, ...issue3.path]);
8541
+ processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
8538
8542
  } else if (issue3.code === "invalid_element") {
8539
- processError({ issues: issue3.issues }, [...path10, ...issue3.path]);
8543
+ processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
8540
8544
  } else {
8541
- const fullpath = [...path10, ...issue3.path];
8545
+ const fullpath = [...path11, ...issue3.path];
8542
8546
  if (fullpath.length === 0) {
8543
8547
  fieldErrors._errors.push(mapper(issue3));
8544
8548
  } else {
@@ -8565,17 +8569,17 @@ function formatError(error95, mapper = (issue3) => issue3.message) {
8565
8569
  }
8566
8570
  function treeifyError(error95, mapper = (issue3) => issue3.message) {
8567
8571
  const result = { errors: [] };
8568
- const processError = (error96, path10 = []) => {
8572
+ const processError = (error96, path11 = []) => {
8569
8573
  var _a3, _b;
8570
8574
  for (const issue3 of error96.issues) {
8571
8575
  if (issue3.code === "invalid_union" && issue3.errors.length) {
8572
- issue3.errors.map((issues) => processError({ issues }, [...path10, ...issue3.path]));
8576
+ issue3.errors.map((issues) => processError({ issues }, [...path11, ...issue3.path]));
8573
8577
  } else if (issue3.code === "invalid_key") {
8574
- processError({ issues: issue3.issues }, [...path10, ...issue3.path]);
8578
+ processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
8575
8579
  } else if (issue3.code === "invalid_element") {
8576
- processError({ issues: issue3.issues }, [...path10, ...issue3.path]);
8580
+ processError({ issues: issue3.issues }, [...path11, ...issue3.path]);
8577
8581
  } else {
8578
- const fullpath = [...path10, ...issue3.path];
8582
+ const fullpath = [...path11, ...issue3.path];
8579
8583
  if (fullpath.length === 0) {
8580
8584
  result.errors.push(mapper(issue3));
8581
8585
  continue;
@@ -8607,8 +8611,8 @@ function treeifyError(error95, mapper = (issue3) => issue3.message) {
8607
8611
  }
8608
8612
  function toDotPath(_path) {
8609
8613
  const segs = [];
8610
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8611
- for (const seg of path10) {
8614
+ const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8615
+ for (const seg of path11) {
8612
8616
  if (typeof seg === "number")
8613
8617
  segs.push(`[${seg}]`);
8614
8618
  else if (typeof seg === "symbol")
@@ -21300,13 +21304,13 @@ function resolveRef(ref, ctx) {
21300
21304
  if (!ref.startsWith("#")) {
21301
21305
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
21302
21306
  }
21303
- const path10 = ref.slice(1).split("/").filter(Boolean);
21304
- if (path10.length === 0) {
21307
+ const path11 = ref.slice(1).split("/").filter(Boolean);
21308
+ if (path11.length === 0) {
21305
21309
  return ctx.rootSchema;
21306
21310
  }
21307
21311
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
21308
- if (path10[0] === defsKey) {
21309
- const key = path10[1];
21312
+ if (path11[0] === defsKey) {
21313
+ const key = path11[1];
21310
21314
  if (!key || !ctx.defs[key]) {
21311
21315
  throw new Error(`Reference not found: ${ref}`);
21312
21316
  }
@@ -23909,10 +23913,10 @@ function mergeDefs2(...defs) {
23909
23913
  function cloneDef2(schema) {
23910
23914
  return mergeDefs2(schema._zod.def);
23911
23915
  }
23912
- function getElementAtPath2(obj, path10) {
23913
- if (!path10)
23916
+ function getElementAtPath2(obj, path11) {
23917
+ if (!path11)
23914
23918
  return obj;
23915
- return path10.reduce((acc, key) => acc?.[key], obj);
23919
+ return path11.reduce((acc, key) => acc?.[key], obj);
23916
23920
  }
23917
23921
  function promiseAllObject2(promisesObj) {
23918
23922
  const keys = Object.keys(promisesObj);
@@ -24273,11 +24277,11 @@ function aborted2(x, startIndex = 0) {
24273
24277
  }
24274
24278
  return false;
24275
24279
  }
24276
- function prefixIssues2(path10, issues) {
24280
+ function prefixIssues2(path11, issues) {
24277
24281
  return issues.map((iss) => {
24278
24282
  var _a3;
24279
24283
  (_a3 = iss).path ?? (_a3.path = []);
24280
- iss.path.unshift(path10);
24284
+ iss.path.unshift(path11);
24281
24285
  return iss;
24282
24286
  });
24283
24287
  }
@@ -24445,7 +24449,7 @@ function treeifyError2(error95, _mapper) {
24445
24449
  return issue3.message;
24446
24450
  };
24447
24451
  const result = { errors: [] };
24448
- const processError = (error96, path10 = []) => {
24452
+ const processError = (error96, path11 = []) => {
24449
24453
  var _a3, _b;
24450
24454
  for (const issue3 of error96.issues) {
24451
24455
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -24455,7 +24459,7 @@ function treeifyError2(error95, _mapper) {
24455
24459
  } else if (issue3.code === "invalid_element") {
24456
24460
  processError({ issues: issue3.issues }, issue3.path);
24457
24461
  } else {
24458
- const fullpath = [...path10, ...issue3.path];
24462
+ const fullpath = [...path11, ...issue3.path];
24459
24463
  if (fullpath.length === 0) {
24460
24464
  result.errors.push(mapper(issue3));
24461
24465
  continue;
@@ -24487,8 +24491,8 @@ function treeifyError2(error95, _mapper) {
24487
24491
  }
24488
24492
  function toDotPath2(_path) {
24489
24493
  const segs = [];
24490
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
24491
- for (const seg of path10) {
24494
+ const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
24495
+ for (const seg of path11) {
24492
24496
  if (typeof seg === "number")
24493
24497
  segs.push(`[${seg}]`);
24494
24498
  else if (typeof seg === "symbol")
@@ -36638,6 +36642,7 @@ function ensureSessionInitialized(sessions, sessionID, directory) {
36638
36642
  timestamp: now,
36639
36643
  startTime: now,
36640
36644
  lastStepTime: now,
36645
+ lastCompletedMessageID: void 0,
36641
36646
  tokens: { totalInput: 0, totalOutput: 0, estimatedCost: 0 }
36642
36647
  };
36643
36648
  if (directory) {
@@ -37284,10 +37289,10 @@ async function resolveCommandPath(key, commandName) {
37284
37289
  const currentPending = pending.get(key);
37285
37290
  if (currentPending) return currentPending;
37286
37291
  const promise3 = (async () => {
37287
- const path10 = await findCommand(commandName);
37288
- cache[key] = path10;
37292
+ const path11 = await findCommand(commandName);
37293
+ cache[key] = path11;
37289
37294
  pending.delete(key);
37290
- return path10;
37295
+ return path11;
37291
37296
  })();
37292
37297
  pending.set(key, promise3);
37293
37298
  return promise3;
@@ -37296,14 +37301,14 @@ async function resolveCommandPath(key, commandName) {
37296
37301
  // src/core/notification/os-notify/notifier.ts
37297
37302
  var execAsync2 = promisify2(exec2);
37298
37303
  async function notifyDarwin(title, message) {
37299
- const path10 = await resolveCommandPath(
37304
+ const path11 = await resolveCommandPath(
37300
37305
  NOTIFICATION_COMMAND_KEYS.OSASCRIPT,
37301
37306
  NOTIFICATION_COMMANDS.OSASCRIPT
37302
37307
  );
37303
- if (!path10) return;
37308
+ if (!path11) return;
37304
37309
  const escT = title.replace(/"/g, '\\"');
37305
37310
  const escM = message.replace(/"/g, '\\"');
37306
- await execAsync2(`${path10} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"' >/dev/null 2>/dev/null`);
37311
+ await execAsync2(`${path11} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"' >/dev/null 2>/dev/null`);
37307
37312
  }
37308
37313
  function isWSL() {
37309
37314
  try {
@@ -37316,11 +37321,11 @@ function isWSL() {
37316
37321
  }
37317
37322
  async function notifyLinux(title, message) {
37318
37323
  if (isWSL()) return;
37319
- const path10 = await resolveCommandPath(
37324
+ const path11 = await resolveCommandPath(
37320
37325
  NOTIFICATION_COMMAND_KEYS.NOTIFY_SEND,
37321
37326
  NOTIFICATION_COMMANDS.NOTIFY_SEND
37322
37327
  );
37323
- if (path10) await execAsync2(`${path10} "${title}" "${message}" >/dev/null 2>/dev/null`);
37328
+ if (path11) await execAsync2(`${path11} "${title}" "${message}" >/dev/null 2>/dev/null`);
37324
37329
  }
37325
37330
  async function notifyWindows(title, message) {
37326
37331
  const ps = await resolveCommandPath(
@@ -37368,11 +37373,11 @@ init_os();
37368
37373
  async function playDarwin(soundPath) {
37369
37374
  if (!soundPath) return;
37370
37375
  try {
37371
- const path10 = await resolveCommandPath(
37376
+ const path11 = await resolveCommandPath(
37372
37377
  NOTIFICATION_COMMAND_KEYS.AFPLAY,
37373
37378
  NOTIFICATION_COMMANDS.AFPLAY
37374
37379
  );
37375
- if (path10) exec3(`"${path10}" "${soundPath}" >/dev/null 2>/dev/null`);
37380
+ if (path11) exec3(`"${path11}" "${soundPath}" >/dev/null 2>/dev/null`);
37376
37381
  } catch (err) {
37377
37382
  log(`[session-notify] Error playing sound (Darwin): ${err}`);
37378
37383
  }
@@ -41541,6 +41546,55 @@ function handleSessionCompacted(sessionID) {
41541
41546
 
41542
41547
  // src/plugin-handlers/event-handler.ts
41543
41548
  init_shared();
41549
+
41550
+ // src/plugin-handlers/assistant-done-handler.ts
41551
+ init_logger();
41552
+ init_shared();
41553
+ async function handleCompletedAssistantMessage(ctx, sessionID, messageID) {
41554
+ const { client, directory, sessions } = ctx;
41555
+ const hooks = HookRegistry.getInstance();
41556
+ const session = sessions.get(sessionID);
41557
+ if (!session?.active || session.lastCompletedMessageID === messageID) {
41558
+ return;
41559
+ }
41560
+ const textContent = await readAssistantText(client, sessionID, messageID);
41561
+ session.lastCompletedMessageID = messageID;
41562
+ const result = await hooks.executeDone(
41563
+ { sessionID, directory, sessions },
41564
+ textContent
41565
+ );
41566
+ if (result.action !== "inject" || result.prompts.length === 0) {
41567
+ return;
41568
+ }
41569
+ const now = Date.now();
41570
+ session.step++;
41571
+ session.timestamp = now;
41572
+ session.lastStepTime = now;
41573
+ try {
41574
+ const parts = result.prompts.map((text) => ({ type: PART_TYPES.TEXT, text }));
41575
+ client.session.prompt({
41576
+ path: { id: sessionID },
41577
+ body: { parts }
41578
+ }).catch((error95) => {
41579
+ log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
41580
+ });
41581
+ } catch (error95) {
41582
+ log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
41583
+ }
41584
+ }
41585
+ async function readAssistantText(client, sessionID, messageID) {
41586
+ try {
41587
+ const response = await client.session.message({
41588
+ path: { id: sessionID, messageID }
41589
+ });
41590
+ return (response.parts ?? []).filter((part) => part.type === PART_TYPES.TEXT || part.type === PART_TYPES.REASONING).map((part) => part.text ?? "").join("\n");
41591
+ } catch (error95) {
41592
+ log("[assistant-done-handler] Failed to read assistant message", { sessionID, messageID, error: error95 });
41593
+ return "";
41594
+ }
41595
+ }
41596
+
41597
+ // src/plugin-handlers/event-handler.ts
41544
41598
  function createEventHandler(ctx) {
41545
41599
  const { client, directory, sessions, state: state2 } = ctx;
41546
41600
  return async (input) => {
@@ -41591,7 +41645,7 @@ function createEventHandler(ctx) {
41591
41645
  if (event.type === MESSAGE_EVENTS.UPDATED) {
41592
41646
  const messageProperties = event.properties;
41593
41647
  const messageInfo = messageProperties?.info;
41594
- const sessionID = messageInfo?.sessionID;
41648
+ const sessionID = messageProperties.sessionID || messageInfo?.sessionID;
41595
41649
  const role = messageInfo?.role;
41596
41650
  if (sessionID && messageProperties?.usage) {
41597
41651
  const totalTokens = messageProperties.usage.totalTokens ?? (messageProperties.usage.inputTokens ?? 0) + (messageProperties.usage.outputTokens ?? 0);
@@ -41601,6 +41655,9 @@ function createEventHandler(ctx) {
41601
41655
  }
41602
41656
  if (sessionID && role === MESSAGE_ROLES.ASSISTANT) {
41603
41657
  markRecoveryComplete(sessionID);
41658
+ if (messageInfo?.id && messageInfo.time?.completed) {
41659
+ await handleCompletedAssistantMessage(ctx, sessionID, messageInfo.id);
41660
+ }
41604
41661
  }
41605
41662
  if (sessionID && role === MESSAGE_ROLES.USER) {
41606
41663
  handleUserMessage(sessionID);
@@ -41682,53 +41739,6 @@ function createToolExecuteAfterHandler(ctx) {
41682
41739
  };
41683
41740
  }
41684
41741
 
41685
- // src/plugin-handlers/assistant-done-handler.ts
41686
- init_logger();
41687
- init_shared();
41688
- function createAssistantDoneHandler(ctx) {
41689
- const { client, directory, sessions } = ctx;
41690
- const hooks = HookRegistry.getInstance();
41691
- return async (assistantInput, assistantOutput) => {
41692
- const sessionID = assistantInput.sessionID;
41693
- const session = sessions.get(sessionID);
41694
- if (!session?.active) return;
41695
- const parts = assistantOutput.parts;
41696
- const textContent = parts?.filter((p) => p.type === PART_TYPES.TEXT || p.type === PART_TYPES.REASONING).map((p) => p.text || "").join("\n") || "";
41697
- const hookContext = {
41698
- sessionID,
41699
- directory,
41700
- sessions
41701
- // Cast because types might slightly differ in strict mode, but it's the same object
41702
- };
41703
- const result = await hooks.executeDone(hookContext, textContent);
41704
- if (result.action === "stop") {
41705
- return;
41706
- }
41707
- if (result.action === "inject" && result.prompts) {
41708
- const now = Date.now();
41709
- session.step++;
41710
- session.timestamp = now;
41711
- session.lastStepTime = now;
41712
- try {
41713
- if (client?.session?.prompt) {
41714
- const parts2 = result.prompts.map((p) => ({
41715
- type: PART_TYPES.TEXT,
41716
- text: p
41717
- }));
41718
- client.session.prompt({
41719
- path: { id: sessionID },
41720
- body: { parts: parts2 }
41721
- }).catch((error95) => {
41722
- log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
41723
- });
41724
- }
41725
- } catch (error95) {
41726
- log("[assistant-done-handler] Failed to inject continuation prompts", { sessionID, error: error95 });
41727
- }
41728
- }
41729
- };
41730
- }
41731
-
41732
41742
  // src/plugin-handlers/session-compacting-handler.ts
41733
41743
  init_shared();
41734
41744
  function createSessionCompactingHandler(ctx) {
@@ -41805,6 +41815,564 @@ Wait for these tasks to complete before concluding the mission.
41805
41815
 
41806
41816
  // src/plugin-handlers/system-transform-handler.ts
41807
41817
  init_shared();
41818
+
41819
+ // src/core/knowledge/context-provider.ts
41820
+ import { existsSync as existsSync9, readFileSync as readFileSync5, readdirSync } from "node:fs";
41821
+ import path10 from "node:path";
41822
+
41823
+ // src/core/knowledge/graph-parser.ts
41824
+ var GraphParser = class _GraphParser {
41825
+ /** Heading marker used for the auto-generated backlinks section */
41826
+ static BACKLINKS_HEADING = "## \u{1F517} Backlinks";
41827
+ // Maps note name -> set of target note names it links to
41828
+ forwardLinks = /* @__PURE__ */ new Map();
41829
+ // Maps note name -> set of source note names that link to it
41830
+ backlinks = /* @__PURE__ */ new Map();
41831
+ // Maps note name -> file path
41832
+ noteToPath = /* @__PURE__ */ new Map();
41833
+ // Maps file path -> note name
41834
+ pathToNote = /* @__PURE__ */ new Map();
41835
+ /**
41836
+ * Resolve file path or relative link to a note name (basename without extension).
41837
+ */
41838
+ getNoteName(filePath) {
41839
+ const basename = filePath.split(/[/\\]/).pop() || "";
41840
+ const dotIdx = basename.lastIndexOf(".");
41841
+ return dotIdx !== -1 ? basename.slice(0, dotIdx) : basename;
41842
+ }
41843
+ /**
41844
+ * Parse content and extract all unique referenced target note names.
41845
+ */
41846
+ parseLinks(content) {
41847
+ const targets = [];
41848
+ const wikiRegex = /\[\[([^[\]|#]+)(?:\|[^[\]]+)?(?:#[^[\]]+)?\]\]/g;
41849
+ let match;
41850
+ while ((match = wikiRegex.exec(content)) !== null) {
41851
+ const target = match[1].trim();
41852
+ if (target && !targets.includes(target)) {
41853
+ targets.push(target);
41854
+ }
41855
+ }
41856
+ const mdLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
41857
+ while ((match = mdLinkRegex.exec(content)) !== null) {
41858
+ const url3 = match[2].trim();
41859
+ if (!url3.includes("://") && (url3.endsWith(".md") || url3.startsWith(".") || url3.startsWith("/"))) {
41860
+ const targetName = this.getNoteName(url3);
41861
+ if (targetName && !targets.includes(targetName)) {
41862
+ targets.push(targetName);
41863
+ }
41864
+ }
41865
+ }
41866
+ return targets;
41867
+ }
41868
+ /**
41869
+ * Index file and register its bi-directional links.
41870
+ */
41871
+ indexFile(filePath, content) {
41872
+ const sourceNote = this.getNoteName(filePath);
41873
+ this.noteToPath.set(sourceNote, filePath);
41874
+ this.pathToNote.set(filePath, sourceNote);
41875
+ this.clearIndexForNote(sourceNote);
41876
+ const targets = this.parseLinks(content);
41877
+ if (targets.length > 0) {
41878
+ const forwardSet = new Set(targets);
41879
+ this.forwardLinks.set(sourceNote, forwardSet);
41880
+ for (const target of targets) {
41881
+ let backSet = this.backlinks.get(target);
41882
+ if (!backSet) {
41883
+ backSet = /* @__PURE__ */ new Set();
41884
+ this.backlinks.set(target, backSet);
41885
+ }
41886
+ backSet.add(sourceNote);
41887
+ }
41888
+ }
41889
+ }
41890
+ /**
41891
+ * Retrieve backlinks for a note name or file path.
41892
+ */
41893
+ getBacklinks(noteOrPath) {
41894
+ const note = noteOrPath.includes("/") || noteOrPath.endsWith(".md") ? this.getNoteName(noteOrPath) : noteOrPath;
41895
+ const sources = this.backlinks.get(note);
41896
+ return sources ? Array.from(sources).sort() : [];
41897
+ }
41898
+ /**
41899
+ * Retrieve forward links for a note name or file path.
41900
+ */
41901
+ getForwardLinks(noteOrPath) {
41902
+ const note = noteOrPath.includes("/") || noteOrPath.endsWith(".md") ? this.getNoteName(noteOrPath) : noteOrPath;
41903
+ const targets = this.forwardLinks.get(note);
41904
+ return targets ? Array.from(targets).sort() : [];
41905
+ }
41906
+ /**
41907
+ * Synchronize the ## 🔗 Backlinks section in a file's content.
41908
+ */
41909
+ syncBacklinksSection(content, backlinksList) {
41910
+ const backlinksHeading = _GraphParser.BACKLINKS_HEADING;
41911
+ const headingRegex = new RegExp(`(?:\\r?\\n|^)${backlinksHeading}\\b[\\s\\S]*$`);
41912
+ let backlinksSectionContent = `
41913
+
41914
+ ${backlinksHeading}
41915
+
41916
+ `;
41917
+ if (backlinksList.length > 0) {
41918
+ backlinksSectionContent += backlinksList.map((b) => `- [[${b}]]`).join("\n");
41919
+ } else {
41920
+ backlinksSectionContent += "*(No backlinks found)*";
41921
+ }
41922
+ if (content.includes(backlinksHeading)) {
41923
+ return content.replace(headingRegex, backlinksSectionContent);
41924
+ } else {
41925
+ return content.trimEnd() + backlinksSectionContent;
41926
+ }
41927
+ }
41928
+ /**
41929
+ * Clear all indexes for a note.
41930
+ */
41931
+ clearIndexForNote(sourceNote) {
41932
+ const targets = this.forwardLinks.get(sourceNote);
41933
+ if (targets) {
41934
+ for (const target of targets) {
41935
+ const backSet = this.backlinks.get(target);
41936
+ if (backSet) {
41937
+ backSet.delete(sourceNote);
41938
+ if (backSet.size === 0) {
41939
+ this.backlinks.delete(target);
41940
+ }
41941
+ }
41942
+ }
41943
+ this.forwardLinks.delete(sourceNote);
41944
+ }
41945
+ }
41946
+ };
41947
+
41948
+ // src/core/knowledge/hybrid-search.ts
41949
+ var RRF_K = 60;
41950
+ var DEFAULT_MAX_RESULTS = 20;
41951
+ var BM25_K1 = 1.2;
41952
+ var BM25_B = 0.75;
41953
+ var GRAPH_HOP_DEPTH = 2;
41954
+ var HybridSearch = class {
41955
+ tagIndexer;
41956
+ graphParser;
41957
+ /** Stores indexed note content keyed by note name. */
41958
+ contentMap = /* @__PURE__ */ new Map();
41959
+ constructor(tagIndexer, graphParser) {
41960
+ this.tagIndexer = tagIndexer;
41961
+ this.graphParser = graphParser;
41962
+ }
41963
+ /**
41964
+ * Register note content for lexical search.
41965
+ * Must be called after TagIndexer.indexFile / GraphParser.indexFile.
41966
+ */
41967
+ indexContent(noteName, content) {
41968
+ this.contentMap.set(noteName, content.toLowerCase());
41969
+ }
41970
+ /**
41971
+ * Fuse lexical, tag, and graph rankings via RRF to produce a single list.
41972
+ */
41973
+ search(query, maxResults) {
41974
+ const limit = maxResults ?? DEFAULT_MAX_RESULTS;
41975
+ const terms = this.tokenize(query);
41976
+ if (terms.length === 0) return [];
41977
+ const lexicalRanked = this.lexicalSearch(terms);
41978
+ const tagRanked = this.tagSearch(terms);
41979
+ const graphRanked = this.graphSearch(terms);
41980
+ return this.fuseResults(lexicalRanked, tagRanked, graphRanked, limit);
41981
+ }
41982
+ /**
41983
+ * BM25-inspired term-frequency scoring across all indexed documents.
41984
+ * Approximates IDF via corpus size and document frequency.
41985
+ */
41986
+ lexicalSearch(terms) {
41987
+ const scores = /* @__PURE__ */ new Map();
41988
+ const corpusSize = this.contentMap.size;
41989
+ if (corpusSize === 0) return [];
41990
+ const avgLen = this.computeAverageLength();
41991
+ for (const term of terms) {
41992
+ const df = this.documentFrequency(term);
41993
+ const idf = Math.log((corpusSize - df + 0.5) / (df + 0.5) + 1);
41994
+ for (const [name, content] of this.contentMap) {
41995
+ const tf = this.countOccurrences(content, term);
41996
+ if (tf === 0) continue;
41997
+ const docLen = content.length;
41998
+ const tfNorm = tf * (BM25_K1 + 1) / (tf + BM25_K1 * (1 - BM25_B + BM25_B * (docLen / avgLen)));
41999
+ const prev = scores.get(name) ?? 0;
42000
+ scores.set(name, prev + idf * tfNorm);
42001
+ }
42002
+ }
42003
+ return this.sortByScore(scores);
42004
+ }
42005
+ /**
42006
+ * Match query terms against the tag index to find tagged notes.
42007
+ */
42008
+ tagSearch(terms) {
42009
+ const scores = /* @__PURE__ */ new Map();
42010
+ for (const term of terms) {
42011
+ const files = this.tagIndexer.getFilesWithTag(term);
42012
+ for (const file3 of files) {
42013
+ const noteName = this.graphParser.getNoteName(file3);
42014
+ const prev = scores.get(noteName) ?? 0;
42015
+ scores.set(noteName, prev + 1);
42016
+ }
42017
+ }
42018
+ return this.sortByScore(scores);
42019
+ }
42020
+ /**
42021
+ * 2-hop graph traversal from tag-matched seed notes to discover related notes.
42022
+ */
42023
+ graphSearch(terms) {
42024
+ const seeds = new Set(this.tagSearch(terms));
42025
+ const visited = /* @__PURE__ */ new Set();
42026
+ const scores = /* @__PURE__ */ new Map();
42027
+ for (const seed of seeds) {
42028
+ this.traverseGraph(seed, GRAPH_HOP_DEPTH, visited, scores);
42029
+ }
42030
+ return this.sortByScore(scores);
42031
+ }
42032
+ /**
42033
+ * Depth-limited BFS from a seed note, scoring neighbors by proximity.
42034
+ */
42035
+ traverseGraph(note, depth, visited, scores) {
42036
+ if (depth <= 0 || visited.has(note)) return;
42037
+ visited.add(note);
42038
+ const neighbors = [
42039
+ ...this.graphParser.getForwardLinks(note),
42040
+ ...this.graphParser.getBacklinks(note)
42041
+ ];
42042
+ for (const neighbor of neighbors) {
42043
+ const prev = scores.get(neighbor) ?? 0;
42044
+ scores.set(neighbor, prev + depth);
42045
+ this.traverseGraph(neighbor, depth - 1, visited, scores);
42046
+ }
42047
+ }
42048
+ /**
42049
+ * Reciprocal Rank Fusion: combine three ranked lists into a single ranking.
42050
+ * Formula: score(d) = Σ 1/(k + rank_i) for each list where d appears.
42051
+ */
42052
+ fuseResults(lexical, tags, graph, limit) {
42053
+ const fused = /* @__PURE__ */ new Map();
42054
+ this.addRrfScores(fused, lexical, "lexical");
42055
+ this.addRrfScores(fused, tags, "tag");
42056
+ this.addRrfScores(fused, graph, "graph");
42057
+ return Array.from(fused.entries()).map(([noteName, { score, matchType }]) => ({ noteName, score, matchType })).sort((a, b) => b.score - a.score).slice(0, limit);
42058
+ }
42059
+ /**
42060
+ * Accumulate RRF scores from a single ranked list into the fused map.
42061
+ * The matchType is set to the source with the highest individual contribution.
42062
+ */
42063
+ addRrfScores(fused, ranked, matchType) {
42064
+ for (let i = 0; i < ranked.length; i++) {
42065
+ const rrfScore = 1 / (RRF_K + i + 1);
42066
+ const existing = fused.get(ranked[i]);
42067
+ if (existing) {
42068
+ existing.score += rrfScore;
42069
+ if (rrfScore > 1 / (RRF_K + 1)) {
42070
+ existing.matchType = matchType;
42071
+ }
42072
+ } else {
42073
+ fused.set(ranked[i], { score: rrfScore, matchType });
42074
+ }
42075
+ }
42076
+ }
42077
+ /** Split query into lowercase tokens for matching. */
42078
+ tokenize(query) {
42079
+ return query.toLowerCase().split(/\s+/).filter(Boolean);
42080
+ }
42081
+ /** Count non-overlapping occurrences of a term in text. */
42082
+ countOccurrences(text, term) {
42083
+ let count = 0;
42084
+ let pos = 0;
42085
+ while ((pos = text.indexOf(term, pos)) !== -1) {
42086
+ count++;
42087
+ pos += term.length;
42088
+ }
42089
+ return count;
42090
+ }
42091
+ /** Average document length across the corpus (character-based). */
42092
+ computeAverageLength() {
42093
+ let total = 0;
42094
+ for (const content of this.contentMap.values()) {
42095
+ total += content.length;
42096
+ }
42097
+ return total / Math.max(this.contentMap.size, 1);
42098
+ }
42099
+ /** Number of documents containing the given term. */
42100
+ documentFrequency(term) {
42101
+ let count = 0;
42102
+ for (const content of this.contentMap.values()) {
42103
+ if (content.includes(term)) count++;
42104
+ }
42105
+ return count;
42106
+ }
42107
+ /** Sort entries by descending score and return the keys in order. */
42108
+ sortByScore(scores) {
42109
+ return Array.from(scores.entries()).sort((a, b) => b[1] - a[1]).map(([name]) => name);
42110
+ }
42111
+ };
42112
+
42113
+ // src/core/knowledge/tag-indexer.ts
42114
+ import { readFileSync as readFileSync4 } from "node:fs";
42115
+ var TagIndexer = class {
42116
+ tagMap = /* @__PURE__ */ new Map();
42117
+ fileCache = /* @__PURE__ */ new Map();
42118
+ /**
42119
+ * Parse frontmatter using regular expressions as a primary safe parser.
42120
+ * This avoids library dependencies and provides deterministic error recovery.
42121
+ */
42122
+ parseFrontmatter(content) {
42123
+ const data = {};
42124
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
42125
+ const match = content.match(frontmatterRegex);
42126
+ if (!match) {
42127
+ return { data, body: content };
42128
+ }
42129
+ const rawYaml = match[1];
42130
+ const body = content.replace(frontmatterRegex, "").trim();
42131
+ const lines = rawYaml.split(/\r?\n/);
42132
+ let activeKey = null;
42133
+ for (const line of lines) {
42134
+ activeKey = this.parseYamlLine(line, data, activeKey);
42135
+ }
42136
+ return { data, body };
42137
+ }
42138
+ /**
42139
+ * Parse a single YAML line and update data object in-place.
42140
+ * Returns the currently active key for block list continuation.
42141
+ */
42142
+ parseYamlLine(line, data, activeKey) {
42143
+ const trimmed = line.trim();
42144
+ if (!trimmed || trimmed.startsWith("#")) return activeKey;
42145
+ const colonIdx = trimmed.indexOf(":");
42146
+ if (colonIdx !== -1) {
42147
+ const key = trimmed.slice(0, colonIdx).trim();
42148
+ const val = trimmed.slice(colonIdx + 1).trim();
42149
+ if (val.startsWith("[") && val.endsWith("]")) {
42150
+ data[key] = val.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
42151
+ } else if (val) {
42152
+ data[key] = this.parseScalar(val);
42153
+ } else {
42154
+ data[key] = [];
42155
+ }
42156
+ return key;
42157
+ }
42158
+ if (trimmed.startsWith("-") && activeKey) {
42159
+ const listVal = trimmed.slice(1).trim();
42160
+ if (listVal) {
42161
+ const currentList = Array.isArray(data[activeKey]) ? data[activeKey] : [];
42162
+ currentList.push(listVal);
42163
+ data[activeKey] = currentList;
42164
+ }
42165
+ }
42166
+ return activeKey;
42167
+ }
42168
+ /**
42169
+ * Index a single markdown file content dynamically.
42170
+ */
42171
+ indexFile(filePath, fileContent) {
42172
+ this.clearIndexForFile(filePath);
42173
+ const { data } = this.parseFrontmatter(fileContent);
42174
+ this.fileCache.set(filePath, data);
42175
+ if (data.tags && Array.isArray(data.tags)) {
42176
+ for (const tag of data.tags) {
42177
+ if (typeof tag === "string") {
42178
+ this.addTagEntry(tag.toLowerCase(), filePath);
42179
+ }
42180
+ }
42181
+ }
42182
+ }
42183
+ /**
42184
+ * Index a file directly from the filesystem.
42185
+ */
42186
+ indexFileFromDisk(filePath) {
42187
+ try {
42188
+ const content = readFileSync4(filePath, "utf8");
42189
+ this.indexFile(filePath, content);
42190
+ } catch {
42191
+ this.clearIndexForFile(filePath);
42192
+ }
42193
+ }
42194
+ /**
42195
+ * Get all files associated with a specific tag O(1).
42196
+ */
42197
+ getFilesWithTag(tag) {
42198
+ return this.tagMap.get(tag.toLowerCase()) || /* @__PURE__ */ new Set();
42199
+ }
42200
+ /**
42201
+ * Get intersection of files containing all specified tags.
42202
+ */
42203
+ getFilesWithAllTags(tags) {
42204
+ if (tags.length === 0) return /* @__PURE__ */ new Set();
42205
+ let result = new Set(this.getFilesWithTag(tags[0]));
42206
+ for (let i = 1; i < tags.length; i++) {
42207
+ const currentSet = this.getFilesWithTag(tags[i]);
42208
+ result = new Set([...result].filter((x) => currentSet.has(x)));
42209
+ }
42210
+ return result;
42211
+ }
42212
+ /**
42213
+ * Get union of files containing any of the specified tags.
42214
+ */
42215
+ getFilesWithAnyTags(tags) {
42216
+ const result = /* @__PURE__ */ new Set();
42217
+ for (const tag of tags) {
42218
+ const files = this.getFilesWithTag(tag);
42219
+ for (const file3 of files) {
42220
+ result.add(file3);
42221
+ }
42222
+ }
42223
+ return result;
42224
+ }
42225
+ /**
42226
+ * Get the cached frontmatter metadata of a file.
42227
+ */
42228
+ getMetadata(filePath) {
42229
+ return this.fileCache.get(filePath);
42230
+ }
42231
+ /**
42232
+ * Return all file paths that have been indexed.
42233
+ */
42234
+ getIndexedFiles() {
42235
+ return Array.from(this.fileCache.keys());
42236
+ }
42237
+ /**
42238
+ * Return all distinct tags currently present across all indexed files.
42239
+ */
42240
+ getAllTags() {
42241
+ return Array.from(this.tagMap.keys());
42242
+ }
42243
+ /**
42244
+ * Remove file references cleanly from tag mappings and metadata cache.
42245
+ */
42246
+ clearIndexForFile(filePath) {
42247
+ this.fileCache.delete(filePath);
42248
+ for (const [tag, files] of this.tagMap.entries()) {
42249
+ if (files.has(filePath)) {
42250
+ files.delete(filePath);
42251
+ if (files.size === 0) {
42252
+ this.tagMap.delete(tag);
42253
+ }
42254
+ }
42255
+ }
42256
+ }
42257
+ /**
42258
+ * Helper to insert tags atomically.
42259
+ */
42260
+ addTagEntry(tag, filePath) {
42261
+ let files = this.tagMap.get(tag);
42262
+ if (!files) {
42263
+ files = /* @__PURE__ */ new Set();
42264
+ this.tagMap.set(tag, files);
42265
+ }
42266
+ files.add(filePath);
42267
+ }
42268
+ /**
42269
+ * Basic scalar value parser for YAML frontmatter fields.
42270
+ */
42271
+ parseScalar(val) {
42272
+ if (val === "true") return true;
42273
+ if (val === "false") return false;
42274
+ const num = Number(val);
42275
+ if (!isNaN(num)) return num;
42276
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
42277
+ return val.slice(1, -1);
42278
+ }
42279
+ return val;
42280
+ }
42281
+ };
42282
+
42283
+ // src/core/knowledge/context-provider.ts
42284
+ var MAX_RESULTS = 3;
42285
+ var MAX_SNIPPET_CHARS = 220;
42286
+ var KNOWLEDGE_ROOTS = ["docs", path10.join(".opencode", "docs")];
42287
+ var SKIP_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", "dist", "bin", ".git", "archive"]);
42288
+ var KnowledgeContextProvider = class {
42289
+ buildPrompt(directory, query) {
42290
+ const normalizedQuery = query.trim();
42291
+ if (!normalizedQuery) return null;
42292
+ const markdownFiles = this.collectMarkdownFiles(directory);
42293
+ if (markdownFiles.length === 0) return null;
42294
+ const indexed = this.indexKnowledge(directory, markdownFiles);
42295
+ const results = indexed.search.search(normalizedQuery, MAX_RESULTS);
42296
+ if (results.length === 0) return null;
42297
+ return this.formatPrompt(normalizedQuery, results, indexed);
42298
+ }
42299
+ collectMarkdownFiles(directory) {
42300
+ const files = [];
42301
+ for (const root of KNOWLEDGE_ROOTS) {
42302
+ const fullRoot = path10.join(directory, root);
42303
+ if (!existsSync9(fullRoot)) continue;
42304
+ files.push(...this.walkDirectory(fullRoot));
42305
+ }
42306
+ return files.sort();
42307
+ }
42308
+ walkDirectory(directory) {
42309
+ const files = [];
42310
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
42311
+ const fullPath = path10.join(directory, entry.name);
42312
+ if (entry.isDirectory()) {
42313
+ if (SKIP_SEGMENTS.has(entry.name)) continue;
42314
+ files.push(...this.walkDirectory(fullPath));
42315
+ continue;
42316
+ }
42317
+ if (entry.isFile() && entry.name.endsWith(".md")) {
42318
+ files.push(fullPath);
42319
+ }
42320
+ }
42321
+ return files;
42322
+ }
42323
+ indexKnowledge(directory, files) {
42324
+ const tagIndexer = new TagIndexer();
42325
+ const graphParser = new GraphParser();
42326
+ const search = new HybridSearch(tagIndexer, graphParser);
42327
+ const noteToPath = /* @__PURE__ */ new Map();
42328
+ const noteToSnippet = /* @__PURE__ */ new Map();
42329
+ for (const filePath of files) {
42330
+ try {
42331
+ const content = readFileSync5(filePath, "utf8");
42332
+ const noteName = graphParser.getNoteName(filePath);
42333
+ const { body } = tagIndexer.parseFrontmatter(content);
42334
+ const normalizedBody = body.trim();
42335
+ tagIndexer.indexFile(filePath, content);
42336
+ graphParser.indexFile(filePath, content);
42337
+ search.indexContent(noteName, normalizedBody);
42338
+ noteToPath.set(noteName, path10.relative(directory, filePath) || filePath);
42339
+ noteToSnippet.set(noteName, this.buildSnippet(normalizedBody));
42340
+ } catch {
42341
+ continue;
42342
+ }
42343
+ }
42344
+ return { search, noteToPath, noteToSnippet };
42345
+ }
42346
+ buildSnippet(content) {
42347
+ const normalized = content.replace(/\s+/g, " ").trim();
42348
+ if (normalized.length <= MAX_SNIPPET_CHARS) return normalized;
42349
+ return `${normalized.slice(0, MAX_SNIPPET_CHARS)}...`;
42350
+ }
42351
+ formatPrompt(query, results, indexed) {
42352
+ const lines = [
42353
+ "<knowledge_rag_context>",
42354
+ `Query: ${query}`,
42355
+ "Repository knowledge matches:",
42356
+ ""
42357
+ ];
42358
+ for (const [index, result] of results.entries()) {
42359
+ const filePath = indexed.noteToPath.get(result.noteName) ?? result.noteName;
42360
+ const snippet = indexed.noteToSnippet.get(result.noteName) ?? "";
42361
+ lines.push(`${index + 1}. ${result.noteName} [${result.matchType}]`);
42362
+ lines.push(` Source: ${filePath}`);
42363
+ if (snippet) {
42364
+ lines.push(` Snippet: ${snippet}`);
42365
+ }
42366
+ }
42367
+ lines.push("");
42368
+ lines.push("Use this as supplemental repository memory. Verify against source files before making claims.");
42369
+ lines.push("</knowledge_rag_context>");
42370
+ return lines.join("\n");
42371
+ }
42372
+ };
42373
+
42374
+ // src/plugin-handlers/system-transform-handler.ts
42375
+ var knowledgeContextProvider = new KnowledgeContextProvider();
41808
42376
  function createSystemTransformHandler(ctx) {
41809
42377
  const { directory, sessions, state: state2 } = ctx;
41810
42378
  return async (input, output) => {
@@ -41825,6 +42393,14 @@ function createSystemTransformHandler(ctx) {
41825
42393
  if (session?.active) {
41826
42394
  systemAdditions.push(buildActiveSessionPrompt(session.step));
41827
42395
  }
42396
+ const knowledgePrompt = buildKnowledgeContextPrompt(
42397
+ directory,
42398
+ loopState?.prompt,
42399
+ state2.sessions.get(sessionID)?.currentTask
42400
+ );
42401
+ if (knowledgePrompt) {
42402
+ systemAdditions.push(knowledgePrompt);
42403
+ }
41828
42404
  try {
41829
42405
  const manager = ParallelAgentManager.getInstance();
41830
42406
  const tasks = manager.getTasksByParent(sessionID);
@@ -41840,6 +42416,10 @@ function createSystemTransformHandler(ctx) {
41840
42416
  }
41841
42417
  };
41842
42418
  }
42419
+ function buildKnowledgeContextPrompt(directory, missionPrompt, currentTask) {
42420
+ const queryParts = [missionPrompt ?? "", currentTask ?? ""].filter(Boolean);
42421
+ return knowledgeContextProvider.buildPrompt(directory, queryParts.join(" ").trim());
42422
+ }
41843
42423
  function buildMissionLoopSystemPrompt(iteration, maxIterations) {
41844
42424
  return `<orchestrator_mission_loop>
41845
42425
  \u{1F3AF} MISSION LOOP ACTIVE: Iteration ${iteration}/${maxIterations}
@@ -41945,23 +42525,19 @@ var OrchestratorPlugin = async (input) => {
41945
42525
  // -----------------------------------------------------------------
41946
42526
  // tool.execute.before hook - runs before any tool call
41947
42527
  // -----------------------------------------------------------------
41948
- "tool.execute.before": createToolExecuteBeforeHandler(handlerContext),
42528
+ [PLUGIN_HOOKS.TOOL_EXECUTE_BEFORE]: createToolExecuteBeforeHandler(handlerContext),
41949
42529
  // -----------------------------------------------------------------
41950
42530
  // tool.execute.after hook - runs after any tool call completes
41951
42531
  // -----------------------------------------------------------------
41952
42532
  [PLUGIN_HOOKS.TOOL_EXECUTE_AFTER]: createToolExecuteAfterHandler(handlerContext),
41953
42533
  // -----------------------------------------------------------------
41954
- // assistant.done hook - runs when the LLM finishes responding
41955
- // -----------------------------------------------------------------
41956
- [PLUGIN_HOOKS.ASSISTANT_DONE]: createAssistantDoneHandler(handlerContext),
41957
- // -----------------------------------------------------------------
41958
42534
  // experimental.session.compacting hook - preserves mission context during compaction
41959
42535
  // -----------------------------------------------------------------
41960
- "experimental.session.compacting": createSessionCompactingHandler(handlerContext),
42536
+ [PLUGIN_HOOKS.EXPERIMENTAL_SESSION_COMPACTING]: createSessionCompactingHandler(handlerContext),
41961
42537
  // -----------------------------------------------------------------
41962
42538
  // experimental.chat.system.transform hook - dynamic system prompt injection
41963
42539
  // -----------------------------------------------------------------
41964
- "experimental.chat.system.transform": createSystemTransformHandler(handlerContext),
42540
+ [PLUGIN_HOOKS.EXPERIMENTAL_CHAT_SYSTEM_TRANSFORM]: createSystemTransformHandler(handlerContext),
41965
42541
  // -----------------------------------------------------------------
41966
42542
  // shutdown hook - cleanup resources on plugin unload
41967
42543
  // -----------------------------------------------------------------