kibi-cursor 2.7.5 → 2.8.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kibi-cursor",
3
3
  "displayName": "Kibi",
4
- "version": "2.7.5",
4
+ "version": "2.8.0",
5
5
  "description": "Optional Cursor adapter for Kibi. Requires project-local kibi-cli, kibi-mcp, and kibi-core (SWI-Prolog is bundled on Linux and macOS) before the bundled MCP server can start.",
6
6
  "license": "AGPL-3.0-or-later",
7
7
  "author": {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # kibi-cursor
2
2
 
3
+ ## 2.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 00d2871: The Codex, Cursor and ZCode hooks now act on recorded decisions while the work happens, as the Claude Code hooks do.
8
+ - **Firm requirements before the edit.** When an edit changes code that implements a current firm requirement, the pre-edit hook tells the agent to ask the person before an edit that breaks that rule, naming the rule; once per requirement per session. These hosts cannot put the question to the person from a hook (Codex has no "ask", Cursor does not enforce one, and the ZCode adapter stays advisory), so this is a notice to the agent. Set `KIBI_CONFIRM_FIRM_EDITS=0` to turn it off.
9
+ - **Stated rules are captured.** A prompt that reads as a rule ("must", "never", "from now on") gets the note that recording it with `kb_upsert` is part of the change: through a new `UserPromptSubmit` hook in Codex and ZCode, and in Cursor through a new `beforeSubmitPrompt` hook whose note arrives with the next pre-edit message. When a session changed source files and wrote nothing with `kb_upsert`, the stop hook asks once for the decision to be recorded (Codex continues the turn with it; Cursor adds it to the stop follow-up; ZCode adds it to the Stop context).
10
+ - **Codex unlinked-file candidates** are ranked by the latest prompt's words as well as the path's.
11
+
12
+ `kibi-agent-core`: new `kibi-agent-core/decision-guard` entry with the host-neutral pieces (`firmRequirementsTouched`, `firmEditNotice`, `confirmFirmEdits`, the prompt and stop notes, and per-session markers). `kibi-claude` now imports its firm-edit and capture texts from it; behavior is unchanged.
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [00d2871]
17
+ - kibi-agent-core@0.8.0
18
+
19
+ ## 2.7.6
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [b9ec91d]
24
+ - kibi-agent-core@0.7.0
25
+
3
26
  ## 2.7.5
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -122,11 +122,12 @@ agent-plugin/
122
122
  Hooks are warning-only and never replace MCP/CLI behavior:
123
123
 
124
124
  - **sessionStart**: bootstrap reminder when `.kb/manifest.json` is missing.
125
- - **preToolUse**: warns on explicit direct `.kb/**` edits without blocking. Before the first edit of a file whose symbols implement a requirement, it adds the shared Kibi snippet (from `kibi-agent-core/snippets`, the same builder the Claude Code plugin uses): the requirements and their implementing symbols, what the lead requirement must keep true and the decision behind it, the covering tests, and the symbol the edit lands in. A superseded or retired lead requirement gets no "must keep true" or "Decision" line. Once per path per session.
125
+ - **preToolUse**: warns on explicit direct `.kb/**` edits without blocking. Before the first edit of a file whose symbols implement a requirement, it adds the shared Kibi snippet (from `kibi-agent-core/snippets`, the same builder the Claude Code plugin uses): the requirements and their implementing symbols, what the lead requirement must keep true and the decision behind it, the covering tests, and the symbol the edit lands in. A superseded or retired lead requirement gets no "must keep true" or "Decision" line. Once per path per session. When the edited code implements a firm requirement (a rule the person set), the message first tells the agent to ask the person before an edit that breaks it, once per requirement per conversation (Cursor does not enforce a hook's "ask"; `KIBI_CONFIRM_FIRM_EDITS=0` turns this off).
126
+ - **beforeSubmitPrompt**: never blocks a prompt. A prompt that states a rule ("must not", "from now on") leaves a note, delivered with the next pre-edit message, to record that rule with `kb_upsert` as part of the change.
126
127
  - **beforeReadFile** and **postToolUse (Read)**: inject source-linked lookup guidance once per path per session, with the same snippet minus the "must keep true" and "Decision" lines.
127
128
  - **postToolUse** (opt-in telemetry): with `KIBI_DIAGNOSTIC_MODE=1`, appends `interface: "hook"` rows with `host: "cursor"` to `.kb/usage.log` for `kb_*` lookups (MCP or `kibi <route>` in Shell) and for edited files with the requirements they implement, keyed by `conversation_id`. `kibi usage-metrics` reads them for the `lookup_before_first_edit` metric. Nothing is written without the variable.
128
129
  - **postToolUse (Write/Edit)**: inject traceability and freshness guidance once per path per session, including `kb_check({sourceFiles:[...], includeImpactDiagnostics:true, includeWorkingTreeDiff:true})` for meaningful source edits.
129
- - **stop**: emits a single freshness or impact-check follow-up when meaningful paths were **edited** during the turn (reads and search do not count). Plan delivery (`CreatePlan`) stays silent unless that same turn also edited source or mutated the KB.
130
+ - **stop**: emits a single freshness or impact-check follow-up when meaningful paths were **edited** during the turn (reads and search do not count). Plan delivery (`CreatePlan`) stays silent unless that same turn also edited source or mutated the KB. When the conversation changed source files and recorded nothing with `kb_upsert`, the follow-up also asks, once, for the decision behind the change to be recorded.
130
131
 
131
132
  ### What the plugin does not do
132
133
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "kibi",
4
- "version": "2.7.5",
4
+ "version": "2.8.0",
5
5
  "description": "Repo-local, per-branch, queryable knowledge base. Portable Agent Plugin packaging Kibi's Agent Skills and MCP server.",
6
6
  "author": { "name": "Kibi" },
7
7
  "homepage": "https://looted.github.io/kibi/",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kibi-cursor",
3
3
  "displayName": "Kibi",
4
- "version": "2.7.5",
4
+ "version": "2.8.0",
5
5
  "description": "Optional Cursor adapter for Kibi. Requires project-local kibi-cli, kibi-mcp, and kibi-core (SWI-Prolog is bundled on Linux and macOS) before the bundled MCP server can start.",
6
6
  "license": "AGPL-3.0-or-later",
7
7
  "author": {
@@ -1,4 +1,4 @@
1
- export type HookEvent = "sessionStart" | "preToolUse" | "postToolUse" | "beforeReadFile" | "stop";
1
+ export type HookEvent = "sessionStart" | "preToolUse" | "postToolUse" | "beforeReadFile" | "beforeSubmitPrompt" | "stop";
2
2
  export type HookStopStatus = "completed" | "aborted" | "error";
3
3
  export type HookInput = {
4
4
  event: string;
@@ -9,6 +9,8 @@ export type HookInput = {
9
9
  conversationId?: string;
10
10
  filePath?: string;
11
11
  status?: HookStopStatus;
12
+ /** beforeSubmitPrompt: the submitted prompt. */
13
+ prompt?: string;
12
14
  };
13
15
  export declare function parseHookInput(input: unknown): HookInput;
14
16
  export declare function readStdin(): Promise<string>;
@@ -68,6 +68,7 @@ export function parseHookInput(input) {
68
68
  ]);
69
69
  const filePath = readString(input, ["file_path", "filePath", "path"]);
70
70
  const status = parseStopStatus(readString(input, ["status"]));
71
+ const prompt = readString(input, ["prompt"]);
71
72
  const parsed = { event };
72
73
  if (cwd !== undefined) {
73
74
  parsed.cwd = cwd;
@@ -87,6 +88,9 @@ export function parseHookInput(input) {
87
88
  if (filePath !== undefined) {
88
89
  parsed.filePath = filePath;
89
90
  }
91
+ if (prompt !== undefined) {
92
+ parsed.prompt = prompt;
93
+ }
90
94
  if (status !== undefined) {
91
95
  parsed.status = status;
92
96
  }
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  export type CursorHookResult = {
3
+ /** beforeSubmitPrompt only: whether the prompt goes through. */
4
+ continue?: boolean;
3
5
  additional_context?: string;
4
6
  permission?: "allow" | "deny";
5
7
  user_message?: string;
@@ -3,6 +3,7 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { captureReminder, claimCaptureReminder, confirmFirmEdits, firmEditNotice, firmRequirementsTouched, hasDecisionFlag, holdRuleNote, markDecisionFlag, recordRequest, settleFirmRequirements, settledFirmRequirements, takeRuleNote, } from "kibi-agent-core/decision-guard";
6
7
  import { appendHookUsageRows, editTraces, hookTelemetryEnabled, kbUsageTrace, readPackageVersion, } from "kibi-agent-core/hook-usage-log";
7
8
  import { stampKibiWorkspace } from "kibi-agent-core/kb-mcp-tools";
8
9
  import { createEntitySummarizer, editFocus, fileKnowledgeSnippet, implementedRequirementIds, } from "kibi-agent-core/snippets";
@@ -11,7 +12,7 @@ import { parseHookInput, parseStdinJson, readStdin } from "./hook-input.js";
11
12
  import { addDirtyPaths, clearSessionHookState, hasGuidedPath, loadHookState, recordKbMcpTool, recordPlanDelivered, rememberGuidedPath, resolveStateDir, } from "./hook-state.js";
12
13
  import { extractKbMcpToolCall } from "./kb-mcp-tools.js";
13
14
  import { BOOTSTRAP_REMINDER, DIRECT_KB_EDIT_WARNING, interfaceAdvisory, stopFollowupMessage, } from "./messages.js";
14
- import { extractExplicitPathFields, isDirectKbPath, isKbFreshnessRelevantPath, isMeaningfulTrackedPath, toRepoRelativePath, } from "./path-policy.js";
15
+ import { extractExplicitPathFields, isDirectKbPath, isKbFreshnessRelevantPath, isMeaningfulTrackedPath, isSourceImpactRelevantPath, toRepoRelativePath, } from "./path-policy.js";
15
16
  import { getSourceLinkedSymbols, } from "./source-linked-requirements.js";
16
17
  const MAX_LINKED_REQUIREMENTS = 3;
17
18
  /**
@@ -31,6 +32,28 @@ function knowledgeFor(workspaceRoot, linked, surface, focus) {
31
32
  summarize: createEntitySummarizer(workspaceRoot),
32
33
  });
33
34
  }
35
+ // implements REQ-agent-hosts-decision-guard
36
+ /**
37
+ * Cursor does not enforce a hook's "ask", so an edit of code that implements
38
+ * a firm requirement gets a notice telling the agent to ask the person first,
39
+ * once per requirement per conversation.
40
+ */
41
+ function firmNotice(workspaceRoot, linked, focus, session, env) {
42
+ if (!confirmFirmEdits(env))
43
+ return undefined;
44
+ const summarize = createEntitySummarizer(workspaceRoot);
45
+ const { ids, symbol } = firmRequirementsTouched({
46
+ symbols: linked.symbols,
47
+ focus,
48
+ wholeFile: focus === undefined,
49
+ summarize,
50
+ confirmed: settledFirmRequirements(session),
51
+ });
52
+ if (ids.length === 0)
53
+ return undefined;
54
+ settleFirmRequirements(session, ids);
55
+ return firmEditNotice(symbol ? `${symbol.title} in ${linked.relativePath}` : linked.relativePath, ids.map(summarize));
56
+ }
34
57
  let cachedPackageVersion;
35
58
  function packageVersion() {
36
59
  // dist/hook-runner.js sits one level below the package root.
@@ -140,6 +163,10 @@ export async function runHook(rawInput, environment = {}) {
140
163
  const kibiReady = hasKibiConfig(cwd);
141
164
  const workspaceTrusted = environment.workspaceTrusted === true;
142
165
  const initialState = loadHookState(stateDir);
166
+ const session = {
167
+ stateDir,
168
+ sessionId: input.conversationId,
169
+ };
143
170
  switch (input.event) {
144
171
  case "sessionStart":
145
172
  if (!kibiReady) {
@@ -177,23 +204,38 @@ export async function runHook(rawInput, environment = {}) {
177
204
  }
178
205
  const relativePath = toRepoRelativePath(primaryPath, cwd);
179
206
  const state = loadHookState(stateDir);
180
- if (hasGuidedPath(state, "pre-edit", relativePath)) {
181
- return emptyResult();
182
- }
183
207
  const linked = getSourceLinkedSymbols(cwd, primaryPath, stateDir);
184
- const guidance = preEditGuidance(primaryPath, {
185
- cwd,
186
- hasKibi: kibiReady,
187
- mcpState: state.mcpState,
188
- workspaceTrusted,
189
- linkedRequirementIds: implementedRequirementIds(linked.symbols).slice(0, MAX_LINKED_REQUIREMENTS),
190
- knowledge: knowledgeFor(cwd, linked, "edit", editFocus(path.resolve(cwd, primaryPath), input.toolInput)),
191
- });
192
- if (!guidance) {
193
- return emptyResult();
208
+ const focus = editFocus(path.resolve(cwd, primaryPath), input.toolInput);
209
+ const ruleNote = takeRuleNote(session);
210
+ const notice = firmNotice(cwd, linked, focus, session, environment.env ?? process.env);
211
+ const guidance = hasGuidedPath(state, "pre-edit", relativePath)
212
+ ? undefined
213
+ : preEditGuidance(primaryPath, {
214
+ cwd,
215
+ hasKibi: kibiReady,
216
+ mcpState: state.mcpState,
217
+ workspaceTrusted,
218
+ linkedRequirementIds: implementedRequirementIds(linked.symbols).slice(0, MAX_LINKED_REQUIREMENTS),
219
+ knowledge: knowledgeFor(cwd, linked, "edit", focus),
220
+ });
221
+ if (guidance) {
222
+ rememberGuidedPath(stateDir, "pre-edit", relativePath);
223
+ }
224
+ const parts = [ruleNote, notice, guidance].filter((part) => part !== undefined);
225
+ return parts.length > 0
226
+ ? { permission: "allow", agent_message: parts.join("\n\n") }
227
+ : emptyResult();
228
+ }
229
+ // implements REQ-agent-hosts-decision-guard
230
+ case "beforeSubmitPrompt": {
231
+ // Cursor's prompt hook cannot add context: the request's rule note
232
+ // waits for the next pre-edit message.
233
+ if (kibiReady && input.prompt) {
234
+ const rule = recordRequest(session, input.prompt);
235
+ if (rule !== undefined)
236
+ holdRuleNote(session, rule);
194
237
  }
195
- rememberGuidedPath(stateDir, "pre-edit", relativePath);
196
- return { permission: "allow", agent_message: guidance };
238
+ return { continue: true };
197
239
  }
198
240
  case "beforeReadFile": {
199
241
  const primaryPath = resolvePrimaryPath(input);
@@ -249,6 +291,14 @@ export async function runHook(rawInput, environment = {}) {
249
291
  if (dirtyPaths.length > 0) {
250
292
  addDirtyPaths(stateDir, dirtyPaths);
251
293
  }
294
+ if (dirtyPaths.some(isSourceImpactRelevantPath)) {
295
+ markDecisionFlag(session, "source-edited");
296
+ }
297
+ }
298
+ if (kbToolCall?.toolName === "kb_upsert" ||
299
+ kbUsageTrace(input.toolName, input.toolInput)?.kbOperation ===
300
+ "kb_upsert") {
301
+ markDecisionFlag(session, "kb-recorded");
252
302
  }
253
303
  const primaryPath = resolvePrimaryPath(input);
254
304
  if (!primaryPath || !kibiReady) {
@@ -307,7 +357,17 @@ export async function runHook(rawInput, environment = {}) {
307
357
  }
308
358
  return emptyResult();
309
359
  }
310
- const followupMessage = stopFollowupMessage(state);
360
+ // implements REQ-agent-hosts-decision-guard
361
+ // Source changed and nothing was recorded: the follow-up asks for the
362
+ // decision to be recorded, once per conversation.
363
+ const followupMessage = [
364
+ claimCaptureReminder(session)
365
+ ? captureReminder(hasDecisionFlag(session, "rule-prompt"))
366
+ : undefined,
367
+ stopFollowupMessage(state),
368
+ ]
369
+ .filter((part) => part !== undefined)
370
+ .join("\n");
311
371
  if (followupMessage) {
312
372
  clearSessionHookState(stateDir);
313
373
  return { followup_message: followupMessage };
@@ -6,6 +6,11 @@
6
6
  "command": "node dist/hook-runner.js"
7
7
  }
8
8
  ],
9
+ "beforeSubmitPrompt": [
10
+ {
11
+ "command": "node dist/hook-runner.js"
12
+ }
13
+ ],
9
14
  "preToolUse": [
10
15
  {
11
16
  "command": "node dist/hook-runner.js"
package/hooks/hooks.json CHANGED
@@ -6,6 +6,11 @@
6
6
  "command": "node dist/hook-runner.js"
7
7
  }
8
8
  ],
9
+ "beforeSubmitPrompt": [
10
+ {
11
+ "command": "node dist/hook-runner.js"
12
+ }
13
+ ],
9
14
  "preToolUse": [
10
15
  {
11
16
  "command": "node dist/hook-runner.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cursor",
3
- "version": "2.7.5",
3
+ "version": "2.8.0",
4
4
  "description": "Kibi Cursor plugin - thin adapter for Cursor MCP integration and editor hooks",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -59,7 +59,7 @@
59
59
  }
60
60
  },
61
61
  "dependencies": {
62
- "kibi-agent-core": "^0.6.0"
62
+ "kibi-agent-core": "^0.8.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/node": "latest",