@gajae-code/coding-agent 0.12.5 → 0.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +36 -1
  4. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  6. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  7. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  9. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  10. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  11. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  12. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  13. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  14. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  15. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  16. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  17. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/sdk/broker/broker.d.ts +18 -0
  20. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  21. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  22. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
  23. package/dist/types/sdk/bus/index.d.ts +4 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  27. package/dist/types/session/agent-session.d.ts +5 -4
  28. package/dist/types/session/attempt-record-store.d.ts +19 -0
  29. package/dist/types/session/session-manager.d.ts +1 -0
  30. package/dist/types/session/session-storage.d.ts +8 -0
  31. package/dist/types/task/receipt.d.ts +1 -0
  32. package/dist/types/task/types.d.ts +43 -0
  33. package/dist/types/tools/subagent.d.ts +15 -0
  34. package/dist/types/tools/todo-write.d.ts +3 -0
  35. package/package.json +8 -8
  36. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  37. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  38. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  39. package/src/async/job-manager.ts +430 -46
  40. package/src/config/model-profiles.ts +8 -0
  41. package/src/config/settings-schema.ts +17 -3
  42. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  43. package/src/extensibility/extensions/runner.ts +80 -6
  44. package/src/extensibility/extensions/wrapper.ts +22 -15
  45. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  46. package/src/gjc-runtime/launch-tmux.ts +309 -293
  47. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  48. package/src/gjc-runtime/psmux-detect.ts +52 -6
  49. package/src/gjc-runtime/team-launch.ts +50 -2
  50. package/src/gjc-runtime/team-runtime.ts +716 -251
  51. package/src/gjc-runtime/team-store.ts +25 -7
  52. package/src/gjc-runtime/team-workers.ts +161 -1
  53. package/src/gjc-runtime/tmux-common.ts +16 -4
  54. package/src/gjc-runtime/tmux-gc.ts +37 -5
  55. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  56. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  58. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  59. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  60. package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +39 -18
  63. package/src/modes/acp/acp-agent.ts +27 -13
  64. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  65. package/src/modes/components/plugin-settings.ts +5 -0
  66. package/src/modes/components/settings-selector.ts +4 -0
  67. package/src/modes/controllers/selector-controller.ts +1 -0
  68. package/src/prompts/tools/monitor.md +4 -4
  69. package/src/sdk/broker/broker.ts +93 -10
  70. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  71. package/src/sdk/broker/lifecycle.ts +691 -92
  72. package/src/sdk/bus/chat-daemon-control.ts +30 -6
  73. package/src/sdk/bus/index.ts +23 -6
  74. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  75. package/src/sdk/bus/telegram-daemon.ts +283 -44
  76. package/src/sdk/bus/topic-registry.ts +133 -10
  77. package/src/sdk/session.ts +7 -6
  78. package/src/session/agent-session.ts +752 -339
  79. package/src/session/attempt-record-store.ts +106 -0
  80. package/src/session/internal/managed-session-scope.ts +252 -67
  81. package/src/session/internal/managed-session-storage.ts +1 -0
  82. package/src/session/session-manager.ts +7 -0
  83. package/src/session/session-storage.ts +146 -24
  84. package/src/task/index.ts +230 -19
  85. package/src/task/provider-retry-status.ts +4 -1
  86. package/src/task/receipt.ts +2 -0
  87. package/src/task/types.ts +10 -0
  88. package/src/tools/monitor.ts +7 -1
  89. package/src/tools/output-meta.ts +10 -2
  90. package/src/tools/subagent.ts +97 -75
  91. package/src/tools/todo-write.ts +47 -0
@@ -92,6 +92,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
92
92
  critic: "openai-codex/gpt-5.6-sol:max",
93
93
  architect: "openai-codex/gpt-5.6-sol:xhigh",
94
94
  }),
95
+ profile("lunamaxxing", ["openai-codex"], {
96
+ default: "openai-codex/gpt-5.6-luna:medium",
97
+ executor: "openai-codex/gpt-5.6-luna:xhigh",
98
+ planner: "openai-codex/gpt-5.6-luna:max",
99
+ critic: "openai-codex/gpt-5.6-luna:max",
100
+ architect: "openai-codex/gpt-5.6-luna:max",
101
+ }),
95
102
  profile("opencodego", ["opencode-go"], {
96
103
  default: "opencode-go/kimi-k2.6",
97
104
  executor: "opencode-go/deepseek-v4-flash",
@@ -327,6 +334,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
327
334
  "codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
328
335
  "codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
329
336
  "codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
337
+ lunamaxxing: { displayName: "LunaMaxxing", providerGroup: "CODEX" },
330
338
  opencodego: { displayName: "OpenCodeGo", providerGroup: "OPENCODEGO" },
331
339
  "claude-opus": { displayName: "Claude Opus", providerGroup: "CLAUDE" },
332
340
  "claude-fable": { displayName: "Claude Fable", providerGroup: "CLAUDE" },
@@ -3744,6 +3744,11 @@ function schemaPaths(value: Record<string, unknown>, prefix = ""): string[] {
3744
3744
  return paths;
3745
3745
  }
3746
3746
 
3747
+ function validArraySettingValue(value: unknown, allowedValues: readonly string[] | undefined): boolean {
3748
+ if (!Array.isArray(value)) return false;
3749
+ return !allowedValues || value.every(item => typeof item === "string" && allowedValues.includes(item));
3750
+ }
3751
+
3747
3752
  function validSettingValue(definition: (typeof SETTINGS_SCHEMA)[SettingPath], value: unknown): boolean {
3748
3753
  return (
3749
3754
  (definition.type === "boolean" && typeof value === "boolean") ||
@@ -3755,7 +3760,8 @@ function validSettingValue(definition: (typeof SETTINGS_SCHEMA)[SettingPath], va
3755
3760
  (definition.type === "enum" &&
3756
3761
  typeof value === "string" &&
3757
3762
  (definition.values as readonly string[]).includes(value)) ||
3758
- (definition.type === "array" && Array.isArray(value)) ||
3763
+ (definition.type === "array" &&
3764
+ validArraySettingValue(value, "items" in definition ? definition.items?.enum : undefined)) ||
3759
3765
  (definition.type === "record" && !!value && typeof value === "object" && !Array.isArray(value))
3760
3766
  );
3761
3767
  }
@@ -3792,8 +3798,16 @@ export function reconcileSettingsSchema(raw: Record<string, unknown>): {
3792
3798
  schemaSetAtPath(settings, path, next);
3793
3799
  issues.push({ path, kind: "coerced", detail: `Coerced ${typeof value} to ${definition.type}.` });
3794
3800
  }
3795
- if (!validSettingValue(definition, next))
3796
- issues.push({ path, kind: "invalid", detail: `Expected ${definition.type}.` });
3801
+ if (!validSettingValue(definition, next)) {
3802
+ const arrayItemEnum =
3803
+ definition.type === "array" && Array.isArray(next) && "items" in definition
3804
+ ? definition.items?.enum
3805
+ : undefined;
3806
+ const detail = arrayItemEnum
3807
+ ? `Expected array items to be one of: ${arrayItemEnum.join(", ")}.`
3808
+ : `Expected ${definition.type}.`;
3809
+ issues.push({ path, kind: "invalid", detail });
3810
+ }
3797
3811
  if (
3798
3812
  definition.type === "record" &&
3799
3813
  "valueSchema" in definition &&
@@ -314,6 +314,8 @@ One generation freezes the change set and reviews it exactly once:
314
314
  ```sh
315
315
  gjc ultragoal record-review-blockers --goal-id <id> --title "Resolve verification blockers" --objective "<blocker-resolution objective>" --evidence "<joined cohort findings>"
316
316
  ```
317
+
318
+ Review-blocker recursion cap (#3613): `record-review-blockers` dedups identical-objective blockers (same trimmed objective + same blocked goal + open status) and bounds the number of unresolved review_blocker descents per blocked goal to **3**. Descents 1..3 may exist; an attempt to create a 4th throws a typed `review_blocker_recursion_cap` terminal handoff (CLI exit 1, operator-visible marker) — never silently auto-completing findings. When the cap fires, record a human pause/escalation or resolve existing blockers before recording more.
317
319
  10. One consolidated fix batch produces exactly **one new generation**. Re-freeze the fixed source as a new `sourceHash`, bump `reviewGeneration`, and set `deltaOnly: true` with `priorGenerationSourceHash` and the `deltaPaths` actually changed. Generation 2+ reviews are **delta-only**: they may not pull in unrelated scope without an explicit `scopeExpansion` carrying `severity`, `novelty`, and `justification`. Repeat until a generation joins clean.
318
320
  11. Only after a generation joins clean, checkpoint the story as complete with a structured quality gate. The terminal critic runs **once** on that final joined generation; when `criticReview.sourceHash` is present it must match the cohort's `sourceHash`. The checkpoint creates a receipt in `ledger.jsonl`; `goals.json.status` alone is not proof. In aggregate mode, the final aggregate receipt must exist before the agent calls `goal({"op":"complete"})` to reconcile the inline UX goal state.
319
321
 
@@ -2,12 +2,20 @@
2
2
  * Extension runner - executes extensions and manages their lifecycle.
3
3
  */
4
4
  import type { AgentMessage } from "@gajae-code/agent-core";
5
- import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMetadata } from "@gajae-code/ai";
5
+ import type { AttemptScope } from "@gajae-code/agent-core/attempt-scope";
6
+ import type {
7
+ AttemptScopeRef,
8
+ CredentialDisabledEvent,
9
+ ImageContent,
10
+ Model,
11
+ ProviderResponseMetadata,
12
+ } from "@gajae-code/ai";
6
13
  import type { KeyId } from "@gajae-code/tui";
7
14
  import { logger } from "@gajae-code/utils";
8
15
  import type { ModelRegistry } from "../../config/model-registry";
9
16
  import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/workflow-gate-broker";
10
17
  import { type Theme, theme } from "../../modes/theme/theme";
18
+ import type { AttemptRecordStore } from "../../session/attempt-record-store";
11
19
  import { createReadonlySessionManager, type SessionManager } from "../../session/session-manager";
12
20
  import type {
13
21
  AfterProviderResponseEvent,
@@ -176,6 +184,7 @@ export class ExtensionRunner {
176
184
  #uiContext: ExtensionUIContext;
177
185
  #errorListeners: Set<ExtensionErrorListener> = new Set();
178
186
  #handlersByEvent: Map<string, IndexedHandler[]> = new Map();
187
+ #attemptRecordStore: AttemptRecordStore | undefined;
179
188
 
180
189
  #getModel: () => Model | undefined = () => undefined;
181
190
  #isIdleFn: () => boolean = () => true;
@@ -504,6 +513,27 @@ export class ExtensionRunner {
504
513
  return (this.#handlersByEvent.get(eventType)?.length ?? 0) > 0;
505
514
  }
506
515
 
516
+ setAttemptRecordStore(store: AttemptRecordStore): void {
517
+ this.#attemptRecordStore = store;
518
+ }
519
+
520
+ #markAttemptExecuted(scope: AttemptScopeRef | undefined): void {
521
+ if (scope !== undefined) this.#attemptRecordStore?.markExecuted(scope as AttemptScope);
522
+ }
523
+
524
+ /**
525
+ * Scope-presence guard. When the AttemptScope facility is active but a
526
+ * handler-capable delivery lacks a scope, the handler is still delivered
527
+ * (backward-compatible) but NO mark is recorded. The record stays
528
+ * unknown/missing → `isClean` returns false → admission refuses
529
+ * (fail-closed at the decision point, not at delivery).
530
+ */
531
+ #requireScopeOrFailClosed(_scope: AttemptScopeRef | undefined, _eventLabel: string): void {
532
+ // No throw — handler is delivered (backward-compatible); mark is not
533
+ // recorded when scope is absent. isClean returns false for an
534
+ // unmarked scope → admission refuses (fail-closed at decision point).
535
+ }
536
+
507
537
  getMessageRenderer(customType: string): MessageRenderer | undefined {
508
538
  for (const ext of this.extensions) {
509
539
  const renderer = ext.messageRenderers.get(customType);
@@ -704,15 +734,22 @@ export class ExtensionRunner {
704
734
  async emit<TEvent extends RunnerEmitEvent>(
705
735
  event: TEvent,
706
736
  continueWhile?: () => boolean,
737
+ scope?: AttemptScopeRef,
707
738
  ): Promise<RunnerEmitResult<TEvent>> {
708
739
  const handlers = this.#handlersByEvent.get(event.type) ?? [];
709
740
  if (handlers.length === 0) return undefined as RunnerEmitResult<TEvent>;
741
+ this.#requireScopeOrFailClosed(scope, event.type);
710
742
 
711
743
  const ctx = this.createContext();
712
744
  let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
745
+ let marked = false;
713
746
 
714
747
  for (const { ext, handler } of handlers) {
715
748
  if (continueWhile && !continueWhile()) return result as RunnerEmitResult<TEvent>;
749
+ if (!marked) {
750
+ this.#markAttemptExecuted(scope);
751
+ marked = true;
752
+ }
716
753
  const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
717
754
  if (continueWhile && !continueWhile()) return result as RunnerEmitResult<TEvent>;
718
755
 
@@ -731,15 +768,21 @@ export class ExtensionRunner {
731
768
  return result as RunnerEmitResult<TEvent>;
732
769
  }
733
770
 
734
- async emitToolResult(event: ToolResultEvent): Promise<ToolResultEventResult | undefined> {
771
+ async emitToolResult(event: ToolResultEvent, scope?: AttemptScopeRef): Promise<ToolResultEventResult | undefined> {
735
772
  const handlers = this.#handlersByEvent.get("tool_result") ?? [];
736
773
  if (handlers.length === 0) return undefined;
774
+ this.#requireScopeOrFailClosed(scope, "tool_result");
737
775
 
738
776
  const ctx = this.createContext();
739
777
  const currentEvent: ToolResultEvent = { ...event };
740
778
  let modified = false;
779
+ let marked = false;
741
780
 
742
781
  for (const { ext, handler } of handlers) {
782
+ if (!marked) {
783
+ this.#markAttemptExecuted(scope);
784
+ marked = true;
785
+ }
743
786
  const handlerResult = (await this.#runHandlerWithTimeout(
744
787
  handler,
745
788
  currentEvent,
@@ -772,14 +815,20 @@ export class ExtensionRunner {
772
815
  };
773
816
  }
774
817
 
775
- async emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {
818
+ async emitToolCall(event: ToolCallEvent, scope?: AttemptScopeRef): Promise<ToolCallEventResult | undefined> {
776
819
  const handlers = this.#handlersByEvent.get("tool_call") ?? [];
777
820
  if (handlers.length === 0) return undefined;
821
+ this.#requireScopeOrFailClosed(scope, "tool_call");
778
822
 
779
823
  const ctx = this.createContext();
780
824
  let result: ToolCallEventResult | undefined;
825
+ let marked = false;
781
826
 
782
827
  for (const { ext, handler } of handlers) {
828
+ if (!marked) {
829
+ this.#markAttemptExecuted(scope);
830
+ marked = true;
831
+ }
783
832
  try {
784
833
  const handlerResult = await handler(event, ctx);
785
834
 
@@ -894,9 +943,10 @@ export class ExtensionRunner {
894
943
  return currentText !== text || currentImages !== images ? { text: currentText, images: currentImages } : {};
895
944
  }
896
945
 
897
- async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
946
+ async emitContext(messages: AgentMessage[], scope?: AttemptScopeRef): Promise<AgentMessage[]> {
898
947
  const handlers = this.#handlersByEvent.get("context") ?? [];
899
948
  if (handlers.length === 0) return messages;
949
+ this.#requireScopeOrFailClosed(scope, "context");
900
950
 
901
951
  const ctx = this.createContext();
902
952
  let currentMessages: AgentMessage[];
@@ -908,8 +958,13 @@ export class ExtensionRunner {
908
958
  // return new message arrays rather than mutating in place.
909
959
  currentMessages = [...messages];
910
960
  }
961
+ let marked = false;
911
962
 
912
963
  for (const { ext, handler } of handlers) {
964
+ if (!marked) {
965
+ this.#markAttemptExecuted(scope);
966
+ marked = true;
967
+ }
913
968
  const event: ContextEvent = { type: "context", messages: currentMessages };
914
969
  const handlerResult = await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
915
970
 
@@ -921,14 +976,23 @@ export class ExtensionRunner {
921
976
  return currentMessages;
922
977
  }
923
978
 
924
- async emitBeforeProviderRequest(payload: unknown): Promise<BeforeProviderRequestEventResult> {
979
+ async emitBeforeProviderRequest(
980
+ payload: unknown,
981
+ scope?: AttemptScopeRef,
982
+ ): Promise<BeforeProviderRequestEventResult> {
925
983
  const handlers = this.#handlersByEvent.get("before_provider_request") ?? [];
926
984
  if (handlers.length === 0) return payload;
985
+ this.#requireScopeOrFailClosed(scope, "before_provider_request");
927
986
 
928
987
  const ctx = this.createContext();
929
988
  let currentPayload = payload;
989
+ let marked = false;
930
990
 
931
991
  for (const { ext, handler } of handlers) {
992
+ if (!marked) {
993
+ this.#markAttemptExecuted(scope);
994
+ marked = true;
995
+ }
932
996
  const event: BeforeProviderRequestEvent = {
933
997
  type: "before_provider_request",
934
998
  payload: currentPayload,
@@ -942,13 +1006,23 @@ export class ExtensionRunner {
942
1006
  return currentPayload;
943
1007
  }
944
1008
 
945
- async emitAfterProviderResponse(response: ProviderResponseMetadata, _model?: Model): Promise<void> {
1009
+ async emitAfterProviderResponse(
1010
+ response: ProviderResponseMetadata,
1011
+ _model?: Model,
1012
+ scope?: AttemptScopeRef,
1013
+ ): Promise<void> {
946
1014
  const handlers = this.#handlersByEvent.get("after_provider_response") ?? [];
947
1015
  if (handlers.length === 0) return;
1016
+ this.#requireScopeOrFailClosed(scope, "after_provider_response");
948
1017
 
949
1018
  const ctx = this.createContext();
1019
+ let marked = false;
950
1020
 
951
1021
  for (const { ext, handler } of handlers) {
1022
+ if (!marked) {
1023
+ this.#markAttemptExecuted(scope);
1024
+ marked = true;
1025
+ }
952
1026
  const event: AfterProviderResponseEvent = {
953
1027
  type: "after_provider_response",
954
1028
  status: response.status,
@@ -109,15 +109,19 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
109
109
  onUpdate?: AgentToolUpdateCallback<TDetails, TParameters>,
110
110
  context?: AgentToolContext,
111
111
  ) {
112
+ const scope = context?.attemptScope;
112
113
  // Emit tool_call event - extensions can block execution
113
114
  if (this.runner.hasHandlers("tool_call")) {
114
115
  try {
115
- const callResult = (await this.runner.emitToolCall({
116
- type: "tool_call",
117
- toolName: this.tool.name,
118
- toolCallId,
119
- input: params as Record<string, unknown>,
120
- })) as ToolCallEventResult | undefined;
116
+ const callResult = (await this.runner.emitToolCall(
117
+ {
118
+ type: "tool_call",
119
+ toolName: this.tool.name,
120
+ toolCallId,
121
+ input: params as Record<string, unknown>,
122
+ },
123
+ scope,
124
+ )) as ToolCallEventResult | undefined;
121
125
 
122
126
  if (callResult?.block) {
123
127
  const reason = callResult.reason || "Tool execution was blocked by an extension";
@@ -147,15 +151,18 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
147
151
 
148
152
  // Emit tool_result event - extensions can modify the result and error status
149
153
  if (this.runner.hasHandlers("tool_result")) {
150
- const resultResult = await this.runner.emitToolResult({
151
- type: "tool_result",
152
- toolName: this.tool.name,
153
- toolCallId,
154
- input: params as Record<string, unknown>,
155
- content: result.content,
156
- details: result.details,
157
- isError: !!executionError,
158
- });
154
+ const resultResult = await this.runner.emitToolResult(
155
+ {
156
+ type: "tool_result",
157
+ toolName: this.tool.name,
158
+ toolCallId,
159
+ input: params as Record<string, unknown>,
160
+ content: result.content,
161
+ details: result.details,
162
+ isError: !!executionError,
163
+ },
164
+ scope,
165
+ );
159
166
 
160
167
  if (resultResult) {
161
168
  const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
@@ -132,15 +132,13 @@ function isAllowedByPolicy(
132
132
  return true;
133
133
  }
134
134
  function matchesQuery(candidate: RuntimeSkillDiscoveryCandidate, query: string): boolean {
135
- const normalized = query.trim().toLowerCase();
136
- if (!normalized) return true;
135
+ const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
136
+ if (terms.length === 0) return true;
137
+ if (terms.includes(candidate.name.toLowerCase())) return true;
137
138
  const haystack = [candidate.name, candidate.description, candidate.source, ...(candidate.useWhen ?? [])]
138
139
  .join("\n")
139
140
  .toLowerCase();
140
- return normalized
141
- .split(/\s+/)
142
- .filter(Boolean)
143
- .every(term => haystack.includes(term));
141
+ return terms.every(term => haystack.includes(term));
144
142
  }
145
143
 
146
144
  async function realPathOrSelf(filePath: string): Promise<string> {