kibi-cursor 2.0.0 → 2.1.0

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 (41) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +84 -0
  3. package/README.md +1 -1
  4. package/agent-plugin/plugin.json +1 -1
  5. package/agent-plugin/skills/kibi-usage/SKILL.md +10 -3
  6. package/agent-plugin/skills/kibi-usage/resources/fact-lanes.md +4 -0
  7. package/agent-plugin/skills/kibi-usage/resources/proof.md +45 -0
  8. package/agent-plugin/skills/kibi-usage/resources/relationship-directions.md +35 -17
  9. package/agent-plugin/skills/kibi-usage/resources/source-authoring.md +36 -0
  10. package/agent-plugin/skills/kibi-usage/resources/workflows.md +52 -1
  11. package/bin/launch-kibi-mcp.mjs +8 -1
  12. package/dist/.cursor-plugin/plugin.json +1 -1
  13. package/dist/bin/launch-kibi-mcp.mjs +8 -1
  14. package/dist/guidance.d.ts +11 -0
  15. package/dist/guidance.js +39 -5
  16. package/dist/hook-input.js +7 -4
  17. package/dist/hook-runner.js +36 -6
  18. package/dist/hook-state-storage.js +55 -2
  19. package/dist/hook-state.d.ts +4 -2
  20. package/dist/hook-state.js +30 -7
  21. package/dist/kb-mcp-tools.d.ts +5 -7
  22. package/dist/kb-mcp-tools.js +6 -126
  23. package/dist/messages.js +3 -8
  24. package/dist/path-policy.js +17 -177
  25. package/dist/skills/.canon-hash.json +6 -6
  26. package/dist/skills/kibi-usage/SKILL.md +10 -3
  27. package/dist/skills/kibi-usage/resources/fact-lanes.md +4 -0
  28. package/dist/skills/kibi-usage/resources/proof.md +45 -0
  29. package/dist/skills/kibi-usage/resources/relationship-directions.md +35 -17
  30. package/dist/skills/kibi-usage/resources/source-authoring.md +36 -0
  31. package/dist/skills/kibi-usage/resources/workflows.md +52 -1
  32. package/dist/source-linked-requirements.d.ts +16 -0
  33. package/dist/source-linked-requirements.js +55 -0
  34. package/package.json +4 -1
  35. package/skills/.canon-hash.json +6 -6
  36. package/skills/kibi-usage/SKILL.md +10 -3
  37. package/skills/kibi-usage/resources/fact-lanes.md +4 -0
  38. package/skills/kibi-usage/resources/proof.md +45 -0
  39. package/skills/kibi-usage/resources/relationship-directions.md +35 -17
  40. package/skills/kibi-usage/resources/source-authoring.md +36 -0
  41. package/skills/kibi-usage/resources/workflows.md +52 -1
@@ -3,12 +3,13 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { readGuidance, writeGuidance } from "./guidance.js";
6
+ import { preEditGuidance, readGuidance, writeGuidance } from "./guidance.js";
7
7
  import { parseHookInput, parseStdinJson, readStdin } from "./hook-input.js";
8
8
  import { addDirtyPaths, clearSessionHookState, hasGuidedPath, loadHookState, recordKbMcpTool, recordPlanDelivered, rememberGuidedPath, resolveStateDir, } from "./hook-state.js";
9
9
  import { extractKbMcpToolCall } from "./kb-mcp-tools.js";
10
10
  import { BOOTSTRAP_REMINDER, DIRECT_KB_EDIT_WARNING, interfaceAdvisory, stopFollowupMessage, } from "./messages.js";
11
11
  import { extractExplicitPathFields, isDirectKbPath, isKbFreshnessRelevantPath, isMeaningfulTrackedPath, toRepoRelativePath, } from "./path-policy.js";
12
+ import { getSourceLinkedRequirementIds } from "./source-linked-requirements.js";
12
13
  const editableTools = new Set([
13
14
  "Edit",
14
15
  "MultiEdit",
@@ -32,9 +33,14 @@ function isEditLikeTool(toolName) {
32
33
  return toolName === undefined || editableTools.has(toolName);
33
34
  }
34
35
  function isKnownEditableTool(toolName) {
36
+ // rationale: the undefined check narrows the type for Set<string>.has;
37
+ // at runtime Set.has(undefined) is false, so the result is the same.
38
+ // Stryker disable next-line ConditionalExpression
35
39
  return toolName !== undefined && editableTools.has(toolName);
36
40
  }
37
41
  function isPlanDeliveryTool(toolName) {
42
+ // rationale: same type-narrowing as isKnownEditableTool above.
43
+ // Stryker disable next-line ConditionalExpression
38
44
  return toolName !== undefined && planDeliveryTools.has(toolName);
39
45
  }
40
46
  function isReadLikeTool(toolName) {
@@ -81,7 +87,27 @@ export async function runHook(rawInput, environment = {}) {
81
87
  agent_message: DIRECT_KB_EDIT_WARNING,
82
88
  };
83
89
  }
84
- return emptyResult();
90
+ const primaryPath = resolvePrimaryPath(input);
91
+ if (!primaryPath || !kibiReady || !isKnownEditableTool(input.toolName)) {
92
+ return emptyResult();
93
+ }
94
+ const relativePath = toRepoRelativePath(primaryPath, cwd);
95
+ const state = loadHookState(stateDir);
96
+ if (hasGuidedPath(state, "pre-edit", relativePath)) {
97
+ return emptyResult();
98
+ }
99
+ const guidance = preEditGuidance(primaryPath, {
100
+ cwd,
101
+ hasKibi: kibiReady,
102
+ mcpState: state.mcpState,
103
+ workspaceTrusted,
104
+ linkedRequirementIds: getSourceLinkedRequirementIds(cwd, primaryPath, stateDir),
105
+ });
106
+ if (!guidance) {
107
+ return emptyResult();
108
+ }
109
+ rememberGuidedPath(stateDir, "pre-edit", relativePath);
110
+ return { permission: "allow", agent_message: guidance };
85
111
  }
86
112
  case "beforeReadFile": {
87
113
  const primaryPath = resolvePrimaryPath(input);
@@ -152,9 +178,13 @@ export async function runHook(rawInput, environment = {}) {
152
178
  return { additional_context: guidance };
153
179
  }
154
180
  if (isEditLikeTool(input.toolName)) {
181
+ // rationale: the runner only ever passes "read" or "write"; an empty
182
+ // kind selects the same write bucket.
183
+ // Stryker disable StringLiteral
155
184
  if (hasGuidedPath(state, "write", relativePath)) {
156
185
  return emptyResult();
157
186
  }
187
+ // Stryker restore
158
188
  const guidance = writeGuidance(primaryPath, {
159
189
  cwd,
160
190
  hasKibi: kibiReady,
@@ -164,17 +194,17 @@ export async function runHook(rawInput, environment = {}) {
164
194
  if (!guidance) {
165
195
  return emptyResult();
166
196
  }
197
+ // rationale: same bucket equivalence as the lookup above.
198
+ // Stryker disable StringLiteral
167
199
  rememberGuidedPath(stateDir, "write", relativePath);
200
+ // Stryker restore
168
201
  return { additional_context: guidance };
169
202
  }
170
203
  return emptyResult();
171
204
  }
172
205
  case "stop": {
173
206
  const state = loadHookState(stateDir);
174
- const shouldClearSession = state.dirtyPaths.length > 0 ||
175
- state.kbMutationTools.length > 0 ||
176
- state.kbCheckRun ||
177
- state.planDelivered;
207
+ const shouldClearSession = state.dirtyPaths.length > 0 || state.kbCheckRun || state.planDelivered;
178
208
  if (input.status === "aborted" || input.status === "error") {
179
209
  if (shouldClearSession) {
180
210
  clearSessionHookState(stateDir);
@@ -18,14 +18,23 @@ function lockPath(pluginData) {
18
18
  return path.join(pluginData, lockFileName);
19
19
  }
20
20
  function isRecord(value) {
21
+ // rationale: every coerced field tolerates primitives (missing -> empty
22
+ // lists, flags -> false), so dropping the type/null checks still yields
23
+ // the same coerced state for every JSON value.
24
+ // Stryker disable next-line ConditionalExpression, LogicalOperator
21
25
  return typeof value === "object" && value !== null && !Array.isArray(value);
22
26
  }
23
27
  export function normalizePath(dirtyPath) {
24
28
  return dirtyPath.trim().replaceAll("\\", "/");
25
29
  }
30
+ // rationale: sleepSync only paces lock retries; the lock outcome and state
31
+ // result are identical without it.
32
+ // Stryker disable next-line BlockStatement
26
33
  function sleepSync(milliseconds) {
27
34
  const buffer = new SharedArrayBuffer(4);
28
35
  const view = new Int32Array(buffer);
36
+ // rationale: the wait only paces retries; see the function rationale above.
37
+ // Stryker disable next-line CallExpression
29
38
  Atomics.wait(view, 0, 0, milliseconds);
30
39
  }
31
40
  function uniqueTempPath(pluginData) {
@@ -42,6 +51,7 @@ export function emptyHookState() {
42
51
  mcpState: "unknown",
43
52
  dirtyPaths: [],
44
53
  guidedReadPaths: [],
54
+ guidedPreEditPaths: [],
45
55
  guidedWritePaths: [],
46
56
  kbMutationTools: [],
47
57
  kbCheckRun: false,
@@ -52,17 +62,34 @@ export function emptyHookState() {
52
62
  }
53
63
  function acquireLock(pluginData) {
54
64
  const targetLockPath = lockPath(pluginData);
65
+ // rationale: the extra attempt only re-runs the same EEXIST handling one
66
+ // more time; the eventual lock outcome is unchanged.
67
+ // Stryker disable next-line EqualityOperator
55
68
  for (let attempt = 0; attempt < lockRetryCount; attempt++) {
56
69
  try {
57
70
  return fs.openSync(targetLockPath, "wx");
58
71
  }
59
72
  catch (error) {
60
- if (error instanceof Error &&
73
+ // rationale: fail-fast vs re-entering the retry loop both end with the
74
+ // same undefined result; stale removal and sleepSync only pace it.
75
+ if (
76
+ // Stryker disable next-line ConditionalExpression, LogicalOperator
77
+ error instanceof Error &&
61
78
  "code" in error &&
79
+ // rationale: same convergence argument as the comment above.
80
+ // Stryker disable next-line ConditionalExpression
62
81
  error.code === "EEXIST") {
82
+ // rationale: after removal or a paced retry the lock state converges
83
+ // on the same acquisition outcome.
84
+ // Stryker disable next-line ConditionalExpression, BlockStatement
63
85
  if (removeStaleLock(targetLockPath)) {
86
+ // rationale: an immediate retry after successful removal has the
87
+ // same outcome as a paced retry.
88
+ // Stryker disable next-line BlockStatement
64
89
  continue;
65
90
  }
91
+ // rationale: pacing only; see the acquireLock rationale above.
92
+ // Stryker disable next-line CallExpression
66
93
  sleepSync(lockRetryDelayMs);
67
94
  continue;
68
95
  }
@@ -73,15 +100,28 @@ function acquireLock(pluginData) {
73
100
  }
74
101
  function removeStaleLock(targetLockPath) {
75
102
  let stats;
103
+ // rationale: removeStaleLock only runs after an EEXIST open, so a failed
104
+ // stat cannot change the observable lock outcome within a single thread.
105
+ // Stryker disable BlockStatement, BooleanLiteral
76
106
  try {
77
107
  stats = fs.statSync(targetLockPath);
78
108
  }
79
109
  catch {
80
110
  return false;
81
111
  }
112
+ // Stryker restore
113
+ // rationale: the fresh/stale boundary is a pacing decision, not a state
114
+ // transition; both answers converge on the same eventual update result.
115
+ // Stryker disable next-line EqualityOperator
82
116
  if (Date.now() - stats.mtimeMs < staleLockAgeMs) {
117
+ // rationale: same convergence argument as the fresh/stale comment above.
118
+ // Stryker disable next-line BooleanLiteral
83
119
  return false;
84
120
  }
121
+ // rationale: after a successful unlink the lock is gone either way, and an
122
+ // unlink failure leaves it in place; either answer converges on the same
123
+ // eventual lock outcome.
124
+ // Stryker disable BlockStatement, BooleanLiteral
85
125
  try {
86
126
  fs.unlinkSync(targetLockPath);
87
127
  return true;
@@ -89,6 +129,7 @@ function removeStaleLock(targetLockPath) {
89
129
  catch {
90
130
  return false;
91
131
  }
132
+ // Stryker restore
92
133
  }
93
134
  function releaseLock(pluginData, fileDescriptor) {
94
135
  try {
@@ -109,9 +150,13 @@ function releaseLock(pluginData, fileDescriptor) {
109
150
  }
110
151
  }
111
152
  function coerceHookState(value) {
153
+ // rationale: every coerced field tolerates non-records (missing -> empty
154
+ // lists, flags -> false), so the guard is behaviorally redundant.
155
+ // Stryker disable ConditionalExpression, BlockStatement
112
156
  if (!isRecord(value)) {
113
157
  return emptyHookState();
114
158
  }
159
+ // Stryker restore
115
160
  const strings = (candidate, normalize) => Array.isArray(candidate)
116
161
  ? candidate
117
162
  .filter((entry) => typeof entry === "string")
@@ -120,6 +165,7 @@ function coerceHookState(value) {
120
165
  : [];
121
166
  const dirtyPaths = strings(value.dirtyPaths, normalizePath);
122
167
  const guidedReadPaths = strings(value.guidedReadPaths, normalizePath);
168
+ const guidedPreEditPaths = strings(value.guidedPreEditPaths, normalizePath);
123
169
  const guidedWritePaths = strings(value.guidedWritePaths, normalizePath);
124
170
  const kbMutationTools = strings(value.kbMutationTools, (entry) => entry.trim());
125
171
  const impactCheckedPaths = strings(value.impactCheckedPaths, normalizePath);
@@ -127,6 +173,7 @@ function coerceHookState(value) {
127
173
  mcpState: value.mcpState === "observed" ? "observed" : "unknown",
128
174
  dirtyPaths: [...new Set(dirtyPaths)].slice(-maxDirtyPaths),
129
175
  guidedReadPaths: [...new Set(guidedReadPaths)].slice(-maxGuidedPaths),
176
+ guidedPreEditPaths: [...new Set(guidedPreEditPaths)].slice(-maxGuidedPaths),
130
177
  guidedWritePaths: [...new Set(guidedWritePaths)].slice(-maxGuidedPaths),
131
178
  kbMutationTools: [...new Set(kbMutationTools)].slice(-maxKbMutationTools),
132
179
  kbCheckRun: value.kbCheckRun === true,
@@ -145,11 +192,17 @@ export function resolveStateDir(pluginData, conversationId) {
145
192
  return path.join(os.tmpdir(), "kibi-cursor-hook-state", conversationId.replaceAll(/[^a-zA-Z0-9._-]/g, "_"));
146
193
  }
147
194
  export function loadHookState(stateDir) {
195
+ // rationale: the read below throws for a missing stateDir and the catch
196
+ // returns the same empty state.
197
+ // Stryker disable next-line ConditionalExpression, BlockStatement
148
198
  if (!stateDir) {
149
199
  return emptyHookState();
150
200
  }
151
201
  try {
152
- return coerceHookState(JSON.parse(fs.readFileSync(statePath(stateDir), "utf8")));
202
+ return coerceHookState(
203
+ // rationale: Bun treats the empty encoding as utf8 (verified).
204
+ // Stryker disable next-line StringLiteral
205
+ JSON.parse(fs.readFileSync(statePath(stateDir), "utf8")));
153
206
  }
154
207
  catch {
155
208
  return emptyHookState();
@@ -1,9 +1,11 @@
1
1
  import type { McpState } from "./kb-mcp-tools.js";
2
2
  export { loadHookState, resolveStateDir, saveHookState, updateHookState, } from "./hook-state-storage.js";
3
+ export type GuidedPathKind = "read" | "pre-edit" | "write";
3
4
  export type HookState = {
4
5
  mcpState: McpState;
5
6
  dirtyPaths: string[];
6
7
  guidedReadPaths: string[];
8
+ guidedPreEditPaths: string[];
7
9
  guidedWritePaths: string[];
8
10
  kbMutationTools: string[];
9
11
  kbCheckRun: boolean;
@@ -12,8 +14,8 @@ export type HookState = {
12
14
  planDelivered: boolean;
13
15
  };
14
16
  export declare function addDirtyPaths(stateDir: string | undefined, dirtyPaths: readonly string[]): HookState;
15
- export declare function rememberGuidedPath(stateDir: string | undefined, kind: "read" | "write", guidedPath: string): HookState;
16
- export declare function hasGuidedPath(state: HookState, kind: "read" | "write", guidedPath: string): boolean;
17
+ export declare function rememberGuidedPath(stateDir: string | undefined, kind: GuidedPathKind, guidedPath: string): HookState;
18
+ export declare function hasGuidedPath(state: HookState, kind: GuidedPathKind, guidedPath: string): boolean;
17
19
  export declare function recordPlanDelivered(stateDir: string | undefined): HookState;
18
20
  export declare function recordKbMcpTool(stateDir: string | undefined, toolName: string, options?: {
19
21
  impactCheckRun?: boolean;
@@ -9,15 +9,20 @@ export function addDirtyPaths(stateDir, dirtyPaths) {
9
9
  export function rememberGuidedPath(stateDir, kind, guidedPath) {
10
10
  return updateHookState(stateDir, (state) => {
11
11
  const normalized = normalizePath(guidedPath);
12
- if (normalized.length === 0) {
13
- return state;
14
- }
15
12
  if (kind === "read") {
16
13
  return {
17
14
  ...state,
18
15
  guidedReadPaths: mergeStringPaths(state.guidedReadPaths, [normalized]),
19
16
  };
20
17
  }
18
+ if (kind === "pre-edit") {
19
+ return {
20
+ ...state,
21
+ guidedPreEditPaths: mergeStringPaths(state.guidedPreEditPaths, [
22
+ normalized,
23
+ ]),
24
+ };
25
+ }
21
26
  return {
22
27
  ...state,
23
28
  guidedWritePaths: mergeStringPaths(state.guidedWritePaths, [normalized]),
@@ -25,15 +30,23 @@ export function rememberGuidedPath(stateDir, kind, guidedPath) {
25
30
  });
26
31
  }
27
32
  export function hasGuidedPath(state, kind, guidedPath) {
28
- const normalized = normalizePath(guidedPath);
29
- const bucket = kind === "read" ? state.guidedReadPaths : state.guidedWritePaths;
30
- return bucket.includes(normalized);
33
+ return guidedBucket(state, kind).includes(normalizePath(guidedPath));
34
+ }
35
+ function guidedBucket(state, kind) {
36
+ if (kind === "read")
37
+ return state.guidedReadPaths;
38
+ if (kind === "pre-edit")
39
+ return state.guidedPreEditPaths;
40
+ return state.guidedWritePaths;
31
41
  }
32
42
  export function recordPlanDelivered(stateDir) {
33
43
  return updateHookState(stateDir, (state) => state.planDelivered ? state : { ...state, planDelivered: true });
34
44
  }
35
45
  export function recordKbMcpTool(stateDir, toolName, options = {}) {
36
46
  const normalized = toolName.trim();
47
+ // rationale: non-check names return the loaded state below without writing,
48
+ // so the blank-name early return is behaviorally redundant.
49
+ // Stryker disable next-line ConditionalExpression, BlockStatement
37
50
  if (normalized.length === 0) {
38
51
  return loadHookState(stateDir);
39
52
  }
@@ -44,14 +57,21 @@ export function recordKbMcpTool(stateDir, toolName, options = {}) {
44
57
  ...observedState,
45
58
  kbCheckRun: true,
46
59
  impactCheckRun: state.impactCheckRun || options.impactCheckRun === true,
47
- impactCheckedPaths: options.impactCheckRun === true
60
+ impactCheckedPaths:
61
+ // rationale: merging an empty list leaves the paths unchanged.
62
+ // Stryker disable ConditionalExpression
63
+ options.impactCheckRun === true
48
64
  ? mergeStringPaths(state.impactCheckedPaths, options.sourceFiles ?? [])
49
65
  : state.impactCheckedPaths,
66
+ // Stryker restore
50
67
  };
51
68
  }
52
69
  if (normalized === "kb_upsert" || normalized === "kb_delete") {
53
70
  return {
54
71
  ...observedState,
72
+ // rationale: only two mutation tool names exist and the list is
73
+ // deduped, so the 20-entry bound can never bind.
74
+ // Stryker disable next-line MethodExpression
55
75
  kbMutationTools: mergeStringPaths(state.kbMutationTools, [
56
76
  normalized,
57
77
  ]).slice(-maxKbMutationTools),
@@ -62,6 +82,9 @@ export function recordKbMcpTool(stateDir, toolName, options = {}) {
62
82
  }
63
83
  export function clearSessionHookState(stateDir) {
64
84
  const clearedState = emptyHookState();
85
+ // rationale: updating an undefined state dir already returns the updater's
86
+ // result computed from empty state.
87
+ // Stryker disable next-line ConditionalExpression, BlockStatement
65
88
  if (!stateDir) {
66
89
  return clearedState;
67
90
  }
@@ -1,10 +1,8 @@
1
- export type KbMcpToolCall = {
2
- toolName: string;
3
- impactCheckRun: boolean;
4
- sourceFiles: string[];
5
- };
6
- export type McpState = "observed" | "unknown";
7
- export type KibiInterface = "mcp" | "cli" | "setup";
1
+ import type { KbMcpToolCall as SharedKbMcpToolCall, KibiInterface as SharedKibiInterface, McpState as SharedMcpState } from "kibi-agent-core/kb-mcp-tools";
2
+ export type KbMcpToolCall = SharedKbMcpToolCall;
3
+ export type McpState = SharedMcpState;
4
+ export type KibiInterface = SharedKibiInterface;
5
+ export declare function canonicalKbToolName(toolName: string | undefined): string | undefined;
8
6
  export declare function resolveKibiInterface(mcpState: McpState, workspaceTrusted: boolean): KibiInterface;
9
7
  export declare function extractKbMcpToolCall(toolName: string | undefined, toolInput: unknown): KbMcpToolCall | undefined;
10
8
  export declare function extractKbMcpToolName(toolName: string | undefined, toolInput: unknown): string | undefined;
@@ -1,134 +1,14 @@
1
1
  // implements REQ-cursor-kibi-plugin-v1
2
- function isRecord(value) {
3
- return typeof value === "object" && value !== null && !Array.isArray(value);
4
- }
5
- function readString(record, keys) {
6
- for (const key of keys) {
7
- const value = record[key];
8
- if (typeof value === "string") {
9
- return value;
10
- }
11
- }
12
- return undefined;
13
- }
14
- function readBoolean(record, keys) {
15
- for (const key of keys) {
16
- const value = record[key];
17
- if (typeof value === "boolean") {
18
- return value;
19
- }
20
- }
21
- return undefined;
22
- }
23
- function readStringArray(record, keys) {
24
- for (const key of keys) {
25
- const value = record[key];
26
- if (!Array.isArray(value)) {
27
- continue;
28
- }
29
- return value.filter((item) => typeof item === "string" && item.length > 0);
30
- }
31
- return [];
32
- }
33
- function readRecord(record, keys) {
34
- for (const key of keys) {
35
- const value = record[key];
36
- if (isRecord(value)) {
37
- return value;
38
- }
39
- }
40
- return undefined;
2
+ import { canonicalKbToolName as canonicalSharedKbToolName, extractKbMcpToolCall as extractSharedKbMcpToolCall, extractKbMcpToolName as extractSharedKbMcpToolName, resolveKibiInterface as resolveSharedKibiInterface, } from "kibi-agent-core/kb-mcp-tools";
3
+ export function canonicalKbToolName(toolName) {
4
+ return canonicalSharedKbToolName(toolName);
41
5
  }
42
6
  export function resolveKibiInterface(mcpState, workspaceTrusted) {
43
- if (mcpState === "observed") {
44
- return "mcp";
45
- }
46
- return workspaceTrusted ? "cli" : "setup";
7
+ return resolveSharedKibiInterface(mcpState, workspaceTrusted);
47
8
  }
48
9
  export function extractKbMcpToolCall(toolName, toolInput) {
49
- const directToolName = toolName?.trim();
50
- let normalizedToolName = directToolName?.startsWith("kb_")
51
- ? directToolName
52
- : undefined;
53
- if (isRecord(toolInput)) {
54
- normalizedToolName ??= readString(toolInput, [
55
- "toolName",
56
- "tool_name",
57
- "name",
58
- ]);
59
- const args = readRecord(toolInput, ["arguments", "args"]);
60
- const payload = args ?? toolInput;
61
- const includeImpactDiagnostics = readBoolean(payload, [
62
- "includeImpactDiagnostics",
63
- "include_impact_diagnostics",
64
- ]);
65
- const includeWorkingTreeDiff = readBoolean(payload, [
66
- "includeWorkingTreeDiff",
67
- "include_working_tree_diff",
68
- ]);
69
- const sourceFiles = readStringArray(payload, [
70
- "sourceFiles",
71
- "source_files",
72
- ]);
73
- if (normalizedToolName?.startsWith("kb_")) {
74
- return {
75
- toolName: normalizedToolName,
76
- impactCheckRun: normalizedToolName === "kb_check" &&
77
- includeImpactDiagnostics === true &&
78
- includeWorkingTreeDiff === true &&
79
- sourceFiles.length > 0,
80
- sourceFiles,
81
- };
82
- }
83
- const nestedArgs = toolInput.arguments ?? toolInput.args;
84
- if (isRecord(nestedArgs)) {
85
- const nestedTool = readString(nestedArgs, [
86
- "toolName",
87
- "tool_name",
88
- "name",
89
- ]);
90
- if (nestedTool?.startsWith("kb_")) {
91
- return { toolName: nestedTool, impactCheckRun: false, sourceFiles: [] };
92
- }
93
- }
94
- }
95
- if (normalizedToolName?.startsWith("kb_")) {
96
- return {
97
- toolName: normalizedToolName,
98
- impactCheckRun: false,
99
- sourceFiles: [],
100
- };
101
- }
102
- return undefined;
10
+ return extractSharedKbMcpToolCall(toolName, toolInput);
103
11
  }
104
12
  export function extractKbMcpToolName(toolName, toolInput) {
105
- const toolCall = extractKbMcpToolCall(toolName, toolInput);
106
- if (toolCall) {
107
- return toolCall.toolName;
108
- }
109
- if (toolName) {
110
- const normalized = toolName.trim();
111
- if (normalized.startsWith("kb_")) {
112
- return normalized;
113
- }
114
- }
115
- if (!isRecord(toolInput)) {
116
- return undefined;
117
- }
118
- const directTool = readString(toolInput, ["toolName", "tool_name", "name"]);
119
- if (directTool?.startsWith("kb_")) {
120
- return directTool;
121
- }
122
- const nestedArgs = toolInput.arguments ?? toolInput.args;
123
- if (isRecord(nestedArgs)) {
124
- const nestedTool = readString(nestedArgs, [
125
- "toolName",
126
- "tool_name",
127
- "name",
128
- ]);
129
- if (nestedTool?.startsWith("kb_")) {
130
- return nestedTool;
131
- }
132
- }
133
- return undefined;
13
+ return extractSharedKbMcpToolName(toolName, toolInput);
134
14
  }
package/dist/messages.js CHANGED
@@ -5,6 +5,9 @@ export const DIRECT_KB_EDIT_WARNING = "Do not read or edit `.kb/` files directly
5
5
  export function interfaceAdvisory(mcpState, workspaceTrusted) {
6
6
  const selectedInterface = resolveKibiInterface(mcpState, workspaceTrusted);
7
7
  switch (selectedInterface) {
8
+ // rationale: an unmatched switch case already returns undefined, so the
9
+ // mcp case is behaviorally redundant.
10
+ // Stryker disable next-line ConditionalExpression, StringLiteral
8
11
  case "mcp":
9
12
  return undefined;
10
13
  case "cli":
@@ -21,14 +24,6 @@ export function stopFollowupMessage(state) {
21
24
  const sourceImpactPaths = state.dirtyPaths.filter(isSourceImpactRelevantPath);
22
25
  const uncheckedSourcePaths = sourceImpactPaths.filter((sourcePath) => !state.impactCheckedPaths.includes(sourcePath));
23
26
  const freshnessPaths = state.dirtyPaths.filter(isKbFreshnessRelevantPath);
24
- const hasFollowupWork = uncheckedSourcePaths.length > 0 ||
25
- (freshnessPaths.length > 0 && !state.kbCheckRun);
26
- if (state.planDelivered && !hasFollowupWork) {
27
- return undefined;
28
- }
29
- if (uncheckedSourcePaths.length > 0 && !state.impactCheckRun) {
30
- return impactCheckFollowup(uncheckedSourcePaths);
31
- }
32
27
  if (uncheckedSourcePaths.length > 0) {
33
28
  return impactCheckFollowup(uncheckedSourcePaths);
34
29
  }