opencode-swarm 7.32.1 → 7.32.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/cli/index.js CHANGED
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.32.1",
37
+ version: "7.32.3",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
@@ -17331,8 +17331,32 @@ var init_schema = __esm(() => {
17331
17331
  recall: exports_external.object({
17332
17332
  defaultMaxItems: exports_external.number().int().min(1).max(20).default(8),
17333
17333
  defaultTokenBudget: exports_external.number().int().min(100).max(5000).default(1200),
17334
- minScore: exports_external.number().min(0).max(1).default(0.05)
17335
- }).default({ defaultMaxItems: 8, defaultTokenBudget: 1200, minScore: 0.05 }),
17334
+ minScore: exports_external.number().min(0).max(1).default(0.05),
17335
+ injection: exports_external.object({
17336
+ enabled: exports_external.boolean().default(true),
17337
+ minScore: exports_external.number().min(0).max(1).default(0.25),
17338
+ requireQuerySignal: exports_external.boolean().default(true),
17339
+ maxItems: exports_external.number().int().min(1).max(20).default(6),
17340
+ tokenBudget: exports_external.number().int().min(100).max(5000).default(1000)
17341
+ }).default({
17342
+ enabled: true,
17343
+ minScore: 0.25,
17344
+ requireQuerySignal: true,
17345
+ maxItems: 6,
17346
+ tokenBudget: 1000
17347
+ })
17348
+ }).default({
17349
+ defaultMaxItems: 8,
17350
+ defaultTokenBudget: 1200,
17351
+ minScore: 0.05,
17352
+ injection: {
17353
+ enabled: true,
17354
+ minScore: 0.25,
17355
+ requireQuerySignal: true,
17356
+ maxItems: 6,
17357
+ tokenBudget: 1000
17358
+ }
17359
+ }),
17336
17360
  writes: exports_external.object({
17337
17361
  mode: exports_external.literal("propose").default("propose")
17338
17362
  }).default({ mode: "propose" }),
@@ -510,6 +510,13 @@ export declare const MemoryConfigSchema: z.ZodObject<{
510
510
  defaultMaxItems: z.ZodDefault<z.ZodNumber>;
511
511
  defaultTokenBudget: z.ZodDefault<z.ZodNumber>;
512
512
  minScore: z.ZodDefault<z.ZodNumber>;
513
+ injection: z.ZodDefault<z.ZodObject<{
514
+ enabled: z.ZodDefault<z.ZodBoolean>;
515
+ minScore: z.ZodDefault<z.ZodNumber>;
516
+ requireQuerySignal: z.ZodDefault<z.ZodBoolean>;
517
+ maxItems: z.ZodDefault<z.ZodNumber>;
518
+ tokenBudget: z.ZodDefault<z.ZodNumber>;
519
+ }, z.core.$strip>>;
513
520
  }, z.core.$strip>>;
514
521
  writes: z.ZodDefault<z.ZodObject<{
515
522
  mode: z.ZodDefault<z.ZodLiteral<"propose">>;
@@ -1161,6 +1168,13 @@ export declare const PluginConfigSchema: z.ZodObject<{
1161
1168
  defaultMaxItems: z.ZodDefault<z.ZodNumber>;
1162
1169
  defaultTokenBudget: z.ZodDefault<z.ZodNumber>;
1163
1170
  minScore: z.ZodDefault<z.ZodNumber>;
1171
+ injection: z.ZodDefault<z.ZodObject<{
1172
+ enabled: z.ZodDefault<z.ZodBoolean>;
1173
+ minScore: z.ZodDefault<z.ZodNumber>;
1174
+ requireQuerySignal: z.ZodDefault<z.ZodBoolean>;
1175
+ maxItems: z.ZodDefault<z.ZodNumber>;
1176
+ tokenBudget: z.ZodDefault<z.ZodNumber>;
1177
+ }, z.core.$strip>>;
1164
1178
  }, z.core.$strip>>;
1165
1179
  writes: z.ZodDefault<z.ZodObject<{
1166
1180
  mode: z.ZodDefault<z.ZodLiteral<"propose">>;
@@ -0,0 +1,30 @@
1
+ export interface ParsedCriticResponse {
2
+ verdict: string;
3
+ reasoning: string;
4
+ evidenceChecked: string[];
5
+ antiPatternsDetected: string[];
6
+ escalationNeeded: boolean;
7
+ rawResponse: string;
8
+ }
9
+ interface ParseCriticResponseOptions {
10
+ validVerdicts?: readonly string[];
11
+ onUnknownVerdict?: (value: string) => void;
12
+ }
13
+ /**
14
+ * Parses a structured critic response into a `ParsedCriticResponse`.
15
+ *
16
+ * Expected format (one field per line, value may span multiple lines until the next field):
17
+ * VERDICT: <value>
18
+ * REASONING: <text>
19
+ * EVIDENCE_CHECKED: <comma-separated list or "none">
20
+ * ANTI_PATTERNS_DETECTED: <comma-separated list or "none">
21
+ * ESCALATION_NEEDED: YES | NO
22
+ *
23
+ * @param rawResponse - The raw text response from the critic agent.
24
+ * @param options.validVerdicts - Override the default verdict allowlist. Unknown verdicts
25
+ * (those not in this list) default to `NEEDS_REVISION` and trigger `onUnknownVerdict`.
26
+ * @param options.onUnknownVerdict - Called with the raw verdict string when it is not
27
+ * in `validVerdicts`. Use for logging or metrics. Does not affect parsing outcome.
28
+ */
29
+ export declare function parseCriticResponseFields(rawResponse: string, options?: ParseCriticResponseOptions): ParsedCriticResponse;
30
+ export {};
@@ -1,10 +1,5 @@
1
- export interface FullAutoCriticResult {
2
- verdict: string;
3
- reasoning: string;
4
- evidenceChecked: string[];
5
- antiPatternsDetected: string[];
6
- escalationNeeded: boolean;
7
- rawResponse: string;
1
+ import { type ParsedCriticResponse } from './critic-response-parser';
2
+ export interface FullAutoCriticResult extends ParsedCriticResponse {
8
3
  }
9
4
  export type FullAutoTriggerSource = 'text_pattern' | 'tool_action' | 'cadence' | 'subagent_return' | 'phase_boundary' | 'task_completion' | 'risk';
10
5
  export interface FullAutoOversightEvent {
@@ -8,6 +8,7 @@
8
8
  * and injects the critic's autonomous oversight response when escalation is detected.
9
9
  */
10
10
  import type { PluginConfig } from '../config';
11
+ import { type ParsedCriticResponse } from '../full-auto/critic-response-parser';
11
12
  interface MessageWithParts {
12
13
  info: {
13
14
  role: string;
@@ -24,13 +25,7 @@ interface MessageWithParts {
24
25
  /**
25
26
  * Result from critic dispatch — used to inject verdict into message stream.
26
27
  */
27
- interface CriticDispatchResult {
28
- verdict: string;
29
- reasoning: string;
30
- evidenceChecked: string[];
31
- antiPatternsDetected: string[];
32
- escalationNeeded: boolean;
33
- rawResponse: string;
28
+ interface CriticDispatchResult extends ParsedCriticResponse {
34
29
  }
35
30
  /**
36
31
  * Parses the critic's structured text response into a CriticDispatchResult.
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var package_default;
48
48
  var init_package = __esm(() => {
49
49
  package_default = {
50
50
  name: "opencode-swarm",
51
- version: "7.32.1",
51
+ version: "7.32.3",
52
52
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
53
53
  main: "dist/index.js",
54
54
  types: "dist/index.d.ts",
@@ -15544,8 +15544,32 @@ var init_schema = __esm(() => {
15544
15544
  recall: exports_external.object({
15545
15545
  defaultMaxItems: exports_external.number().int().min(1).max(20).default(8),
15546
15546
  defaultTokenBudget: exports_external.number().int().min(100).max(5000).default(1200),
15547
- minScore: exports_external.number().min(0).max(1).default(0.05)
15548
- }).default({ defaultMaxItems: 8, defaultTokenBudget: 1200, minScore: 0.05 }),
15547
+ minScore: exports_external.number().min(0).max(1).default(0.05),
15548
+ injection: exports_external.object({
15549
+ enabled: exports_external.boolean().default(true),
15550
+ minScore: exports_external.number().min(0).max(1).default(0.25),
15551
+ requireQuerySignal: exports_external.boolean().default(true),
15552
+ maxItems: exports_external.number().int().min(1).max(20).default(6),
15553
+ tokenBudget: exports_external.number().int().min(100).max(5000).default(1000)
15554
+ }).default({
15555
+ enabled: true,
15556
+ minScore: 0.25,
15557
+ requireQuerySignal: true,
15558
+ maxItems: 6,
15559
+ tokenBudget: 1000
15560
+ })
15561
+ }).default({
15562
+ defaultMaxItems: 8,
15563
+ defaultTokenBudget: 1200,
15564
+ minScore: 0.05,
15565
+ injection: {
15566
+ enabled: true,
15567
+ minScore: 0.25,
15568
+ requireQuerySignal: true,
15569
+ maxItems: 6,
15570
+ tokenBudget: 1000
15571
+ }
15572
+ }),
15549
15573
  writes: exports_external.object({
15550
15574
  mode: exports_external.literal("propose").default("propose")
15551
15575
  }).default({ mode: "propose" }),
@@ -85071,11 +85095,18 @@ init_utils2();
85071
85095
  init_file_locks();
85072
85096
  init_state();
85073
85097
  init_logger();
85074
- init_state2();
85075
85098
  import * as fs40 from "node:fs";
85076
85099
  import * as path62 from "node:path";
85077
- var oversightSequenceCounter = 0;
85078
- var VALID_VERDICTS = [
85100
+
85101
+ // src/full-auto/critic-response-parser.ts
85102
+ var KNOWN_FIELDS = new Set([
85103
+ "VERDICT",
85104
+ "REASONING",
85105
+ "EVIDENCE_CHECKED",
85106
+ "ANTI_PATTERNS_DETECTED",
85107
+ "ESCALATION_NEEDED"
85108
+ ]);
85109
+ var DEFAULT_VALID_VERDICTS = [
85079
85110
  "APPROVED",
85080
85111
  "NEEDS_REVISION",
85081
85112
  "REJECTED",
@@ -85085,7 +85116,8 @@ var VALID_VERDICTS = [
85085
85116
  "REPHRASE",
85086
85117
  "PENDING"
85087
85118
  ];
85088
- function parseFullAutoCriticResponse(rawResponse) {
85119
+ function parseCriticResponseFields(rawResponse, options = {}) {
85120
+ const validVerdicts = options.validVerdicts ?? DEFAULT_VALID_VERDICTS;
85089
85121
  const result = {
85090
85122
  verdict: "NEEDS_REVISION",
85091
85123
  reasoning: "",
@@ -85102,10 +85134,10 @@ function parseFullAutoCriticResponse(rawResponse) {
85102
85134
  switch (key) {
85103
85135
  case "VERDICT": {
85104
85136
  const normalized = value.trim().toUpperCase().replace(/[`*]/g, "");
85105
- if (VALID_VERDICTS.includes(normalized)) {
85137
+ if (validVerdicts.includes(normalized)) {
85106
85138
  res.verdict = normalized;
85107
85139
  } else {
85108
- warn(`[full-auto/oversight] Unknown verdict '${value}' — defaulting to NEEDS_REVISION`);
85140
+ options.onUnknownVerdict?.(value);
85109
85141
  res.verdict = "NEEDS_REVISION";
85110
85142
  }
85111
85143
  break;
@@ -85132,13 +85164,7 @@ function parseFullAutoCriticResponse(rawResponse) {
85132
85164
  const colonIndex = line.indexOf(":");
85133
85165
  if (colonIndex !== -1) {
85134
85166
  const key = line.slice(0, colonIndex).trim().toUpperCase();
85135
- if ([
85136
- "VERDICT",
85137
- "REASONING",
85138
- "EVIDENCE_CHECKED",
85139
- "ANTI_PATTERNS_DETECTED",
85140
- "ESCALATION_NEEDED"
85141
- ].includes(key)) {
85167
+ if (KNOWN_FIELDS.has(key)) {
85142
85168
  if (currentKey)
85143
85169
  commitField(result, currentKey, currentValue);
85144
85170
  currentKey = key;
@@ -85156,6 +85182,17 @@ ${line}`;
85156
85182
  commitField(result, currentKey, currentValue);
85157
85183
  return result;
85158
85184
  }
85185
+
85186
+ // src/full-auto/oversight.ts
85187
+ init_state2();
85188
+ var oversightSequenceCounter = 0;
85189
+ function parseFullAutoCriticResponse(rawResponse) {
85190
+ return parseCriticResponseFields(rawResponse, {
85191
+ onUnknownVerdict: (value) => {
85192
+ warn(`[full-auto/oversight] Unknown verdict '${value}' — defaulting to NEEDS_REVISION`);
85193
+ }
85194
+ });
85195
+ }
85159
85196
  function buildOversightPrompt(input) {
85160
85197
  const {
85161
85198
  trigger,
@@ -86674,88 +86711,11 @@ function extractMessageText3(message) {
86674
86711
  `);
86675
86712
  }
86676
86713
  function parseCriticResponse(rawResponse) {
86677
- const result = {
86678
- verdict: "NEEDS_REVISION",
86679
- reasoning: "",
86680
- evidenceChecked: [],
86681
- antiPatternsDetected: [],
86682
- escalationNeeded: false,
86683
- rawResponse
86684
- };
86685
- const lines = rawResponse.split(`
86686
- `);
86687
- let currentKey = "";
86688
- let currentValue = "";
86689
- const commitField = (res, key, value) => {
86690
- switch (key) {
86691
- case "VERDICT": {
86692
- const validVerdicts = [
86693
- "APPROVED",
86694
- "NEEDS_REVISION",
86695
- "REJECTED",
86696
- "BLOCKED",
86697
- "ANSWER",
86698
- "ESCALATE_TO_HUMAN",
86699
- "REPHRASE"
86700
- ];
86701
- const normalized = value.trim().toUpperCase().replace(/[`*]/g, "");
86702
- if (validVerdicts.includes(normalized)) {
86703
- res.verdict = normalized;
86704
- } else {
86705
- warn(`[full-auto-intercept] Unknown verdict '${value}' — defaulting to NEEDS_REVISION`);
86706
- res.verdict = "NEEDS_REVISION";
86707
- }
86708
- break;
86709
- }
86710
- case "REASONING":
86711
- res.reasoning = value.trim();
86712
- break;
86713
- case "EVIDENCE_CHECKED":
86714
- if (value && value !== "none" && value !== '"none"') {
86715
- res.evidenceChecked = value.split(",").map((s) => s.trim()).filter(Boolean);
86716
- }
86717
- break;
86718
- case "ANTI_PATTERNS_DETECTED":
86719
- if (value && value !== "none" && value !== '"none"') {
86720
- res.antiPatternsDetected = value.split(",").map((s) => s.trim()).filter(Boolean);
86721
- }
86722
- break;
86723
- case "ESCALATION_NEEDED":
86724
- res.escalationNeeded = value.trim().toUpperCase() === "YES";
86725
- break;
86726
- }
86727
- };
86728
- for (const line of lines) {
86729
- const colonIndex = line.indexOf(":");
86730
- if (colonIndex !== -1) {
86731
- const key = line.slice(0, colonIndex).trim().toUpperCase();
86732
- if ([
86733
- "VERDICT",
86734
- "REASONING",
86735
- "EVIDENCE_CHECKED",
86736
- "ANTI_PATTERNS_DETECTED",
86737
- "ESCALATION_NEEDED"
86738
- ].includes(key)) {
86739
- if (currentKey) {
86740
- commitField(result, currentKey, currentValue);
86741
- }
86742
- currentKey = key;
86743
- currentValue = line.slice(colonIndex + 1).trim();
86744
- } else {
86745
- currentValue += `
86746
- ${line}`;
86747
- }
86748
- } else {
86749
- if (line.trim()) {
86750
- currentValue += `
86751
- ${line}`;
86752
- }
86714
+ return parseCriticResponseFields(rawResponse, {
86715
+ onUnknownVerdict: (value) => {
86716
+ warn(`[full-auto-intercept] Unknown verdict '${value}' — defaulting to NEEDS_REVISION`);
86753
86717
  }
86754
- }
86755
- if (currentKey) {
86756
- commitField(result, currentKey, currentValue);
86757
- }
86758
- return result;
86718
+ });
86759
86719
  }
86760
86720
  function escalationTypeToInteractionMode(escalationType) {
86761
86721
  return escalationType === "phase_completion" ? "phase_completion" : "question_resolution";
@@ -86789,7 +86749,8 @@ async function writeAutoOversightEvent(directory, architectOutput, criticVerdict
86789
86749
  fs43.appendFileSync(eventsPath, `${JSON.stringify(event)}
86790
86750
  `, "utf-8");
86791
86751
  } catch (writeError) {
86792
- error48(`[full-auto-intercept] Warning: failed to write auto_oversight event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
86752
+ error48(`[full-auto-intercept] Failed to write auto_oversight event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
86753
+ throw writeError;
86793
86754
  } finally {
86794
86755
  if (lockResult?.acquired && lockResult.lock._release) {
86795
86756
  try {
@@ -86974,11 +86935,7 @@ ESCALATION_NEEDED: NO`;
86974
86935
  rawResponse: criticResponse
86975
86936
  };
86976
86937
  }
86977
- try {
86978
- await writeAutoOversightEvent(directory, architectOutput, parsed.verdict, parsed.reasoning, parsed.evidenceChecked, interactionCount, deadlockCount, escalationType);
86979
- } catch (writeError) {
86980
- error48(`[full-auto-intercept] Failed to write auto_oversight event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
86981
- }
86938
+ await writeAutoOversightEvent(directory, architectOutput, parsed.verdict, parsed.reasoning, parsed.evidenceChecked, interactionCount, deadlockCount, escalationType);
86982
86939
  try {
86983
86940
  await mirrorReactiveVerdictToV2({
86984
86941
  directory,
@@ -93958,6 +93915,7 @@ async function writeDelegationEvent(directory, event) {
93958
93915
  `, "utf-8");
93959
93916
  } catch (error93) {
93960
93917
  error48(`[full-auto/delegation] failed to write event: ${error93 instanceof Error ? error93.message : String(error93)}`);
93918
+ throw error93;
93961
93919
  } finally {
93962
93920
  if (lockResult?.acquired && lockResult.lock._release) {
93963
93921
  try {
@@ -97640,7 +97598,14 @@ var DEFAULT_MEMORY_CONFIG = {
97640
97598
  recall: {
97641
97599
  defaultMaxItems: 8,
97642
97600
  defaultTokenBudget: 1200,
97643
- minScore: 0.05
97601
+ minScore: 0.05,
97602
+ injection: {
97603
+ enabled: true,
97604
+ minScore: 0.25,
97605
+ requireQuerySignal: true,
97606
+ maxItems: 6,
97607
+ tokenBudget: 1000
97608
+ }
97644
97609
  },
97645
97610
  writes: {
97646
97611
  mode: "propose"
@@ -97671,7 +97636,11 @@ function resolveMemoryConfig(input) {
97671
97636
  ...input ?? {},
97672
97637
  recall: {
97673
97638
  ...DEFAULT_MEMORY_CONFIG.recall,
97674
- ...input?.recall ?? {}
97639
+ ...input?.recall ?? {},
97640
+ injection: {
97641
+ ...DEFAULT_MEMORY_CONFIG.recall.injection,
97642
+ ...input?.recall?.injection ?? {}
97643
+ }
97675
97644
  },
97676
97645
  writes: {
97677
97646
  ...DEFAULT_MEMORY_CONFIG.writes,
@@ -97953,6 +97922,24 @@ function validateMemoryProposal(proposal) {
97953
97922
  function tokenize(text) {
97954
97923
  return new Set(text.toLowerCase().replace(/[^\w\s-]/g, " ").split(/\s+/).map((token) => token.trim()).filter(Boolean));
97955
97924
  }
97925
+ function normalizeKindText(kind) {
97926
+ return kind.replace(/_/g, " ");
97927
+ }
97928
+ function collectMetadataStrings(metadata2, keys) {
97929
+ const values = [];
97930
+ for (const key of keys) {
97931
+ const value = metadata2[key];
97932
+ if (typeof value === "string")
97933
+ values.push(value);
97934
+ if (Array.isArray(value)) {
97935
+ for (const item of value) {
97936
+ if (typeof item === "string")
97937
+ values.push(item);
97938
+ }
97939
+ }
97940
+ }
97941
+ return values;
97942
+ }
97956
97943
  function overlap(a, b) {
97957
97944
  if (a.size === 0 || b.size === 0)
97958
97945
  return 0;
@@ -97990,37 +97977,101 @@ function sameScope(a, b) {
97990
97977
  function scopeAllowed(recordScope, allowedScopes) {
97991
97978
  return allowedScopes.some((scope) => sameScope(recordScope, scope));
97992
97979
  }
97993
- function scoreMemoryRecord(record3, request) {
97994
- if (!request.includeExpired && isExpired2(record3))
97995
- return null;
97980
+ function scoreMemoryRecordDetailed(record3, request) {
97981
+ if (!request.includeExpired && isExpired2(record3)) {
97982
+ return { item: null, skipReason: "filtered" };
97983
+ }
97996
97984
  if (record3.supersededBy)
97997
- return null;
97998
- if (record3.metadata.deleted === true)
97999
- return null;
98000
- if (!scopeAllowed(record3.scope, request.scopes))
98001
- return null;
98002
- if (request.kinds && !request.kinds.includes(record3.kind))
98003
- return null;
98004
- const queryTokens = tokenize(request.query);
97985
+ return { item: null, skipReason: "filtered" };
97986
+ if (record3.metadata.deleted === true) {
97987
+ return { item: null, skipReason: "filtered" };
97988
+ }
97989
+ if (!scopeAllowed(record3.scope, request.scopes)) {
97990
+ return { item: null, skipReason: "filtered" };
97991
+ }
97992
+ if (request.kinds && !request.kinds.includes(record3.kind)) {
97993
+ return { item: null, skipReason: "filtered" };
97994
+ }
97995
+ const queryTokens = request.mode === "injection" && request.task ? tokenize(request.task) : tokenize(request.query);
98005
97996
  const textTokens = tokenize(record3.text);
98006
97997
  const tagTokens = tokenize(record3.tags.join(" "));
97998
+ const fileTokens = tokenize([
97999
+ record3.source.filePath,
98000
+ ...collectMetadataStrings(record3.metadata, [
98001
+ "file",
98002
+ "filePath",
98003
+ "files",
98004
+ "touchedFiles"
98005
+ ])
98006
+ ].filter((value) => typeof value === "string").join(" "));
98007
+ const symbolTokens = tokenize(collectMetadataStrings(record3.metadata, ["symbol", "symbols"]).join(" "));
98008
+ const kindQueryOverlap = overlap(queryTokens, tokenize(normalizeKindText(record3.kind)));
98007
98009
  const textOverlap = overlap(queryTokens, textTokens);
98008
98010
  const tagOverlap = overlap(queryTokens, tagTokens);
98009
- const score = textOverlap * 0.45 + tagOverlap * 0.2 + scopeSpecificityBoost(record3.scope) * 0.15 + kindProfileBoost(record3.kind, request) * 0.1 + record3.confidence * 0.1;
98011
+ const fileOverlap = overlap(queryTokens, fileTokens);
98012
+ const symbolOverlap = overlap(queryTokens, symbolTokens);
98013
+ const kindMatch = request.kinds?.includes(record3.kind) ?? false;
98014
+ const scopeMatch = scopeAllowed(record3.scope, request.scopes);
98015
+ const hasQuerySignal = textOverlap > 0 || tagOverlap > 0 || fileOverlap > 0 || symbolOverlap > 0 || kindQueryOverlap > 0;
98016
+ if (request.mode === "injection" && request.requireQuerySignal !== false && !hasQuerySignal) {
98017
+ return { item: null, skipReason: "no_signal" };
98018
+ }
98019
+ const score = textOverlap * 0.45 + tagOverlap * 0.2 + fileOverlap * 0.05 + symbolOverlap * 0.05 + scopeSpecificityBoost(record3.scope) * 0.15 + kindProfileBoost(record3.kind, request) * 0.1 + record3.confidence * 0.1;
98010
98020
  const reasonParts = [
98011
98021
  textOverlap > 0 ? `text_overlap=${textOverlap.toFixed(2)}` : null,
98012
98022
  tagOverlap > 0 ? `tag_overlap=${tagOverlap.toFixed(2)}` : null,
98023
+ fileOverlap > 0 ? `file_overlap=${fileOverlap.toFixed(2)}` : null,
98024
+ symbolOverlap > 0 ? `symbol_overlap=${symbolOverlap.toFixed(2)}` : null,
98025
+ kindQueryOverlap > 0 ? `kind_query=${kindQueryOverlap.toFixed(2)}` : null,
98013
98026
  `scope=${record3.scope.type}`,
98014
98027
  `confidence=${record3.confidence.toFixed(2)}`
98015
98028
  ].filter(Boolean);
98016
98029
  return {
98017
- record: record3,
98018
- score,
98019
- reason: reasonParts.join(", ")
98030
+ item: {
98031
+ record: record3,
98032
+ score,
98033
+ reason: reasonParts.join(", "),
98034
+ signals: {
98035
+ textOverlap,
98036
+ tagOverlap,
98037
+ fileOverlap,
98038
+ symbolOverlap,
98039
+ kindMatch,
98040
+ scopeMatch
98041
+ }
98042
+ }
98020
98043
  };
98021
98044
  }
98022
- function scoreMemoryRecords(records, request) {
98023
- return records.map((record3) => scoreMemoryRecord(record3, request)).filter((item) => item !== null).filter((item) => item.score >= (request.minScore ?? 0)).sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
98045
+ function scoreMemoryRecordsWithDiagnostics(records, request) {
98046
+ const minScore = request.minScore ?? 0;
98047
+ const diagnostics = {
98048
+ candidateCount: records.length,
98049
+ preScoredFilteredCount: 0,
98050
+ scoredCount: 0,
98051
+ returnedCount: 0,
98052
+ noSignalCount: 0,
98053
+ belowThresholdCount: 0
98054
+ };
98055
+ const items = [];
98056
+ for (const record3 of records) {
98057
+ const result = scoreMemoryRecordDetailed(record3, request);
98058
+ if (!result.item) {
98059
+ if (result.skipReason === "filtered")
98060
+ diagnostics.preScoredFilteredCount++;
98061
+ if (result.skipReason === "no_signal")
98062
+ diagnostics.noSignalCount++;
98063
+ continue;
98064
+ }
98065
+ diagnostics.scoredCount++;
98066
+ if (result.item.score < minScore) {
98067
+ diagnostics.belowThresholdCount++;
98068
+ continue;
98069
+ }
98070
+ items.push(result.item);
98071
+ }
98072
+ items.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
98073
+ diagnostics.returnedCount = items.length;
98074
+ return { items, diagnostics };
98024
98075
  }
98025
98076
 
98026
98077
  // src/memory/local-jsonl-provider.ts
@@ -98096,13 +98147,23 @@ class LocalJsonlMemoryProvider {
98096
98147
  await this.audit("delete", id, reason);
98097
98148
  }
98098
98149
  async recall(request) {
98150
+ return (await this.recallWithDiagnostics(request)).items;
98151
+ }
98152
+ async recallWithDiagnostics(request) {
98099
98153
  await this.initialize();
98100
98154
  const records = await this.list({
98101
98155
  scopes: request.scopes,
98102
98156
  kinds: request.kinds,
98103
98157
  includeExpired: request.includeExpired
98104
98158
  });
98105
- return scoreMemoryRecords(records, request).slice(0, request.maxItems);
98159
+ const result = scoreMemoryRecordsWithDiagnostics(records, request);
98160
+ return {
98161
+ items: result.items.slice(0, request.maxItems),
98162
+ diagnostics: {
98163
+ ...result.diagnostics,
98164
+ returnedCount: Math.min(result.diagnostics.returnedCount, request.maxItems)
98165
+ }
98166
+ };
98106
98167
  }
98107
98168
  async recordRecallUsage(event) {
98108
98169
  await this.initialize();
@@ -98307,7 +98368,8 @@ function toRecallBundle(input) {
98307
98368
  generatedAt: input.generatedAt,
98308
98369
  items: block.items,
98309
98370
  tokenEstimate: block.tokenEstimate,
98310
- promptBlock: block.promptBlock
98371
+ promptBlock: block.promptBlock,
98372
+ diagnostics: input.diagnostics
98311
98373
  };
98312
98374
  }
98313
98375
 
@@ -98368,20 +98430,29 @@ class MemoryGateway {
98368
98430
  query,
98369
98431
  task: input.task,
98370
98432
  agentRole: this.context.agentRole,
98433
+ mode: input.mode ?? "manual",
98371
98434
  scopes,
98372
98435
  kinds: input.kinds,
98373
98436
  maxItems,
98374
98437
  tokenBudget,
98375
98438
  minScore: input.minScore ?? this.config.recall.minScore,
98439
+ requireQuerySignal: input.requireQuerySignal,
98376
98440
  includeExpired: input.includeExpired
98377
98441
  };
98378
- const results = await this.provider.recall(request);
98442
+ const recallResult = this.provider.recallWithDiagnostics ? await this.provider.recallWithDiagnostics(request) : { items: await this.provider.recall(request) };
98379
98443
  const bundle = toRecallBundle({
98380
98444
  id: createBundleId(query, generatedAt),
98381
98445
  query,
98382
98446
  generatedAt,
98383
- items: results,
98384
- tokenBudget
98447
+ items: recallResult.items,
98448
+ tokenBudget,
98449
+ diagnostics: recallResult.diagnostics ? {
98450
+ injectionSkipReason: input.mode === "injection" ? resolveInjectionSkipReason(recallResult.diagnostics) : undefined,
98451
+ candidateCount: recallResult.diagnostics.candidateCount,
98452
+ preScoredFilteredCount: recallResult.diagnostics.preScoredFilteredCount,
98453
+ noSignalCount: recallResult.diagnostics.noSignalCount,
98454
+ belowThresholdCount: recallResult.diagnostics.belowThresholdCount
98455
+ } : undefined
98385
98456
  });
98386
98457
  await this.provider.recordRecallUsage?.({
98387
98458
  bundleId: bundle.id,
@@ -98569,6 +98640,19 @@ function validateRequestedScopes(requested, allowed) {
98569
98640
  }
98570
98641
  return requested;
98571
98642
  }
98643
+ function resolveInjectionSkipReason(diagnostics) {
98644
+ if (diagnostics.returnedCount > 0)
98645
+ return;
98646
+ if (diagnostics.candidateCount === 0)
98647
+ return "no_results";
98648
+ const signalEligibleCount = diagnostics.candidateCount - diagnostics.preScoredFilteredCount;
98649
+ if (signalEligibleCount > 0 && diagnostics.noSignalCount > 0 && diagnostics.noSignalCount >= signalEligibleCount) {
98650
+ return "no_signal";
98651
+ }
98652
+ if (diagnostics.belowThresholdCount > 0)
98653
+ return "below_threshold";
98654
+ return "no_results";
98655
+ }
98572
98656
  function scopeKey(scope) {
98573
98657
  return JSON.stringify({
98574
98658
  type: scope.type,
@@ -98963,8 +99047,15 @@ async function recallForAgent(input) {
98963
99047
  agentId: input.agentId,
98964
99048
  runId: input.sessionID
98965
99049
  }, { config: input.config });
98966
- if (!gateway.isEnabled())
99050
+ const resolvedConfig = resolveMemoryConfig(input.config);
99051
+ if (!gateway.isEnabled()) {
99052
+ await logInjectionSkipped(input, "disabled");
99053
+ return null;
99054
+ }
99055
+ if (!resolvedConfig.recall.injection.enabled) {
99056
+ await logInjectionSkipped(input, "disabled");
98967
99057
  return null;
99058
+ }
98968
99059
  const scopes = gateway.deriveAllowedScopes();
98969
99060
  const planInput = {
98970
99061
  userGoal: compactText(input.userGoal),
@@ -98975,6 +99066,8 @@ async function recallForAgent(input) {
98975
99066
  touchedFiles: extractTouchedFiles(input.agentTask)
98976
99067
  };
98977
99068
  const plan = buildMemoryRecallPlan(planInput, { scopes });
99069
+ plan.maxItems = resolvedConfig.recall.injection.maxItems;
99070
+ plan.tokenBudget = resolvedConfig.recall.injection.tokenBudget;
98978
99071
  await input.appendRunLog(input.directory, input.sessionID, {
98979
99072
  event: "recall_requested",
98980
99073
  runId: input.sessionID ?? "unknown",
@@ -98990,10 +99083,13 @@ async function recallForAgent(input) {
98990
99083
  const recallInput = {
98991
99084
  query: plan.query,
98992
99085
  task: planInput.agentTask,
99086
+ mode: "injection",
98993
99087
  scopes: plan.scopes,
98994
99088
  kinds: plan.kinds,
98995
99089
  maxItems: plan.maxItems,
98996
- tokenBudget: plan.tokenBudget
99090
+ tokenBudget: plan.tokenBudget,
99091
+ minScore: resolvedConfig.recall.injection.minScore,
99092
+ requireQuerySignal: resolvedConfig.recall.injection.requireQuerySignal
98997
99093
  };
98998
99094
  const bundle = await gateway.recall(recallInput);
98999
99095
  await input.appendRunLog(input.directory, input.sessionID, {
@@ -99006,8 +99102,31 @@ async function recallForAgent(input) {
99006
99102
  scores: bundle.items.map((item) => item.score),
99007
99103
  tokenEstimate: bundle.tokenEstimate
99008
99104
  });
99105
+ if (bundle.items.length === 0) {
99106
+ await logInjectionSkipped(input, bundle.diagnostics?.injectionSkipReason ?? "no_results", bundle);
99107
+ }
99009
99108
  return { bundle, scopes };
99010
99109
  }
99110
+ async function logInjectionSkipped(input, reason, bundle) {
99111
+ await input.appendRunLog(input.directory, input.sessionID, {
99112
+ event: "prompt_injection_skipped",
99113
+ runId: input.sessionID ?? "unknown",
99114
+ agentRole: input.agentRole,
99115
+ agentId: input.agentId,
99116
+ bundleId: bundle?.id,
99117
+ memoryIds: bundle?.items.map((item) => item.record.id),
99118
+ scores: bundle?.items.map((item) => item.score),
99119
+ tokenEstimate: bundle?.tokenEstimate,
99120
+ rejectionReason: reason,
99121
+ metadata: {
99122
+ reason,
99123
+ candidateCount: bundle?.diagnostics?.candidateCount,
99124
+ preScoredFilteredCount: bundle?.diagnostics?.preScoredFilteredCount,
99125
+ noSignalCount: bundle?.diagnostics?.noSignalCount,
99126
+ belowThresholdCount: bundle?.diagnostics?.belowThresholdCount
99127
+ }
99128
+ });
99129
+ }
99011
99130
  function parseTaskToolInput(input) {
99012
99131
  const record3 = input;
99013
99132
  const rawTool = typeof record3.tool === "string" ? record3.tool : undefined;
@@ -105260,7 +105379,7 @@ function verifyFullAutoPhaseApproval(directory, sessionID, phase, config3) {
105260
105379
  };
105261
105380
  }
105262
105381
  const events = files.map((f) => parseEvidence(f)).filter((e) => Boolean(e));
105263
- const phaseBoundary = events.filter((e) => (e.phase === phase || e.phase === undefined) && e.trigger_source === "phase_boundary" && (e.verdict ?? "").toUpperCase() === "APPROVED");
105382
+ const phaseBoundary = events.filter((e) => e.phase === phase && e.trigger_source === "phase_boundary" && (e.verdict ?? "").toUpperCase() === "APPROVED");
105264
105383
  if (phaseBoundary.length === 0) {
105265
105384
  return failClosed ? {
105266
105385
  ok: false,
@@ -114959,6 +115078,10 @@ var swarm_memory_recall = createSwarmTool({
114959
115078
  memory_ids: bundle.items.map((item) => item.record.id),
114960
115079
  total: bundle.items.length,
114961
115080
  token_estimate: bundle.tokenEstimate,
115081
+ signals: bundle.items.map((item) => ({
115082
+ memory_id: item.record.id,
115083
+ ...item.signals
115084
+ })),
114962
115085
  prompt_block: bundle.promptBlock
114963
115086
  }, null, 2);
114964
115087
  }
@@ -7,6 +7,13 @@ export interface MemoryConfig {
7
7
  defaultMaxItems: number;
8
8
  defaultTokenBudget: number;
9
9
  minScore: number;
10
+ injection: {
11
+ enabled: boolean;
12
+ minScore: number;
13
+ requireQuerySignal: boolean;
14
+ maxItems: number;
15
+ tokenBudget: number;
16
+ };
10
17
  };
11
18
  writes: {
12
19
  mode: 'propose';
@@ -1,6 +1,6 @@
1
1
  import { type MemoryConfig } from './config';
2
2
  import type { MemoryProposalStore, MemoryProvider } from './provider';
3
- import type { MemoryContext, MemoryKind, MemoryProposal, MemoryRecord, MemoryScopeRef, MemorySource, RecallBundle } from './types';
3
+ import type { MemoryContext, MemoryKind, MemoryProposal, MemoryRecord, MemoryScopeRef, MemorySource, RecallBundle, RecallMode } from './types';
4
4
  export interface MemoryGatewayOptions {
5
5
  config?: Partial<MemoryConfig>;
6
6
  provider?: MemoryProvider & Partial<MemoryProposalStore>;
@@ -18,11 +18,13 @@ export interface ProposeMemoryInput {
18
18
  export interface RecallMemoryInput {
19
19
  query: string;
20
20
  task?: string;
21
+ mode?: RecallMode;
21
22
  scopes?: MemoryScopeRef[];
22
23
  kinds?: MemoryKind[];
23
24
  maxItems?: number;
24
25
  tokenBudget?: number;
25
26
  minScore?: number;
27
+ requireQuerySignal?: boolean;
26
28
  includeExpired?: boolean;
27
29
  }
28
30
  export declare class MemoryGateway {
@@ -12,4 +12,4 @@ export { findSecrets, redactSecrets } from './redaction';
12
12
  export { MEMORY_RECALL_PROFILES, type MemoryRecallProfile, normalizeMemoryAgentRole, resolveMemoryRecallProfile, } from './role-profiles';
13
13
  export { appendMemoryRunLog, sanitizeRunId } from './run-log';
14
14
  export { computeMemoryContentHash, createBundleId, createMemoryId, createProposalId, isExpired, normalizeMemoryText, validateMemoryProposal, validateMemoryRecordRules, } from './schema';
15
- export type { MemoryContext, MemoryKind, MemoryListFilter, MemoryProposal, MemoryRecord, MemoryScopeRef, MemoryScopeType, RecallBundle, RecallRequest, RecallResultItem, } from './types';
15
+ export type { MemoryContext, MemoryKind, MemoryListFilter, MemoryProposal, MemoryRecord, MemoryScopeRef, MemoryScopeType, RecallBundle, RecallInjectionSkipReason, RecallMode, RecallRequest, RecallResultItem, } from './types';
@@ -1,4 +1,4 @@
1
- import type { MemoryConfig } from './config';
1
+ import { type MemoryConfig } from './config';
2
2
  import type { MemoryGateway, ProposeMemoryInput } from './gateway';
3
3
  import { appendMemoryRunLog } from './run-log';
4
4
  import type { MemoryKind } from './types';
@@ -1,5 +1,6 @@
1
1
  import { type MemoryConfig } from './config';
2
2
  import type { MemoryProposalStore, MemoryProvider, MemoryRecallUsageEvent } from './provider';
3
+ import type { RecallScoringDiagnostics } from './scoring';
3
4
  import type { MemoryListFilter, MemoryProposal, MemoryRecord, RecallRequest, RecallResultItem } from './types';
4
5
  export declare class LocalJsonlMemoryProvider implements MemoryProvider, MemoryProposalStore {
5
6
  readonly name = "local-jsonl";
@@ -15,6 +16,10 @@ export declare class LocalJsonlMemoryProvider implements MemoryProvider, MemoryP
15
16
  get(id: string): Promise<MemoryRecord | null>;
16
17
  delete(id: string, reason?: string): Promise<void>;
17
18
  recall(request: RecallRequest): Promise<RecallResultItem[]>;
19
+ recallWithDiagnostics(request: RecallRequest): Promise<{
20
+ items: RecallResultItem[];
21
+ diagnostics: RecallScoringDiagnostics;
22
+ }>;
18
23
  recordRecallUsage(event: MemoryRecallUsageEvent): Promise<void>;
19
24
  list(filter?: MemoryListFilter): Promise<MemoryRecord[]>;
20
25
  createProposal(proposal: MemoryProposal): Promise<MemoryProposal>;
@@ -10,4 +10,5 @@ export declare function toRecallBundle(input: {
10
10
  generatedAt: string;
11
11
  items: RecallResultItem[];
12
12
  tokenBudget: number;
13
+ diagnostics?: RecallBundle['diagnostics'];
13
14
  }): RecallBundle;
@@ -1,4 +1,9 @@
1
+ import type { RecallScoringDiagnostics } from './scoring';
1
2
  import type { MemoryListFilter, MemoryProposal, MemoryRecord, RecallRequest, RecallResultItem } from './types';
3
+ export interface MemoryRecallResult {
4
+ items: RecallResultItem[];
5
+ diagnostics?: RecallScoringDiagnostics;
6
+ }
2
7
  export interface MemoryRecallUsageEvent {
3
8
  bundleId: string;
4
9
  query: string;
@@ -18,6 +23,7 @@ export interface MemoryProvider {
18
23
  get(id: string): Promise<MemoryRecord | null>;
19
24
  delete(id: string, reason?: string): Promise<void>;
20
25
  recall(request: RecallRequest): Promise<RecallResultItem[]>;
26
+ recallWithDiagnostics?(request: RecallRequest): Promise<MemoryRecallResult>;
21
27
  recordRecallUsage?(event: MemoryRecallUsageEvent): Promise<void>;
22
28
  list(filter: MemoryListFilter): Promise<MemoryRecord[]>;
23
29
  }
@@ -1,4 +1,4 @@
1
- export type MemoryRunLogEventName = 'recall_requested' | 'recall_returned' | 'prompt_injected' | 'proposal_created' | 'proposal_rejected_by_validation';
1
+ export type MemoryRunLogEventName = 'recall_requested' | 'recall_returned' | 'prompt_injection_skipped' | 'prompt_injected' | 'proposal_created' | 'proposal_rejected_by_validation';
2
2
  export interface MemoryRunLogEvent {
3
3
  event: MemoryRunLogEventName;
4
4
  runId: string;
@@ -1,5 +1,17 @@
1
1
  import type { MemoryRecord, MemoryScopeRef, RecallRequest, RecallResultItem } from './types';
2
+ export interface RecallScoringDiagnostics {
3
+ candidateCount: number;
4
+ preScoredFilteredCount: number;
5
+ scoredCount: number;
6
+ returnedCount: number;
7
+ noSignalCount: number;
8
+ belowThresholdCount: number;
9
+ }
2
10
  export declare function sameScope(a: MemoryScopeRef, b: MemoryScopeRef): boolean;
3
11
  export declare function scopeAllowed(recordScope: MemoryScopeRef, allowedScopes: MemoryScopeRef[]): boolean;
4
12
  export declare function scoreMemoryRecord(record: MemoryRecord, request: RecallRequest): RecallResultItem | null;
5
13
  export declare function scoreMemoryRecords(records: MemoryRecord[], request: RecallRequest): RecallResultItem[];
14
+ export declare function scoreMemoryRecordsWithDiagnostics(records: MemoryRecord[], request: RecallRequest): {
15
+ items: RecallResultItem[];
16
+ diagnostics: RecallScoringDiagnostics;
17
+ };
@@ -56,15 +56,19 @@ export interface MemoryProposal {
56
56
  createdAt: string;
57
57
  metadata: Record<string, unknown>;
58
58
  }
59
+ export type RecallMode = 'manual' | 'injection' | 'curator' | 'evaluation';
60
+ export type RecallInjectionSkipReason = 'disabled' | 'no_signal' | 'below_threshold' | 'no_results';
59
61
  export interface RecallRequest {
60
62
  query: string;
61
63
  task?: string;
62
64
  agentRole?: string;
65
+ mode?: RecallMode;
63
66
  scopes: MemoryScopeRef[];
64
67
  kinds?: MemoryKind[];
65
68
  maxItems: number;
66
69
  tokenBudget: number;
67
70
  minScore?: number;
71
+ requireQuerySignal?: boolean;
68
72
  includeExpired?: boolean;
69
73
  includePendingProposals?: boolean;
70
74
  }
@@ -72,6 +76,14 @@ export interface RecallResultItem {
72
76
  record: MemoryRecord;
73
77
  score: number;
74
78
  reason: string;
79
+ signals: {
80
+ textOverlap: number;
81
+ tagOverlap: number;
82
+ fileOverlap?: number;
83
+ symbolOverlap?: number;
84
+ kindMatch: boolean;
85
+ scopeMatch: boolean;
86
+ };
75
87
  }
76
88
  export interface RecallBundle {
77
89
  id: string;
@@ -80,6 +92,13 @@ export interface RecallBundle {
80
92
  items: RecallResultItem[];
81
93
  tokenEstimate: number;
82
94
  promptBlock: string;
95
+ diagnostics?: {
96
+ injectionSkipReason?: RecallInjectionSkipReason;
97
+ candidateCount?: number;
98
+ preScoredFilteredCount?: number;
99
+ noSignalCount?: number;
100
+ belowThresholdCount?: number;
101
+ };
83
102
  }
84
103
  export interface MemoryContext {
85
104
  directory: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.32.1",
3
+ "version": "7.32.3",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",