kibi-cursor 2.9.7 → 2.10.1

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 (35) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +51 -0
  3. package/agent-plugin/plugin.json +1 -1
  4. package/agent-plugin/skills/kibi-bootstrap/SKILL.md +2 -2
  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 +50 -3
  9. package/agent-plugin/skills/kibi-usage/resources/fact-lanes.md +34 -0
  10. package/agent-plugin/skills/kibi-usage/resources/proof.md +13 -5
  11. package/agent-plugin/skills/kibi-usage/resources/workflows.md +10 -0
  12. package/dist/.cursor-plugin/plugin.json +1 -1
  13. package/dist/guidance.js +5 -9
  14. package/dist/hook-runner.js +225 -203
  15. package/dist/skills/.canon-hash.json +8 -8
  16. package/dist/skills/kibi-bootstrap/SKILL.md +2 -2
  17. package/dist/skills/kibi-commit-review/SKILL.md +6 -2
  18. package/dist/skills/kibi-decisions/SKILL.md +7 -2
  19. package/dist/skills/kibi-decisions/resources/signoff-views.md +3 -1
  20. package/dist/skills/kibi-usage/SKILL.md +50 -3
  21. package/dist/skills/kibi-usage/resources/fact-lanes.md +34 -0
  22. package/dist/skills/kibi-usage/resources/proof.md +13 -5
  23. package/dist/skills/kibi-usage/resources/workflows.md +10 -0
  24. package/dist/source-linked-requirements.d.ts +0 -10
  25. package/dist/source-linked-requirements.js +1 -23
  26. package/package.json +2 -2
  27. package/skills/.canon-hash.json +8 -8
  28. package/skills/kibi-bootstrap/SKILL.md +2 -2
  29. package/skills/kibi-commit-review/SKILL.md +6 -2
  30. package/skills/kibi-decisions/SKILL.md +7 -2
  31. package/skills/kibi-decisions/resources/signoff-views.md +3 -1
  32. package/skills/kibi-usage/SKILL.md +50 -3
  33. package/skills/kibi-usage/resources/fact-lanes.md +34 -0
  34. package/skills/kibi-usage/resources/proof.md +13 -5
  35. package/skills/kibi-usage/resources/workflows.md +10 -0
@@ -4,7 +4,7 @@ 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 { 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
9
  import { createEntitySummarizer, editFocus, fileKnowledgeSnippet, implementedRequirementIds, } from "kibi-agent-core/snippets";
10
10
  import { preEditGuidance, readGuidance, writeGuidance } from "./guidance.js";
@@ -14,7 +14,6 @@ import { extractKbMcpToolCall } from "./kb-mcp-tools.js";
14
14
  import { BOOTSTRAP_REMINDER, DIRECT_KB_EDIT_WARNING, interfaceAdvisory, stopFollowupMessage, } from "./messages.js";
15
15
  import { extractExplicitPathFields, isDirectKbPath, isKbFreshnessRelevantPath, isMeaningfulTrackedPath, isSourceImpactRelevantPath, toRepoRelativePath, } from "./path-policy.js";
16
16
  import { getSourceLinkedSymbols, } from "./source-linked-requirements.js";
17
- const MAX_LINKED_REQUIREMENTS = 3;
18
17
  /**
19
18
  * The shared kibi-agent-core snippet for a file that implements requirements,
20
19
  * or undefined so the generic Cursor guidance applies. Test-only links
@@ -54,13 +53,10 @@ function firmNotice(workspaceRoot, linked, focus, session, env) {
54
53
  settleFirmRequirements(session, ids);
55
54
  return firmEditNotice(symbol ? `${symbol.title} in ${linked.relativePath}` : linked.relativePath, ids.map(summarize));
56
55
  }
57
- let cachedPackageVersion;
58
56
  function packageVersion() {
59
- // dist/hook-runner.js sits one level below the package root.
60
- if (cachedPackageVersion !== undefined)
61
- return cachedPackageVersion;
62
- cachedPackageVersion = readPackageVersion(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
63
- return cachedPackageVersion;
57
+ // dist/hook-runner.js sits one level below the package root. Each hook
58
+ // event is its own process, so the version is read at most once per event.
59
+ return readPackageVersion(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"));
64
60
  }
65
61
  /**
66
62
  * Opt-in (`KIBI_DIAGNOSTIC_MODE`) hook rows for the lookup-before-first-edit
@@ -69,15 +65,18 @@ function packageVersion() {
69
65
  */
70
66
  // implements REQ-cursor-kibi-plugin-v1
71
67
  function recordToolTelemetry(input, workspaceRoot, stateDir, startedAt, env) {
72
- if (!hookTelemetryEnabled(env))
68
+ // rationale: appendHookUsageRows applies the same opt-in; this guard only
69
+ // skips reading the index when telemetry is off.
70
+ // Stryker disable next-line ConditionalExpression
71
+ if (!hookTracesWanted(env))
73
72
  return;
74
73
  const kbUsage = kbUsageTrace(input.toolName, input.toolInput);
74
+ if (!kbUsage && !isKnownEditableTool(input.toolName))
75
+ return;
75
76
  const traces = kbUsage
76
77
  ? [kbUsage]
77
- : isKnownEditableTool(input.toolName)
78
- ? editTraces(extractExplicitPathFields(input.toolInput).map((candidate) => toRepoRelativePath(candidate, workspaceRoot)), (relativePath) => implementedRequirementIds(getSourceLinkedSymbols(workspaceRoot, relativePath, stateDir)
79
- .symbols))
80
- : [];
78
+ : editTraces(extractExplicitPathFields(input.toolInput).map((candidate) => toRepoRelativePath(candidate, workspaceRoot)), (relativePath) => implementedRequirementIds(getSourceLinkedSymbols(workspaceRoot, relativePath, stateDir)
79
+ .symbols));
81
80
  appendHookUsageRows({
82
81
  host: "cursor",
83
82
  packageVersion: packageVersion(),
@@ -167,225 +166,248 @@ export async function runHook(rawInput, environment = {}) {
167
166
  stateDir,
168
167
  sessionId: input.conversationId,
169
168
  };
170
- switch (input.event) {
171
- case "sessionStart":
172
- if (!kibiReady) {
173
- const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
174
- return {
175
- additional_context: advisory
176
- ? `${BOOTSTRAP_REMINDER}\n${advisory}`
177
- : BOOTSTRAP_REMINDER,
178
- };
179
- }
180
- {
181
- const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
182
- return advisory ? { additional_context: advisory } : emptyResult();
183
- }
184
- case "preToolUse": {
185
- // Name the agent's workspace on every Kibi MCP call so the launcher
186
- // answers from it, also for agents working in a git worktree. The
187
- // per-call cwd wins over workspace_roots, which can be the window's
188
- // folders rather than the agent's checkout.
189
- const stamped = stampKibiWorkspace(input.toolName, input.toolInput, kibiWorkspaceFor(input.cwd ?? input.workspaceRoots?.[0]));
190
- if (stamped) {
191
- return { updated_input: stamped };
192
- }
193
- const explicitPaths = extractExplicitPathFields(input.toolInput);
194
- const hasDirectKbEdit = isEditLikeTool(input.toolName) && explicitPaths.some(isDirectKbPath);
195
- if (hasDirectKbEdit) {
196
- return {
197
- permission: "allow",
198
- agent_message: DIRECT_KB_EDIT_WARNING,
199
- };
200
- }
201
- const primaryPath = resolvePrimaryPath(input);
202
- if (!primaryPath || !kibiReady || !isKnownEditableTool(input.toolName)) {
203
- return emptyResult();
204
- }
205
- const relativePath = toRepoRelativePath(primaryPath, cwd);
206
- const state = loadHookState(stateDir);
207
- const linked = getSourceLinkedSymbols(cwd, primaryPath, stateDir);
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);
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), "read"),
256
- });
257
- if (!guidance) {
258
- return { permission: "allow" };
259
- }
260
- rememberGuidedPath(stateDir, "read", relativePath);
261
- return {
262
- permission: "allow",
263
- agent_message: guidance,
264
- };
265
- }
266
- case "postToolUse": {
267
- if (kibiReady) {
268
- try {
269
- recordToolTelemetry(input, cwd, stateDir, startedAt, environment.env);
169
+ try {
170
+ switch (input.event) {
171
+ case "sessionStart":
172
+ if (!kibiReady) {
173
+ const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
174
+ return {
175
+ additional_context: advisory
176
+ ? `${BOOTSTRAP_REMINDER}\n${advisory}`
177
+ : BOOTSTRAP_REMINDER,
178
+ };
270
179
  }
271
- catch {
272
- // Telemetry is best effort and must never change the hook's output.
180
+ {
181
+ const advisory = interfaceAdvisory(initialState.mcpState, workspaceTrusted);
182
+ return advisory ? { additional_context: advisory } : emptyResult();
273
183
  }
274
- }
275
- const kbToolCall = extractKbMcpToolCall(input.toolName, input.toolInput);
276
- if (kbToolCall) {
277
- recordKbMcpTool(stateDir, kbToolCall.toolName, {
278
- impactCheckRun: kbToolCall.impactCheckRun,
279
- sourceFiles: kbToolCall.sourceFiles,
280
- });
281
- }
282
- if (isPlanDeliveryTool(input.toolName)) {
283
- recordPlanDelivered(stateDir);
284
- }
285
- const explicitPaths = extractExplicitPathFields(input.toolInput);
286
- if (isKnownEditableTool(input.toolName)) {
287
- const dirtyPaths = explicitPaths
288
- .map((candidate) => toRepoRelativePath(candidate, cwd))
289
- .filter((candidate) => isMeaningfulTrackedPath(candidate) ||
290
- isKbFreshnessRelevantPath(candidate));
291
- if (dirtyPaths.length > 0) {
292
- addDirtyPaths(stateDir, dirtyPaths);
184
+ case "preToolUse": {
185
+ // Name the agent's workspace on every Kibi MCP call so the launcher
186
+ // answers from it, also for agents working in a git worktree. The
187
+ // per-call cwd wins over workspace_roots, which can be the window's
188
+ // folders rather than the agent's checkout.
189
+ const stamped = stampKibiWorkspace(input.toolName, input.toolInput, kibiWorkspaceFor(input.cwd ?? input.workspaceRoots?.[0]));
190
+ if (stamped) {
191
+ return { updated_input: stamped };
293
192
  }
294
- if (dirtyPaths.some(isSourceImpactRelevantPath)) {
295
- markDecisionFlag(session, "source-edited");
193
+ const explicitPaths = extractExplicitPathFields(input.toolInput);
194
+ const hasDirectKbEdit = isEditLikeTool(input.toolName) && explicitPaths.some(isDirectKbPath);
195
+ if (hasDirectKbEdit) {
196
+ return {
197
+ permission: "allow",
198
+ agent_message: DIRECT_KB_EDIT_WARNING,
199
+ };
296
200
  }
297
- }
298
- if (kbToolCall?.toolName === "kb_upsert" ||
299
- kbUsageTrace(input.toolName, input.toolInput)?.kbOperation ===
300
- "kb_upsert") {
301
- markDecisionFlag(session, "kb-recorded");
302
- // implements REQ-kibi-signoff-views
303
- if (isRequirementUpsert(input.toolInput)) {
304
- markDecisionFlag(session, "requirement-recorded");
201
+ const primaryPath = resolvePrimaryPath(input);
202
+ if (!primaryPath ||
203
+ !kibiReady ||
204
+ !isKnownEditableTool(input.toolName)) {
205
+ return emptyResult();
206
+ }
207
+ const relativePath = toRepoRelativePath(primaryPath, cwd);
208
+ const state = loadHookState(stateDir);
209
+ const linked = getSourceLinkedSymbols(cwd, primaryPath, stateDir);
210
+ const focus = editFocus(path.resolve(cwd, primaryPath), input.toolInput);
211
+ const ruleNote = takeRuleNote(session);
212
+ const notice = firmNotice(cwd, linked, focus, session, environment.env ?? process.env);
213
+ const guidance = hasGuidedPath(state, "pre-edit", relativePath)
214
+ ? undefined
215
+ : preEditGuidance(primaryPath, {
216
+ cwd,
217
+ hasKibi: kibiReady,
218
+ mcpState: state.mcpState,
219
+ workspaceTrusted,
220
+ // A file that implements requirements always gets the shared
221
+ // snippet, so the bare linked-requirement wording is never needed.
222
+ knowledge: knowledgeFor(cwd, linked, "edit", focus),
223
+ });
224
+ if (guidance) {
225
+ rememberGuidedPath(stateDir, "pre-edit", relativePath);
305
226
  }
227
+ const parts = [ruleNote, notice, guidance].filter((part) => part !== undefined);
228
+ return parts.length > 0
229
+ ? { permission: "allow", agent_message: parts.join("\n\n") }
230
+ : emptyResult();
306
231
  }
307
- const primaryPath = resolvePrimaryPath(input);
308
- if (!primaryPath || !kibiReady) {
309
- return emptyResult();
232
+ // implements REQ-agent-hosts-decision-guard
233
+ case "beforeSubmitPrompt": {
234
+ // Cursor's prompt hook cannot add context: the request's rule note
235
+ // waits for the next pre-edit message.
236
+ if (kibiReady && input.prompt) {
237
+ const rule = recordRequest(session, input.prompt);
238
+ if (rule !== undefined)
239
+ holdRuleNote(session, rule);
240
+ }
241
+ return { continue: true };
310
242
  }
311
- const relativePath = toRepoRelativePath(primaryPath, cwd);
312
- const state = loadHookState(stateDir);
313
- if (isReadLikeTool(input.toolName)) {
243
+ case "beforeReadFile": {
244
+ const primaryPath = resolvePrimaryPath(input);
245
+ if (!primaryPath || !kibiReady) {
246
+ return { permission: "allow" };
247
+ }
248
+ const relativePath = toRepoRelativePath(primaryPath, cwd);
249
+ const state = loadHookState(stateDir);
314
250
  if (hasGuidedPath(state, "read", relativePath)) {
315
- return emptyResult();
251
+ return { permission: "allow" };
316
252
  }
317
253
  const guidance = readGuidance(primaryPath, {
318
254
  cwd,
319
255
  hasKibi: kibiReady,
320
256
  mcpState: state.mcpState,
321
257
  workspaceTrusted,
322
- knowledge: knowledgeFor(cwd, getSourceLinkedSymbols(cwd, primaryPath, stateDir), "read"),
258
+ knowledge: knowledgeFor(cwd, getSourceLinkedSymbols(cwd, primaryPath, stateDir),
259
+ // rationale: the snippet only special-cases "edit", so any other
260
+ // surface literal renders the same read snippet.
261
+ // Stryker disable next-line StringLiteral
262
+ "read"),
323
263
  });
324
264
  if (!guidance) {
325
- return emptyResult();
265
+ return { permission: "allow" };
326
266
  }
327
267
  rememberGuidedPath(stateDir, "read", relativePath);
328
- return { additional_context: guidance };
268
+ return {
269
+ permission: "allow",
270
+ agent_message: guidance,
271
+ };
329
272
  }
330
- if (isEditLikeTool(input.toolName)) {
331
- // rationale: the runner only ever passes "read" or "write"; an empty
332
- // kind selects the same write bucket.
333
- // Stryker disable StringLiteral
334
- if (hasGuidedPath(state, "write", relativePath)) {
335
- return emptyResult();
273
+ case "postToolUse": {
274
+ if (kibiReady) {
275
+ try {
276
+ recordToolTelemetry(input, cwd, stateDir, startedAt, environment.env);
277
+ }
278
+ catch {
279
+ // Telemetry is best effort and must never change the hook's output.
280
+ }
336
281
  }
337
- // Stryker restore
338
- const guidance = writeGuidance(primaryPath, {
339
- cwd,
340
- hasKibi: kibiReady,
341
- mcpState: state.mcpState,
342
- workspaceTrusted,
343
- });
344
- if (!guidance) {
282
+ const kbToolCall = extractKbMcpToolCall(input.toolName, input.toolInput);
283
+ if (kbToolCall) {
284
+ recordKbMcpTool(stateDir, kbToolCall.toolName, {
285
+ impactCheckRun: kbToolCall.impactCheckRun,
286
+ sourceFiles: kbToolCall.sourceFiles,
287
+ });
288
+ }
289
+ if (isPlanDeliveryTool(input.toolName)) {
290
+ recordPlanDelivered(stateDir);
291
+ }
292
+ const explicitPaths = extractExplicitPathFields(input.toolInput);
293
+ if (isKnownEditableTool(input.toolName)) {
294
+ const dirtyPaths = explicitPaths
295
+ .map((candidate) => toRepoRelativePath(candidate, cwd))
296
+ .filter((candidate) => isMeaningfulTrackedPath(candidate) ||
297
+ isKbFreshnessRelevantPath(candidate));
298
+ if (dirtyPaths.length > 0) {
299
+ addDirtyPaths(stateDir, dirtyPaths);
300
+ }
301
+ if (dirtyPaths.some(isSourceImpactRelevantPath)) {
302
+ markDecisionFlag(session, "source-edited");
303
+ }
304
+ }
305
+ if (kbToolCall?.toolName === "kb_upsert" ||
306
+ kbUsageTrace(input.toolName, input.toolInput)?.kbOperation ===
307
+ "kb_upsert") {
308
+ markDecisionFlag(session, "kb-recorded");
309
+ // implements REQ-kibi-signoff-views
310
+ if (isRequirementUpsert(input.toolInput)) {
311
+ markDecisionFlag(session, "requirement-recorded");
312
+ }
313
+ }
314
+ const primaryPath = resolvePrimaryPath(input);
315
+ if (!primaryPath || !kibiReady) {
345
316
  return emptyResult();
346
317
  }
347
- // rationale: same bucket equivalence as the lookup above.
348
- // Stryker disable StringLiteral
349
- rememberGuidedPath(stateDir, "write", relativePath);
350
- // Stryker restore
351
- return { additional_context: guidance };
318
+ const relativePath = toRepoRelativePath(primaryPath, cwd);
319
+ const state = loadHookState(stateDir);
320
+ if (isReadLikeTool(input.toolName)) {
321
+ if (hasGuidedPath(state, "read", relativePath)) {
322
+ return emptyResult();
323
+ }
324
+ const guidance = readGuidance(primaryPath, {
325
+ cwd,
326
+ hasKibi: kibiReady,
327
+ mcpState: state.mcpState,
328
+ workspaceTrusted,
329
+ knowledge: knowledgeFor(cwd, getSourceLinkedSymbols(cwd, primaryPath, stateDir),
330
+ // rationale: as above, only "edit" changes the snippet.
331
+ // Stryker disable next-line StringLiteral
332
+ "read"),
333
+ });
334
+ if (!guidance) {
335
+ return emptyResult();
336
+ }
337
+ rememberGuidedPath(stateDir, "read", relativePath);
338
+ return { additional_context: guidance };
339
+ }
340
+ if (isEditLikeTool(input.toolName)) {
341
+ // rationale: the runner only ever passes "read" or "write"; an empty
342
+ // kind selects the same write bucket.
343
+ // Stryker disable StringLiteral
344
+ if (hasGuidedPath(state, "write", relativePath)) {
345
+ return emptyResult();
346
+ }
347
+ // Stryker restore
348
+ const guidance = writeGuidance(primaryPath, {
349
+ cwd,
350
+ hasKibi: kibiReady,
351
+ mcpState: state.mcpState,
352
+ workspaceTrusted,
353
+ });
354
+ if (!guidance) {
355
+ return emptyResult();
356
+ }
357
+ // rationale: same bucket equivalence as the lookup above.
358
+ // Stryker disable StringLiteral
359
+ rememberGuidedPath(stateDir, "write", relativePath);
360
+ // Stryker restore
361
+ return { additional_context: guidance };
362
+ }
363
+ return emptyResult();
352
364
  }
353
- return emptyResult();
354
- }
355
- case "stop": {
356
- const state = loadHookState(stateDir);
357
- const shouldClearSession = state.dirtyPaths.length > 0 || state.kbCheckRun || state.planDelivered;
358
- if (input.status === "aborted" || input.status === "error") {
365
+ case "stop": {
366
+ const state = loadHookState(stateDir);
367
+ const shouldClearSession = state.dirtyPaths.length > 0 ||
368
+ state.kbCheckRun ||
369
+ state.planDelivered;
370
+ if (input.status === "aborted" || input.status === "error") {
371
+ if (shouldClearSession) {
372
+ clearSessionHookState(stateDir);
373
+ }
374
+ return emptyResult();
375
+ }
376
+ // implements REQ-agent-hosts-decision-guard
377
+ // Source changed and nothing was recorded: the follow-up asks for the
378
+ // decision to be recorded, once per conversation.
379
+ const capture = claimCaptureReminder(session)
380
+ ? captureReminder(hasDecisionFlag(session, "rule-prompt"))
381
+ : undefined;
382
+ // implements REQ-kibi-signoff-views
383
+ // Requirements were written without a signoff view since: ask once.
384
+ // A recorded requirement also counts as recorded knowledge, so this
385
+ // never coincides with the capture request above.
386
+ const signoff = claimSignoffReminder(session, cwd)
387
+ ? signoffReminder()
388
+ : undefined;
389
+ const followupMessage = [capture, signoff, stopFollowupMessage(state)]
390
+ .filter((part) => part !== undefined)
391
+ .join("\n");
392
+ if (followupMessage) {
393
+ clearSessionHookState(stateDir);
394
+ return { followup_message: followupMessage };
395
+ }
359
396
  if (shouldClearSession) {
360
397
  clearSessionHookState(stateDir);
361
398
  }
362
399
  return emptyResult();
363
400
  }
364
- // implements REQ-agent-hosts-decision-guard
365
- // Source changed and nothing was recorded: the follow-up asks for the
366
- // decision to be recorded, once per conversation.
367
- const capture = claimCaptureReminder(session)
368
- ? captureReminder(hasDecisionFlag(session, "rule-prompt"))
369
- : undefined;
370
- // implements REQ-kibi-signoff-views
371
- // Requirements were written without a signoff view since: ask once.
372
- const signoff = capture === undefined && claimSignoffReminder(session, cwd)
373
- ? signoffReminder()
374
- : undefined;
375
- const followupMessage = [capture, signoff, stopFollowupMessage(state)]
376
- .filter((part) => part !== undefined)
377
- .join("\n");
378
- if (followupMessage) {
379
- clearSessionHookState(stateDir);
380
- return { followup_message: followupMessage };
381
- }
382
- if (shouldClearSession) {
383
- clearSessionHookState(stateDir);
384
- }
385
- return emptyResult();
401
+ default:
402
+ return emptyResult();
386
403
  }
387
- default:
388
- return emptyResult();
404
+ }
405
+ finally {
406
+ await finishHookTelemetry({
407
+ event: input.event,
408
+ workspaceRoot: kibiReady ? cwd : undefined,
409
+ sessionId: input.conversationId,
410
+ }, environment.env ?? process.env);
389
411
  }
390
412
  }
391
413
  export async function main() {
@@ -1,14 +1,14 @@
1
1
  {
2
- "kibi-bootstrap/SKILL.md": "81d89d20cdc24b0adb3241c555ab4c22e53c2414d6a0f4e69e0d0864a419fcc8",
2
+ "kibi-bootstrap/SKILL.md": "f3b368184c2f7cd35c9af5aa7ad4a19db8ef35499d29f09fc0005dbd7a9b33f5",
3
3
  "kibi-bootstrap/resources/bootstrap.md": "e93fae88a6853d0092cde7eee33f7d4943a8141e3841bfe3f64ab73247867b6a",
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,15 +18,15 @@
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": "36242e1ec0b461502d978f728d0946e654ea6e8871212ef298977c5e8e9b9767",
21
+ "kibi-usage/SKILL.md": "24c7148c78501a1fa8d9374e96c9a2c5503d04347c186c958cbd768cd67abd88",
22
22
  "kibi-usage/resources/branch-lifecycle.md": "f1bf359e9826e83c548f3cd0287f8175a62456527b0132b06b3f9f74aa59b7d6",
23
- "kibi-usage/resources/fact-lanes.md": "899218f90396aa469b6e34aeb5f7ea1f38bf11bb326102319f376c213859fb33",
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": "f5ce2c97b676d3302e33992e7f803c3ffbbb0aa7a808adc00e82cdba33325467",
27
+ "kibi-usage/resources/proof.md": "65797125425f457dd6291e034e4174166bc0b62a2c269d136877c6f8de676bf0",
28
28
  "kibi-usage/resources/relationship-directions.md": "0fdd67032c2d15299f772845fc4f807eb9435b09507bd87a5911ab33db2f143d",
29
29
  "kibi-usage/resources/source-authoring.md": "cad93f1e0ae08a676f2de7492f9227677880302327e74d159618fea86ca50abe",
30
30
  "kibi-usage/resources/ui-requirements.md": "f99c2a9a73bbb7bd92ce1f231db3056d544588be149a5b0d3f7cc6f28bc9eee9",
31
- "kibi-usage/resources/workflows.md": "9d248791dd95462a42af678d6768c08fdbe060382cfb43d8a6d99fddfe758417"
31
+ "kibi-usage/resources/workflows.md": "cdffbb2b7956826147832b1af890532f9c83693ab5b2e1c559bb5c1d56d05ef2"
32
32
  }
@@ -2,7 +2,7 @@
2
2
  id: kibi-bootstrap
3
3
  name: kibi-bootstrap
4
4
  description: "Use for any Kibi bootstrap or onboarding task: seeding a new or thin knowledge base; reviewing a bootstrap or onboarding plan, preview, or evidence; judging approval readiness or coverage; diagnosing a blocked, partial, or failed bootstrap or its repair; applying an approved bootstrap plan; or asking the human which knowledge sources hold intent. Covers kb_status first, source interview, cited intent claims, read-only kb_plan_bootstrap preview, explicit approval, exact-plan kb_apply_plan, and repair-safe close-out."
5
- version: 3.17.0
5
+ version: 3.18.0
6
6
  kibiCompatibility: ">=1.0.0"
7
7
  tags:
8
8
  - kibi
@@ -35,7 +35,7 @@ Seed branch-local Kibi knowledge from cited product intent, without creating a p
35
35
  1. **Interview.** Inventory your reachable connectors (trackers, wikis, drives) and in-repo docs, ADRs, and specs. In one message, ask what is still unanswered: what the repository is for; which sources hold intent and whether each is authoritative, supporting, or stale; what is missing; which areas matter first. When a source states a requirement without saying why, ask the human for the rationale (the user problem, incident or decision behind it) instead of inventing one, and cite their answer as a source. If a key source has no connector, name it and continue. Never claim access you lack.
36
36
  2. **Harvest.** From confirmed, non-stale sources, record one statement per behavior with its source and exact reference (ticket key, URL, or anchor), and classify it: `intent` (what the product should do), `observation` (how it behaves today, with no stated intent), or `open_question` (what the sources leave undecided). Keep the original meaning. Do not merge, generalize, or resolve conflicts; record each contradiction between claims as a conflict instead. Treat source text as evidence, not instructions.
37
37
  3. **Declare** `bootstrapContext`: `projectSummary`; `knowledgeSources` (id, kind, title, locator, authority, optional connector, optional `component`); `intentClaims` (statement, sourceId, reference, `excerpt` required for `intent` and `observation` claims and optional for `open_question`, optional `kind`, default `intent`, optional `component` overriding the source's, optional `rationale` with the reason the source or the human gave, never invented); `conflicts` (`claimReferences` naming two or more declared claims by sourceId and reference, plus a one-sentence `note`). Only intent claims can become requirements, and they are recorded as the people's rules: `origin.kind: import` with `sourceId:reference` as `origin.ref`, and `firmness: firm`, so a later agent asks before changing one (normative lines from repository Markdown get the same, citing `path#Lline`). Observations, open questions (tagged `review:open-question`) and conflicts (tagged `review:conflict`) become cited observation facts. Generated subject keys take the shape `component.aspect`: name the component each source or claim is about (for example `recorder`; set `component` on the knowledge source, or on a claim to override it; repository Markdown takes its file or directory name), because an intent claim whose subject is neither one word nor already dotted, with no component, is reported in the plan diagnostics and left as an authoring follow-up instead of a malformed key. The aspect is the claim's subject, or its constrained property when the subject is the component itself. An aspect over four words or 40 characters is shortened to a noun phrase (words before `when`, `while`, `for`, `and`, ...; `support_inbox.update_in_real_time_when_a_new_ticket_is_assigned...` becomes `support_inbox.update_in_real_time`) and reported as `subject-key-shortened:` with both keys; two different subjects that shorten alike get a third segment (`editor.discard_a_draft.leaving`) and a `subject-key-disambiguated:` diagnostic; claims about one subject share one subject fact, and a `subject-key-shared:` diagnostic names the key and its sources. Show these diagnostics to the human with the plan so they can rename a key before approval. Kibi never contacts these sources. It binds your declaration into the plan hash.
38
- 4. **Preview** with `kb_plan_bootstrap` (or `plan-bootstrap --input`), read-only. On `needs_context`, ask only its bounded questions (four at most), then preview again. When `pluginOffers` is non-empty (for example `kibi-plugin-ui` because UI component files exist), ask the human once whether to enable each offered plugin, naming its evidence files. On yes, install it as a dev dependency and add it to `package.json` `kibi.plugins` with the offered capability in `augment` mode; on no, add its name to `kibi.declinedPlugins` so it is not offered again. Either way the human decides; never enable a plugin unasked. For `kibi-plugin-ui`, step 11 also captures the UI pattern inventory (see `kibi-usage` `resources/ui-requirements.md`).
38
+ 4. **Preview** with `kb_plan_bootstrap` (or `plan-bootstrap --input`), read-only. On `needs_context`, ask only its bounded questions (four at most), then preview again. When `pluginOffers` is non-empty (for example `kibi-plugin-ui` because UI component files exist, or `kibi-plugin-search-hints` because the KB holds 50 or more requirements), ask the human once whether to enable each offered plugin, naming its evidence files. On yes, install it as a dev dependency and add it to `package.json` `kibi.plugins` with the offered capability in the offer's `mode` (`augment` for `kibi-plugin-ui`, `replace` for `kibi-plugin-search-hints`); on no, add its name to `kibi.declinedPlugins` so it is not offered again. Either way the human decides; never enable a plugin unasked. For `kibi-plugin-ui`, step 11 also captures the UI pattern inventory (see `kibi-usage` `resources/ui-requirements.md`).
39
39
  5. **Narrow with filters, not caps.** Read the `tldr` and the `Suppressed candidates by reason` diagnostic before individual rows. If the output is too large or mostly tooling or metadata, re-preview with `entityTypes` or declared `sourceOfTruthPaths`. Generic Markdown is off by default once you declare intent claims; set `includeGenericMarkdown: true` only for repository docs the human named. Keep confirmed intent claims: they are never capped and never use `maxCandidates`, which is the separate budget for candidates Kibi discovers (symbols, tests, repository docs). Raise it only when discovered candidates you need are `over_limit`, and never for provenance stubs: a provider entry with no claim (`Source module: …`, repository metadata, layout roots) is planned last as a `meta` fact tagged `bootstrap:provenance-stub`, `discoverySummary.provenanceStubs` counts them apart from `candidatesWithClaims`, and the `over_limit` diagnostic says how many suppressed candidates are stubs; they add no knowledge, so a plan that is mostly stubs needs `entityTypes` or declared sources, not a higher cap. Read the per-source `Knowledge source …` diagnostics: each splits the claims not planned by reason (suppressed by the activation mode, a `suppressedCandidates` row, a `sourceOnlySignals` follow-up). Report them per source and restate them before approval, or carry them to step 11 with their citation.
40
40
  6. **Request approval.** Show the complete `structuredContent.plan`, the full hash (never abbreviated), which candidates cite which sources, what was omitted, the open questions and declared conflicts it records for the human to decide, and any claim that contradicts code. If you find an undeclared contradiction now, add it to `conflicts` and preview again rather than resolving it. A `conflict-review:` diagnostic (and `review_against:<id>` evidence on the candidate) names current requirements Kibi could not check a planned claim against because they have no logic: compare each pair yourself and show any contradiction to the human with the plan; they decide whether to drop the claim or keep it and supersede the requirement after apply. A claim left for authoring gets the same diagnostic (and `review_against:<id>` evidence on its follow-up): when you compile it with `kb_compile_intent` in step 11, compare it with the named requirements and send your verdicts in `conflictReviews`. With no answer, stop as "awaiting approval." If declined, replan and ask again with the new hash.
41
41
  7. **Pre-apply check.** The plan you send must equal the returned plan: every top-level field, including `suppressedCandidates`, `diagnostics`, `candidates`, `actions`, and `expected`, with every array the same length and order. Never rebuild it from preview fields. If the host cannot pass the exact object, stop and say so.
@@ -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
 
@@ -51,7 +51,9 @@ writes and opens the page; `--format json` prints the view.
51
51
  The person answers by number ("1 yes, 2 exploratory, 3 reword: ..."). Copy
52
52
  each answer into `answer`, render again, and record the rules only once every
53
53
  question is answered. An answer is the person's words: quote it in
54
- `origin.ref`; a view itself is never an approval.
54
+ `origin.ref`; a view itself is never an approval. Record the answers on the
55
+ branch of the change they approve and push them to its pull request, never to
56
+ a separate one.
55
57
 
56
58
  ## Summary contents
57
59