kibi-cursor 2.10.0 → 2.11.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 (33) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +91 -0
  3. package/README.md +21 -3
  4. package/agent-plugin/plugin.json +1 -1
  5. package/agent-plugin/skills/kibi-commit-review/SKILL.md +6 -2
  6. package/agent-plugin/skills/kibi-decisions/SKILL.md +7 -2
  7. package/agent-plugin/skills/kibi-decisions/resources/signoff-views.md +3 -1
  8. package/agent-plugin/skills/kibi-usage/SKILL.md +14 -3
  9. package/agent-plugin/skills/kibi-usage/resources/prolog-proofs.md +207 -0
  10. package/agent-plugin/skills/kibi-usage/resources/proof.md +44 -3
  11. package/bin/hook-runner.mjs +3233 -0
  12. package/bin/launch-kibi-mcp.mjs +2 -1
  13. package/dist/.cursor-plugin/plugin.json +1 -1
  14. package/dist/bin/hook-runner.mjs +3233 -0
  15. package/dist/bin/launch-kibi-mcp.mjs +2 -1
  16. package/dist/hook-runner.js +245 -200
  17. package/dist/hooks/hooks.json +6 -6
  18. package/dist/skills/.canon-hash.json +6 -5
  19. package/dist/skills/kibi-commit-review/SKILL.md +6 -2
  20. package/dist/skills/kibi-decisions/SKILL.md +7 -2
  21. package/dist/skills/kibi-decisions/resources/signoff-views.md +3 -1
  22. package/dist/skills/kibi-usage/SKILL.md +14 -3
  23. package/dist/skills/kibi-usage/resources/prolog-proofs.md +207 -0
  24. package/dist/skills/kibi-usage/resources/proof.md +44 -3
  25. package/hooks/hooks.json +6 -6
  26. package/package.json +5 -3
  27. package/skills/.canon-hash.json +6 -5
  28. package/skills/kibi-commit-review/SKILL.md +6 -2
  29. package/skills/kibi-decisions/SKILL.md +7 -2
  30. package/skills/kibi-decisions/resources/signoff-views.md +3 -1
  31. package/skills/kibi-usage/SKILL.md +14 -3
  32. package/skills/kibi-usage/resources/prolog-proofs.md +207 -0
  33. package/skills/kibi-usage/resources/proof.md +44 -3
@@ -180,7 +180,8 @@ export function resolveProjectLocalMcp(workspaceRoot) {
180
180
  if (!packageInfo) {
181
181
  throw new Error(
182
182
  `[KIBI-CURSOR] No project-local ${PACKAGE_NAME} package was found for ${root}. ` +
183
- `Install it in that workspace (for example: npm install --save-dev ${PACKAGE_NAME}) and reload Cursor.`,
183
+ `Install it in that workspace (for example: npm install --save-dev ${PACKAGE_NAME}) and reload Cursor. ` +
184
+ `The plugin launcher does not use a global ${PACKAGE_NAME}; to use one, add the manual MCP entry from the kibi-cursor README instead.`,
184
185
  );
185
186
  }
186
187
 
@@ -4,8 +4,9 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { captureReminder, claimCaptureReminder, claimSignoffReminder, confirmFirmEdits, firmEditNotice, firmRequirementsTouched, hasDecisionFlag, holdRuleNote, isRequirementUpsert, markDecisionFlag, recordRequest, settleFirmRequirements, settledFirmRequirements, signoffReminder, takeRuleNote, } from "kibi-agent-core/decision-guard";
7
- import { appendHookUsageRows, editTraces, hookTelemetryEnabled, kbUsageTrace, readPackageVersion, } from "kibi-agent-core/hook-usage-log";
7
+ import { appendHookUsage, appendHookUsageRows, editTraces, finishHookTelemetry, hookTracesWanted, kbUsageTrace, readPackageVersion, } from "kibi-agent-core/hook-usage-log";
8
8
  import { stampKibiWorkspace } from "kibi-agent-core/kb-mcp-tools";
9
+ import { classifyPath } from "kibi-agent-core/path-policy";
9
10
  import { createEntitySummarizer, editFocus, fileKnowledgeSnippet, implementedRequirementIds, } from "kibi-agent-core/snippets";
10
11
  import { preEditGuidance, readGuidance, writeGuidance } from "./guidance.js";
11
12
  import { parseHookInput, parseStdinJson, readStdin } from "./hook-input.js";
@@ -54,7 +55,8 @@ function firmNotice(workspaceRoot, linked, focus, session, env) {
54
55
  return firmEditNotice(symbol ? `${symbol.title} in ${linked.relativePath}` : linked.relativePath, ids.map(summarize));
55
56
  }
56
57
  function packageVersion() {
57
- // dist/hook-runner.js sits one level below the package root. Each hook
58
+ // bin/hook-runner.mjs and dist/hook-runner.js sit one level below the
59
+ // package root. Each hook
58
60
  // event is its own process, so the version is read at most once per event.
59
61
  return readPackageVersion(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
60
62
  }
@@ -68,7 +70,7 @@ function recordToolTelemetry(input, workspaceRoot, stateDir, startedAt, env) {
68
70
  // rationale: appendHookUsageRows applies the same opt-in; this guard only
69
71
  // skips reading the index when telemetry is off.
70
72
  // Stryker disable next-line ConditionalExpression
71
- if (!hookTelemetryEnabled(env))
73
+ if (!hookTracesWanted(env))
72
74
  return;
73
75
  const kbUsage = kbUsageTrace(input.toolName, input.toolInput);
74
76
  if (!kbUsage && !isKnownEditableTool(input.toolName))
@@ -166,157 +168,100 @@ export async function runHook(rawInput, environment = {}) {
166
168
  stateDir,
167
169
  sessionId: input.conversationId,
168
170
  };
169
- switch (input.event) {
170
- case "sessionStart":
171
- if (!kibiReady) {
172
- const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
173
- return {
174
- additional_context: advisory
175
- ? `${BOOTSTRAP_REMINDER}\n${advisory}`
176
- : BOOTSTRAP_REMINDER,
177
- };
178
- }
179
- {
180
- const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
181
- return advisory ? { additional_context: advisory } : emptyResult();
182
- }
183
- case "preToolUse": {
184
- // Name the agent's workspace on every Kibi MCP call so the launcher
185
- // answers from it, also for agents working in a git worktree. The
186
- // per-call cwd wins over workspace_roots, which can be the window's
187
- // folders rather than the agent's checkout.
188
- const stamped = stampKibiWorkspace(input.toolName, input.toolInput, kibiWorkspaceFor(input.cwd ?? input.workspaceRoots?.[0]));
189
- if (stamped) {
190
- return { updated_input: stamped };
191
- }
192
- const explicitPaths = extractExplicitPathFields(input.toolInput);
193
- const hasDirectKbEdit = isEditLikeTool(input.toolName) && explicitPaths.some(isDirectKbPath);
194
- if (hasDirectKbEdit) {
195
- return {
196
- permission: "allow",
197
- agent_message: DIRECT_KB_EDIT_WARNING,
198
- };
199
- }
200
- const primaryPath = resolvePrimaryPath(input);
201
- if (!primaryPath || !kibiReady || !isKnownEditableTool(input.toolName)) {
202
- return emptyResult();
203
- }
204
- const relativePath = toRepoRelativePath(primaryPath, cwd);
205
- const state = loadHookState(stateDir);
206
- const linked = getSourceLinkedSymbols(cwd, primaryPath, stateDir);
207
- const focus = editFocus(path.resolve(cwd, primaryPath), input.toolInput);
208
- const ruleNote = takeRuleNote(session);
209
- const notice = firmNotice(cwd, linked, focus, session, environment.env ?? process.env);
210
- const guidance = hasGuidedPath(state, "pre-edit", relativePath)
211
- ? undefined
212
- : preEditGuidance(primaryPath, {
213
- cwd,
214
- hasKibi: kibiReady,
215
- mcpState: state.mcpState,
216
- workspaceTrusted,
217
- // A file that implements requirements always gets the shared
218
- // snippet, so the bare linked-requirement wording is never needed.
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);
237
- }
238
- return { continue: true };
239
- }
240
- case "beforeReadFile": {
241
- const primaryPath = resolvePrimaryPath(input);
242
- if (!primaryPath || !kibiReady) {
243
- return { permission: "allow" };
244
- }
245
- const relativePath = toRepoRelativePath(primaryPath, cwd);
246
- const state = loadHookState(stateDir);
247
- if (hasGuidedPath(state, "read", relativePath)) {
248
- return { permission: "allow" };
249
- }
250
- const guidance = readGuidance(primaryPath, {
251
- cwd,
252
- hasKibi: kibiReady,
253
- mcpState: state.mcpState,
254
- workspaceTrusted,
255
- knowledge: knowledgeFor(cwd, getSourceLinkedSymbols(cwd, primaryPath, stateDir),
256
- // rationale: the snippet only special-cases "edit", so any other
257
- // surface literal renders the same read snippet.
258
- // Stryker disable next-line StringLiteral
259
- "read"),
260
- });
261
- if (!guidance) {
262
- return { permission: "allow" };
263
- }
264
- rememberGuidedPath(stateDir, "read", relativePath);
265
- return {
266
- permission: "allow",
267
- agent_message: guidance,
268
- };
269
- }
270
- case "postToolUse": {
271
- if (kibiReady) {
272
- try {
273
- recordToolTelemetry(input, cwd, stateDir, startedAt, environment.env);
171
+ const trace = {};
172
+ try {
173
+ switch (input.event) {
174
+ case "sessionStart":
175
+ trace.action = "session_start";
176
+ if (!kibiReady) {
177
+ const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
178
+ return {
179
+ additional_context: advisory
180
+ ? `${BOOTSTRAP_REMINDER}\n${advisory}`
181
+ : BOOTSTRAP_REMINDER,
182
+ };
274
183
  }
275
- catch {
276
- // Telemetry is best effort and must never change the hook's output.
184
+ {
185
+ const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
186
+ return advisory ? { additional_context: advisory } : emptyResult();
277
187
  }
278
- }
279
- const kbToolCall = extractKbMcpToolCall(input.toolName, input.toolInput);
280
- if (kbToolCall) {
281
- recordKbMcpTool(stateDir, kbToolCall.toolName, {
282
- impactCheckRun: kbToolCall.impactCheckRun,
283
- sourceFiles: kbToolCall.sourceFiles,
284
- });
285
- }
286
- if (isPlanDeliveryTool(input.toolName)) {
287
- recordPlanDelivered(stateDir);
288
- }
289
- const explicitPaths = extractExplicitPathFields(input.toolInput);
290
- if (isKnownEditableTool(input.toolName)) {
291
- const dirtyPaths = explicitPaths
292
- .map((candidate) => toRepoRelativePath(candidate, cwd))
293
- .filter((candidate) => isMeaningfulTrackedPath(candidate) ||
294
- isKbFreshnessRelevantPath(candidate));
295
- if (dirtyPaths.length > 0) {
296
- addDirtyPaths(stateDir, dirtyPaths);
188
+ case "preToolUse": {
189
+ // Name the agent's workspace on every Kibi MCP call so the launcher
190
+ // answers from it, also for agents working in a git worktree. The
191
+ // per-call cwd wins over workspace_roots, which can be the window's
192
+ // folders rather than the agent's checkout.
193
+ const stamped = stampKibiWorkspace(input.toolName, input.toolInput, kibiWorkspaceFor(input.cwd ?? input.workspaceRoots?.[0]));
194
+ if (stamped) {
195
+ return { updated_input: stamped };
297
196
  }
298
- if (dirtyPaths.some(isSourceImpactRelevantPath)) {
299
- markDecisionFlag(session, "source-edited");
197
+ const explicitPaths = extractExplicitPathFields(input.toolInput);
198
+ const hasDirectKbEdit = isEditLikeTool(input.toolName) && explicitPaths.some(isDirectKbPath);
199
+ if (hasDirectKbEdit) {
200
+ trace.action = "kb_direct_note";
201
+ trace.pathKind = "kb";
202
+ return {
203
+ permission: "allow",
204
+ agent_message: DIRECT_KB_EDIT_WARNING,
205
+ };
300
206
  }
301
- }
302
- if (kbToolCall?.toolName === "kb_upsert" ||
303
- kbUsageTrace(input.toolName, input.toolInput)?.kbOperation ===
304
- "kb_upsert") {
305
- markDecisionFlag(session, "kb-recorded");
306
- // implements REQ-kibi-signoff-views
307
- if (isRequirementUpsert(input.toolInput)) {
308
- markDecisionFlag(session, "requirement-recorded");
207
+ const primaryPath = resolvePrimaryPath(input);
208
+ if (!primaryPath ||
209
+ !kibiReady ||
210
+ !isKnownEditableTool(input.toolName)) {
211
+ return emptyResult();
309
212
  }
213
+ const relativePath = toRepoRelativePath(primaryPath, cwd);
214
+ const state = loadHookState(stateDir);
215
+ const linked = getSourceLinkedSymbols(cwd, primaryPath, stateDir);
216
+ trace.pathKind = classifyPath(relativePath);
217
+ trace.requirementIds = implementedRequirementIds(linked.symbols);
218
+ const focus = editFocus(path.resolve(cwd, primaryPath), input.toolInput);
219
+ const ruleNote = takeRuleNote(session);
220
+ const notice = firmNotice(cwd, linked, focus, session, environment.env ?? process.env);
221
+ const guidance = hasGuidedPath(state, "pre-edit", relativePath)
222
+ ? undefined
223
+ : preEditGuidance(primaryPath, {
224
+ cwd,
225
+ hasKibi: kibiReady,
226
+ mcpState: state.mcpState,
227
+ workspaceTrusted,
228
+ // A file that implements requirements always gets the shared
229
+ // snippet, so the bare linked-requirement wording is never needed.
230
+ knowledge: knowledgeFor(cwd, linked, "edit", focus),
231
+ });
232
+ if (guidance) {
233
+ rememberGuidedPath(stateDir, "pre-edit", relativePath);
234
+ }
235
+ const parts = [ruleNote, notice, guidance].filter((part) => part !== undefined);
236
+ trace.action = parts.length > 0 ? "edit_snippet" : "edit_silent";
237
+ return parts.length > 0
238
+ ? { permission: "allow", agent_message: parts.join("\n\n") }
239
+ : emptyResult();
310
240
  }
311
- const primaryPath = resolvePrimaryPath(input);
312
- if (!primaryPath || !kibiReady) {
313
- return emptyResult();
241
+ // implements REQ-agent-hosts-decision-guard
242
+ case "beforeSubmitPrompt": {
243
+ // Cursor's prompt hook cannot add context: the request's rule note
244
+ // waits for the next pre-edit message.
245
+ if (kibiReady && input.prompt) {
246
+ const rule = recordRequest(session, input.prompt);
247
+ if (rule !== undefined)
248
+ holdRuleNote(session, rule);
249
+ trace.action =
250
+ rule === undefined ? "prompt_silent" : "prompt_rule_note";
251
+ }
252
+ return { continue: true };
314
253
  }
315
- const relativePath = toRepoRelativePath(primaryPath, cwd);
316
- const state = loadHookState(stateDir);
317
- if (isReadLikeTool(input.toolName)) {
254
+ case "beforeReadFile": {
255
+ const primaryPath = resolvePrimaryPath(input);
256
+ if (!primaryPath || !kibiReady) {
257
+ return { permission: "allow" };
258
+ }
259
+ const relativePath = toRepoRelativePath(primaryPath, cwd);
260
+ trace.pathKind = classifyPath(relativePath);
261
+ trace.action = "read_silent";
262
+ const state = loadHookState(stateDir);
318
263
  if (hasGuidedPath(state, "read", relativePath)) {
319
- return emptyResult();
264
+ return { permission: "allow" };
320
265
  }
321
266
  const guidance = readGuidance(primaryPath, {
322
267
  cwd,
@@ -324,77 +269,177 @@ export async function runHook(rawInput, environment = {}) {
324
269
  mcpState: state.mcpState,
325
270
  workspaceTrusted,
326
271
  knowledge: knowledgeFor(cwd, getSourceLinkedSymbols(cwd, primaryPath, stateDir),
327
- // rationale: as above, only "edit" changes the snippet.
272
+ // rationale: the snippet only special-cases "edit", so any other
273
+ // surface literal renders the same read snippet.
328
274
  // Stryker disable next-line StringLiteral
329
275
  "read"),
330
276
  });
331
277
  if (!guidance) {
332
- return emptyResult();
278
+ return { permission: "allow" };
333
279
  }
334
280
  rememberGuidedPath(stateDir, "read", relativePath);
335
- return { additional_context: guidance };
281
+ trace.action = "read_snippet";
282
+ return {
283
+ permission: "allow",
284
+ agent_message: guidance,
285
+ };
336
286
  }
337
- if (isEditLikeTool(input.toolName)) {
338
- // rationale: the runner only ever passes "read" or "write"; an empty
339
- // kind selects the same write bucket.
340
- // Stryker disable StringLiteral
341
- if (hasGuidedPath(state, "write", relativePath)) {
342
- return emptyResult();
287
+ case "postToolUse": {
288
+ if (kibiReady) {
289
+ try {
290
+ recordToolTelemetry(input, cwd, stateDir, startedAt, environment.env);
291
+ }
292
+ catch {
293
+ // Telemetry is best effort and must never change the hook's output.
294
+ }
343
295
  }
344
- // Stryker restore
345
- const guidance = writeGuidance(primaryPath, {
346
- cwd,
347
- hasKibi: kibiReady,
348
- mcpState: state.mcpState,
349
- workspaceTrusted,
350
- });
351
- if (!guidance) {
296
+ const kbToolCall = extractKbMcpToolCall(input.toolName, input.toolInput);
297
+ if (kbToolCall) {
298
+ recordKbMcpTool(stateDir, kbToolCall.toolName, {
299
+ impactCheckRun: kbToolCall.impactCheckRun,
300
+ sourceFiles: kbToolCall.sourceFiles,
301
+ });
302
+ }
303
+ if (isPlanDeliveryTool(input.toolName)) {
304
+ recordPlanDelivered(stateDir);
305
+ }
306
+ const explicitPaths = extractExplicitPathFields(input.toolInput);
307
+ if (isKnownEditableTool(input.toolName)) {
308
+ const dirtyPaths = explicitPaths
309
+ .map((candidate) => toRepoRelativePath(candidate, cwd))
310
+ .filter((candidate) => isMeaningfulTrackedPath(candidate) ||
311
+ isKbFreshnessRelevantPath(candidate));
312
+ if (dirtyPaths.length > 0) {
313
+ addDirtyPaths(stateDir, dirtyPaths);
314
+ }
315
+ if (dirtyPaths.some(isSourceImpactRelevantPath)) {
316
+ markDecisionFlag(session, "source-edited");
317
+ }
318
+ }
319
+ if (kbToolCall?.toolName === "kb_upsert" ||
320
+ kbUsageTrace(input.toolName, input.toolInput)?.kbOperation ===
321
+ "kb_upsert") {
322
+ markDecisionFlag(session, "kb-recorded");
323
+ // implements REQ-kibi-signoff-views
324
+ if (isRequirementUpsert(input.toolInput)) {
325
+ markDecisionFlag(session, "requirement-recorded");
326
+ }
327
+ }
328
+ const primaryPath = resolvePrimaryPath(input);
329
+ if (!primaryPath || !kibiReady) {
352
330
  return emptyResult();
353
331
  }
354
- // rationale: same bucket equivalence as the lookup above.
355
- // Stryker disable StringLiteral
356
- rememberGuidedPath(stateDir, "write", relativePath);
357
- // Stryker restore
358
- return { additional_context: guidance };
332
+ const relativePath = toRepoRelativePath(primaryPath, cwd);
333
+ const state = loadHookState(stateDir);
334
+ if (isReadLikeTool(input.toolName)) {
335
+ if (hasGuidedPath(state, "read", relativePath)) {
336
+ return emptyResult();
337
+ }
338
+ const guidance = readGuidance(primaryPath, {
339
+ cwd,
340
+ hasKibi: kibiReady,
341
+ mcpState: state.mcpState,
342
+ workspaceTrusted,
343
+ knowledge: knowledgeFor(cwd, getSourceLinkedSymbols(cwd, primaryPath, stateDir),
344
+ // rationale: as above, only "edit" changes the snippet.
345
+ // Stryker disable next-line StringLiteral
346
+ "read"),
347
+ });
348
+ if (!guidance) {
349
+ return emptyResult();
350
+ }
351
+ rememberGuidedPath(stateDir, "read", relativePath);
352
+ return { additional_context: guidance };
353
+ }
354
+ if (isEditLikeTool(input.toolName)) {
355
+ // rationale: the runner only ever passes "read" or "write"; an empty
356
+ // kind selects the same write bucket.
357
+ // Stryker disable StringLiteral
358
+ if (hasGuidedPath(state, "write", relativePath)) {
359
+ return emptyResult();
360
+ }
361
+ // Stryker restore
362
+ const guidance = writeGuidance(primaryPath, {
363
+ cwd,
364
+ hasKibi: kibiReady,
365
+ mcpState: state.mcpState,
366
+ workspaceTrusted,
367
+ });
368
+ if (!guidance) {
369
+ return emptyResult();
370
+ }
371
+ // rationale: same bucket equivalence as the lookup above.
372
+ // Stryker disable StringLiteral
373
+ rememberGuidedPath(stateDir, "write", relativePath);
374
+ // Stryker restore
375
+ return { additional_context: guidance };
376
+ }
377
+ return emptyResult();
359
378
  }
360
- return emptyResult();
361
- }
362
- case "stop": {
363
- const state = loadHookState(stateDir);
364
- const shouldClearSession = state.dirtyPaths.length > 0 || state.kbCheckRun || state.planDelivered;
365
- if (input.status === "aborted" || input.status === "error") {
379
+ case "stop": {
380
+ const state = loadHookState(stateDir);
381
+ const shouldClearSession = state.dirtyPaths.length > 0 ||
382
+ state.kbCheckRun ||
383
+ state.planDelivered;
384
+ if (input.status === "aborted" || input.status === "error") {
385
+ if (shouldClearSession) {
386
+ clearSessionHookState(stateDir);
387
+ }
388
+ return emptyResult();
389
+ }
390
+ // implements REQ-agent-hosts-decision-guard
391
+ // Source changed and nothing was recorded: the follow-up asks for the
392
+ // decision to be recorded, once per conversation.
393
+ const capture = claimCaptureReminder(session)
394
+ ? captureReminder(hasDecisionFlag(session, "rule-prompt"))
395
+ : undefined;
396
+ // implements REQ-kibi-signoff-views
397
+ // Requirements were written without a signoff view since: ask once.
398
+ // A recorded requirement also counts as recorded knowledge, so this
399
+ // never coincides with the capture request above.
400
+ const signoff = claimSignoffReminder(session, cwd)
401
+ ? signoffReminder()
402
+ : undefined;
403
+ const reminder = stopFollowupMessage(state);
404
+ const followupMessage = [capture, signoff, reminder]
405
+ .filter((part) => part !== undefined)
406
+ .join("\n");
407
+ if (followupMessage) {
408
+ trace.action = capture
409
+ ? "stop_capture_reminder"
410
+ : signoff
411
+ ? "stop_signoff_reminder"
412
+ : "stop_reminder";
413
+ clearSessionHookState(stateDir);
414
+ return { followup_message: followupMessage };
415
+ }
366
416
  if (shouldClearSession) {
367
417
  clearSessionHookState(stateDir);
368
418
  }
369
419
  return emptyResult();
370
420
  }
371
- // implements REQ-agent-hosts-decision-guard
372
- // Source changed and nothing was recorded: the follow-up asks for the
373
- // decision to be recorded, once per conversation.
374
- const capture = claimCaptureReminder(session)
375
- ? captureReminder(hasDecisionFlag(session, "rule-prompt"))
376
- : undefined;
377
- // implements REQ-kibi-signoff-views
378
- // Requirements were written without a signoff view since: ask once.
379
- // A recorded requirement also counts as recorded knowledge, so this
380
- // never coincides with the capture request above.
381
- const signoff = claimSignoffReminder(session, cwd)
382
- ? signoffReminder()
383
- : undefined;
384
- const followupMessage = [capture, signoff, stopFollowupMessage(state)]
385
- .filter((part) => part !== undefined)
386
- .join("\n");
387
- if (followupMessage) {
388
- clearSessionHookState(stateDir);
389
- return { followup_message: followupMessage };
390
- }
391
- if (shouldClearSession) {
392
- clearSessionHookState(stateDir);
393
- }
394
- return emptyResult();
421
+ default:
422
+ return emptyResult();
423
+ }
424
+ }
425
+ finally {
426
+ if (kibiReady) {
427
+ appendHookUsage({
428
+ host: "cursor",
429
+ packageVersion: packageVersion(),
430
+ workspaceRoot: cwd,
431
+ event: input.event,
432
+ sessionId: input.conversationId,
433
+ hostTool: input.toolName,
434
+ trace,
435
+ startedAt,
436
+ }, environment.env ?? process.env);
395
437
  }
396
- default:
397
- return emptyResult();
438
+ await finishHookTelemetry({
439
+ event: input.event,
440
+ workspaceRoot: kibiReady ? cwd : undefined,
441
+ sessionId: input.conversationId,
442
+ }, environment.env ?? process.env);
398
443
  }
399
444
  }
400
445
  export async function main() {
@@ -3,32 +3,32 @@
3
3
  "hooks": {
4
4
  "sessionStart": [
5
5
  {
6
- "command": "node dist/hook-runner.js"
6
+ "command": "node bin/hook-runner.mjs"
7
7
  }
8
8
  ],
9
9
  "beforeSubmitPrompt": [
10
10
  {
11
- "command": "node dist/hook-runner.js"
11
+ "command": "node bin/hook-runner.mjs"
12
12
  }
13
13
  ],
14
14
  "preToolUse": [
15
15
  {
16
- "command": "node dist/hook-runner.js"
16
+ "command": "node bin/hook-runner.mjs"
17
17
  }
18
18
  ],
19
19
  "postToolUse": [
20
20
  {
21
- "command": "node dist/hook-runner.js"
21
+ "command": "node bin/hook-runner.mjs"
22
22
  }
23
23
  ],
24
24
  "beforeReadFile": [
25
25
  {
26
- "command": "node dist/hook-runner.js"
26
+ "command": "node bin/hook-runner.mjs"
27
27
  }
28
28
  ],
29
29
  "stop": [
30
30
  {
31
- "command": "node dist/hook-runner.js",
31
+ "command": "node bin/hook-runner.mjs",
32
32
  "loop_limit": 1
33
33
  }
34
34
  ]
@@ -4,11 +4,11 @@
4
4
  "kibi-bootstrap/resources/branch-lifecycle.md": "c854264ce888439140883a3fc636db5e522f020dd27b30a5c1ecc4105b64ed13",
5
5
  "kibi-bootstrap/resources/operation-access.md": "b5a6a59e72d800c376e7fad79132897623c32a0ce152a1e8d73f9628b444db51",
6
6
  "kibi-bootstrap/resources/source-authoring.md": "bf296e62f69474e01797d6cb9469bf624551dca834dd188abb6e705bd96fa586",
7
- "kibi-commit-review/SKILL.md": "cd3a0d5ed5e2e7562995180e440dce1aa1607d3d072d3400e966e988725ed399",
7
+ "kibi-commit-review/SKILL.md": "bec768ad3e4cd4e782e9e13360b69bcd40a0964b305909818115d7f9e8d282d9",
8
8
  "kibi-commit-review/resources/staged-gate.md": "37f750f327bb9202c1f30989763f4f1205b906cf672318ca50887d7984bc3e08",
9
- "kibi-decisions/SKILL.md": "02685fa50f093025782986bfe009940e934c3a64d47858920815e7eb66b0c523",
9
+ "kibi-decisions/SKILL.md": "d91f784410251095e9d881bce751728cc4596858a518fb24f5e5d38c940bab50",
10
10
  "kibi-decisions/resources/decision-records.md": "b201e619c48daf6ebe5989991a96f6c0573adb6c37abf814b9d34b7a1cd423c6",
11
- "kibi-decisions/resources/signoff-views.md": "123da13a3f2b30b5672044a1284626e218d3088411f84f2692746f95e06c1393",
11
+ "kibi-decisions/resources/signoff-views.md": "84eb2b7123e54047205b84af6fbf60c910ebcaaf98023e5812f3de9b65a4871c",
12
12
  "kibi-freshness/SKILL.md": "b5c4f24bebdb5b4e0d04c2fa1176fc33ace35334b0c0b6ca943d6f445dc9b69e",
13
13
  "kibi-freshness/resources/branch-lifecycle.md": "ebc01b19fe3dca8df5fc0175978963f6f1204f71f7c805cb2a5fb1b277ac78e4",
14
14
  "kibi-freshness/resources/operation-access.md": "4e1db9294ee540cd29b8b3a2532a81e7894d5b4228b39e99ba48b37212f489f3",
@@ -18,13 +18,14 @@
18
18
  "kibi-traceability/resources/operation-access.md": "39f7972ab7f4ae7479e78f7072bb50e8e6c4cdf4f857425f69d2522d37db437c",
19
19
  "kibi-traceability/resources/source-authoring.md": "408c6ee2bef1b84e0194c0538ef929c26a0a617e631b2234247388140dbbdd08",
20
20
  "kibi-traceability/resources/traceability.md": "38c290a0bc71bd1692e115f22694743946e2f69630e134acfd43cee5f613fb88",
21
- "kibi-usage/SKILL.md": "d5c2e9e2e643f51f068a4e586002976d28125001c33e791e70994efdbbc8130f",
21
+ "kibi-usage/SKILL.md": "91639d49eae60a83b01017de1843ce49575db231f3f9cb1b0270c57a20462a88",
22
22
  "kibi-usage/resources/branch-lifecycle.md": "f1bf359e9826e83c548f3cd0287f8175a62456527b0132b06b3f9f74aa59b7d6",
23
23
  "kibi-usage/resources/fact-lanes.md": "7d487ffb794f6a7c32afabc7756a265a258b072ca83510ddb40d275863d44648",
24
24
  "kibi-usage/resources/kb-improvement.md": "a8408fce27342bdce6415a6718599e53fe00bb33aa1fd3dc42353ec6f61adcd7",
25
25
  "kibi-usage/resources/logic-ir.md": "0e8ce8aad07433ab4803ec169149ff6c4e1cf8cf010af18fa6fd67862d9be807",
26
26
  "kibi-usage/resources/operation-access.md": "eb455796c066787ffd3fae8db0db1d3e87a84265a57f06ae61e30103cd6aed66",
27
- "kibi-usage/resources/proof.md": "65797125425f457dd6291e034e4174166bc0b62a2c269d136877c6f8de676bf0",
27
+ "kibi-usage/resources/prolog-proofs.md": "b630f1edf32c4d14dbd096deb15c370f6ce05dede8aa9cda4fbf294d8668f164",
28
+ "kibi-usage/resources/proof.md": "fe5c0ed8117ec724f925c29a95345e2f7b9969a413a3ba56eece23273f91346e",
28
29
  "kibi-usage/resources/relationship-directions.md": "0fdd67032c2d15299f772845fc4f807eb9435b09507bd87a5911ab33db2f143d",
29
30
  "kibi-usage/resources/source-authoring.md": "cad93f1e0ae08a676f2de7492f9227677880302327e74d159618fea86ca50abe",
30
31
  "kibi-usage/resources/ui-requirements.md": "f99c2a9a73bbb7bd92ce1f231db3056d544588be149a5b0d3f7cc6f28bc9eee9",
@@ -2,7 +2,7 @@
2
2
  id: kibi-commit-review
3
3
  name: kibi-commit-review
4
4
  description: "Use when kibi check --staged or the Kibi pre-commit hook blocks a commit (kibi_impact_evidence_missing, symbols_manifest_stale, symbol_granularity_violation, symbol-coverage, kibi check-generated drift, Staged impact review failed), or when preparing a commit or pull-request impact review. Covers reading the per-file Detail lines, authoring .kb/symbols.yaml symbol entries through kb_upsert, staging the tracked Kibi evidence with git add (symbol coordinates are derived and never staged), kb_prepare_impact_review for a staged or diff scope, the kibi signoff requirements summary to show before a commit, and how merge commits and migration renames are judged."
5
- version: 1.3.0
5
+ version: 1.4.0
6
6
  kibiCompatibility: ">=1.0.0"
7
7
  tags:
8
8
  - kibi
@@ -91,7 +91,11 @@ apps); never attach the `.html` as a file, which chat apps show as raw source;
91
91
  past 15 requirements the Markdown stops and only the page lists them all.
92
92
  Host details: `kibi-decisions`, `resources/signoff-views.md`. `--no-proof` skips the proof lookup, which reads coverage and is slow
93
93
  on a large knowledge base. The summary is not an approval: never set
94
- `approved_by` or `origin.ref` from it.
94
+ `approved_by` or `origin.ref` from it. Show it before you open or merge the
95
+ pull request. When the person answers, record their answer in the same
96
+ change (their name in `approved_by`, their words in `origin.ref` or
97
+ `approval_ref`) and push it to the same pull request; never move a signoff,
98
+ firmness or waiver approval into a separate pull request.
95
99
 
96
100
  ## Impact reviews
97
101
 
@@ -2,7 +2,7 @@
2
2
  id: kibi-decisions
3
3
  name: kibi-decisions
4
4
  description: "Use when the person states a rule or decision (must, never, always, from now on, we decided), when a Kibi hook asks to confirm or warns about an edit to code that implements a firm requirement, when the Stop hook says source files changed but nothing was recorded, when kb_check quotes requirement_statement_unverified, or when a request conflicts with a recorded requirement or ADR. Covers reading firmness and origin, recording a person's rule as firm with origin.ref, superseding with a Why this changed section, the kb_compile_intent unresolved/reviewCandidates/conflictReviews loop, asking before overriding a firm rule, and putting decisions to the person as one numbered kibi signoff --questions view."
5
- version: 1.4.0
5
+ version: 1.5.0
6
6
  kibiCompatibility: ">=1.0.0"
7
7
  tags:
8
8
  - kibi
@@ -115,7 +115,12 @@ again until none is open, then record each rule with their words in
115
115
  `origin.ref`. Fields and outputs: `resources/signoff-views.md`.
116
116
 
117
117
  Never invent words for `origin.ref`, never set `approved_by` yourself, and
118
- mark your own picks `default` and guesses `exploratory`. Payload shapes,
118
+ mark your own picks `default` and guesses `exploratory`. Once the person has
119
+ answered, recording their answer (their name in `approved_by` or a
120
+ `*_waiver_approved_by` field, their words in `origin.ref` or `approval_ref`)
121
+ is yours to do: put it on the branch of the change it approves and push it to
122
+ that pull request, never to a separate one. Ask before you open or merge the
123
+ pull request, so the answer lands in it. Payload shapes,
119
124
  refusal messages and the `kb_compile_intent` review loop are in
120
125
  `resources/decision-records.md`.
121
126