kibi-agent-core 0.3.0 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # kibi-agent-core
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - dbfe490: Claude Code: when the agent reads a requirement-linked file and then edits it, the edit hook now still says what the lead requirement must keep true (its linked facts and decision) and names the `kb_check` impact call, alongside the symbol the edit lands in; before, the earlier read suppressed everything but that one line, so an agent that read first never saw the facts or the check. A `SessionStart` after a compact or clear resets the session's "already shown" memory, so the next read or edit of a linked file shows its snippet again instead of returning nothing into a context that no longer holds it; a resume keeps the memory. Codex: an `apply_patch` (or `Write`/`Edit`) to a source file no symbol links to a requirement now gets the same one-time discovery note as Claude Code, with the `kb_search` call that finds requirements for it (an unreadable manifest is reported once instead), and `Stop` now reminds of the `kb_check` impact call after `apply_patch` edits, which were not tracked because the patch names its files only in the patch text.
8
+
9
+ Technical summary: `kibi-agent-core/snippets` gains `editKnowledgeUpdate` (focus line, `requirementGroundingLines`, impact-check line), the shared `unownedSourceNote` and `unreadableManifestNote` (re-exported by `kibi-claude`), and an optional `noteFor` callback on `editKnowledgeContext` that claims the same once-per-file slot for a file without a snippet. The Claude session journal gets a `context_reset` event that clears the shown, notice and explored sets but keeps pending edits and their reminders; `dispatchHook` appends it for `source: "compact" | "clear"`. The Codex runner passes `noteFor` from the knowledge index's manifest state and tracks dirty paths (and direct `.kb/` edits) through `extractEditedPaths`, which reads `apply_patch` headers.
10
+
3
11
  ## 0.3.0
4
12
 
5
13
  ### Minor Changes
@@ -71,6 +71,8 @@ export type EntitySummary = {
71
71
  id: string;
72
72
  title?: string;
73
73
  status?: string;
74
+ /** Requirement or ADR firmness as authored (absent reads as default). */
75
+ firmness?: string;
74
76
  /**
75
77
  * Frontmatter `links` (plain string entries read as `relates_to`) merged
76
78
  * with canonical relationship-shard records whose `from` is this entity.
@@ -718,11 +718,14 @@ export function readEntitySummary(workspaceRoot, entityId) {
718
718
  for (const line of lines.slice(1)) {
719
719
  if (line.trim() === "---")
720
720
  break;
721
- const match = /^(title|status):\s*(.*)$/.exec(line);
721
+ const match = /^(title|status|firmness):\s*(.*)$/.exec(line);
722
722
  if (match?.[1] === "title" && match[2])
723
723
  summary.title = unquote(match[2]);
724
724
  if (match?.[1] === "status" && match[2])
725
725
  summary.status = unquote(match[2]);
726
+ // implements REQ-kb-decision-firmness
727
+ if (match?.[1] === "firmness" && match[2])
728
+ summary.firmness = unquote(match[2]);
726
729
  if (/^\S/.test(line)) {
727
730
  flushLink(authored, pending);
728
731
  pending = undefined;
@@ -24,8 +24,16 @@ export declare function focusedSymbols(symbols: readonly IndexedSymbol[], focus:
24
24
  export declare function editFocus(absolutePath: string, toolInput: unknown): LineRange[] | undefined;
25
25
  /** True for a status that marks an entity as no longer current policy. */
26
26
  export declare function isRetiredStatus(status: string | undefined): boolean;
27
- /** `ID (status): title`, with the status shown only when it is retired. */
27
+ /**
28
+ * `ID (status) [firmness]: title`, with the status shown only when it is
29
+ * retired and the firmness only when it is firm or exploratory (unlabeled
30
+ * decisions are `default`).
31
+ */
28
32
  export declare function describeEntity(summary: EntitySummary): string;
33
+ /** Discovery note for a source file none of whose symbols implement a requirement. */
34
+ export declare function unownedSourceNote(relativePath: string): string;
35
+ /** Shown instead of the discovery note when the manifest itself could not be read. */
36
+ export declare function unreadableManifestNote(relativePath: string): string;
29
37
  /** Link types through which a requirement is grounded in semantic facts. */
30
38
  export declare const GROUNDING_LINK_TYPES: readonly string[];
31
39
  /**
@@ -49,6 +57,15 @@ export declare function createEntitySummarizer(workspaceRoot: string): Summarize
49
57
  * when the manifest links nothing to it.
50
58
  */
51
59
  export declare function fileKnowledgeSnippet(input: SnippetInput): string | undefined;
60
+ /**
61
+ * Reduced edit snippet for a file whose descriptive snippet (requirements,
62
+ * symbols, tests, next-layer calls) was already shown this session: which
63
+ * symbol the edit lands in, what the lead requirement must keep true and the
64
+ * decision behind it, and the impact check. The descriptive parts are not
65
+ * repeated. Undefined when no symbol of the file implements a requirement
66
+ * and the edit lands in no symbol.
67
+ */
68
+ export declare function editKnowledgeUpdate(input: SnippetInput): string | undefined;
52
69
  /**
53
70
  * One-line focus update for a file whose snippet was already shown this
54
71
  * session: only the new fact (which symbol the edit lands in) is repeated.
@@ -69,11 +86,17 @@ export type EditKnowledgeInput = {
69
86
  /** Most files described in one call (an `apply_patch` may touch many). */
70
87
  maxFiles?: number | undefined;
71
88
  maxChars?: number | undefined;
89
+ /**
90
+ * Note for a file that gets no snippet (for example an unlinked source
91
+ * file), shown under the same once-per-file-per-session rule.
92
+ */
93
+ noteFor?: ((relativePath: string) => string | undefined) | undefined;
72
94
  };
73
95
  /**
74
96
  * Pre-edit context for hosts without their own snippet memory: the shared
75
- * edit snippet for each linked file the call targets, shown once per file per
76
- * session, at most `maxFiles` per call.
97
+ * edit snippet for each linked file the call targets (or `noteFor`'s note for
98
+ * a file without one), shown once per file per session, at most `maxFiles`
99
+ * per call.
77
100
  */
78
101
  export declare function editKnowledgeContext(input: EditKnowledgeInput): string | undefined;
79
102
  /**
package/dist/snippets.js CHANGED
@@ -100,10 +100,18 @@ const RETIRED_STATUS = /supersed|deprecat|reject|obsolete|retired/i;
100
100
  export function isRetiredStatus(status) {
101
101
  return status !== undefined && RETIRED_STATUS.test(status);
102
102
  }
103
- // implements REQ-agent-core-edit-snippets, REQ-claude-code-kibi-plugin-v1, REQ-opencode-kibi-plugin-v1
104
- /** `ID (status): title`, with the status shown only when it is retired. */
103
+ // implements REQ-agent-core-edit-snippets, REQ-claude-code-kibi-plugin-v1, REQ-opencode-kibi-plugin-v1, REQ-kb-decision-firmness
104
+ /**
105
+ * `ID (status) [firmness]: title`, with the status shown only when it is
106
+ * retired and the firmness only when it is firm or exploratory (unlabeled
107
+ * decisions are `default`).
108
+ */
105
109
  export function describeEntity(summary) {
106
- const notable = isRetiredStatus(summary.status) ? ` (${summary.status})` : "";
110
+ const retired = isRetiredStatus(summary.status) ? ` (${summary.status})` : "";
111
+ const firmness = summary.firmness === "firm" || summary.firmness === "exploratory"
112
+ ? ` [${summary.firmness}]`
113
+ : "";
114
+ const notable = `${retired}${firmness}`;
107
115
  return summary.title
108
116
  ? `${summary.id}${notable}: ${truncate(summary.title, MAX_TITLE)}`
109
117
  : `${summary.id}${notable}`;
@@ -111,6 +119,26 @@ export function describeEntity(summary) {
111
119
  function jsonString(value) {
112
120
  return JSON.stringify(value);
113
121
  }
122
+ // implements REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1
123
+ /** Discovery note for a source file none of whose symbols implement a requirement. */
124
+ export function unownedSourceNote(relativePath) {
125
+ return [
126
+ `Kibi: no symbol in ${relativePath} is linked to a requirement yet.`,
127
+ `kb_search({query:"<behavior being changed>", sourceLocations:[{path:${jsonString(relativePath)}}]}) surfaces requirements that may already describe it; new behavior is recorded with kb_upsert (requirement + symbol implements link).`,
128
+ ].join("\n");
129
+ }
130
+ // implements REQ-kibi-hooks-flow-manifest, REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1
131
+ /** Shown instead of the discovery note when the manifest itself could not be read. */
132
+ export function unreadableManifestNote(relativePath) {
133
+ return [
134
+ "Kibi: the hook could not read .kb/symbols.yaml, so requirement snippets are unavailable this session.",
135
+ `kb_query({sourceFile:${jsonString(relativePath)}}) still lists what ${relativePath} implements; \`kibi sync\` rewrites the manifest in the canonical block layout.`,
136
+ ].join("\n");
137
+ }
138
+ /** The impact-check line that closes every edit snippet. */
139
+ function impactCheckLine(relativePath) {
140
+ return `Behavior changes here are traced to these requirements; kb_check({sourceFiles:[${jsonString(relativePath)}], includeImpactDiagnostics:true, includeWorkingTreeDiff:true}) reports ownership drift after the edit.`;
141
+ }
114
142
  // implements REQ-agent-core-edit-snippets, REQ-claude-code-kibi-plugin-v1, REQ-opencode-kibi-plugin-v1
115
143
  /** Link types through which a requirement is grounded in semantic facts. */
116
144
  export const GROUNDING_LINK_TYPES = [
@@ -176,15 +204,11 @@ export function createEntitySummarizer(workspaceRoot) {
176
204
  return summary;
177
205
  };
178
206
  }
179
- // implements REQ-agent-core-edit-snippets, REQ-claude-code-kibi-plugin-v1, REQ-cursor-kibi-plugin-v1, REQ-codex-kibi-plugin-v1, REQ-zcode-kibi-plugin-v1
180
207
  /**
181
- * Knowledge snippet for a file with requirement-linked symbols, or undefined
182
- * when the manifest links nothing to it.
208
+ * Requirement -> symbol titles, with the requirement IDs ranked so the focus
209
+ * symbols' owners lead, then the requirements with the most symbols.
183
210
  */
184
- export function fileKnowledgeSnippet(input) {
185
- const { relativePath, symbols, surface, summarize } = input;
186
- const focus = focusedSymbols(symbols, input.focus);
187
- // Requirement -> symbol titles, focus symbols first so their owners lead.
211
+ function requirementOwners(symbols, focus) {
188
212
  const owners = new Map();
189
213
  const ordered = [
190
214
  ...focus,
@@ -198,6 +222,24 @@ export function fileKnowledgeSnippet(input) {
198
222
  owners.set(requirement, titles);
199
223
  }
200
224
  }
225
+ const focusOwners = new Set(focus.flatMap((symbol) => symbol.implements));
226
+ const requirementIds = [...owners.keys()].sort((left, right) => {
227
+ const focusOrder = Number(focusOwners.has(right)) - Number(focusOwners.has(left));
228
+ return focusOrder !== 0
229
+ ? focusOrder
230
+ : (owners.get(right)?.length ?? 0) - (owners.get(left)?.length ?? 0);
231
+ });
232
+ return { owners, requirementIds };
233
+ }
234
+ // implements REQ-agent-core-edit-snippets, REQ-claude-code-kibi-plugin-v1, REQ-cursor-kibi-plugin-v1, REQ-codex-kibi-plugin-v1, REQ-zcode-kibi-plugin-v1
235
+ /**
236
+ * Knowledge snippet for a file with requirement-linked symbols, or undefined
237
+ * when the manifest links nothing to it.
238
+ */
239
+ export function fileKnowledgeSnippet(input) {
240
+ const { relativePath, symbols, surface, summarize } = input;
241
+ const focus = focusedSymbols(symbols, input.focus);
242
+ const { owners, requirementIds } = requirementOwners(symbols, focus);
201
243
  const tests = [
202
244
  ...new Set(symbols.flatMap((symbol) => symbol.coveredBy)),
203
245
  ].sort();
@@ -207,13 +249,6 @@ export function fileKnowledgeSnippet(input) {
207
249
  if (owners.size === 0 && tests.length === 0 && executes.length === 0) {
208
250
  return undefined;
209
251
  }
210
- const focusOwners = new Set(focus.flatMap((symbol) => symbol.implements));
211
- const requirementIds = [...owners.keys()].sort((left, right) => {
212
- const focusOrder = Number(focusOwners.has(right)) - Number(focusOwners.has(left));
213
- return focusOrder !== 0
214
- ? focusOrder
215
- : (owners.get(right)?.length ?? 0) - (owners.get(left)?.length ?? 0);
216
- });
217
252
  const lines = [
218
253
  `Kibi knowledge for ${relativePath} (symbol manifest):`,
219
254
  ];
@@ -252,9 +287,36 @@ export function fileKnowledgeSnippet(input) {
252
287
  `kb_search({query:"<topic>", sourceLocations:[${location}]}) answers with governing requirements, facts, decisions, and tests`,
253
288
  ].filter((part) => part !== undefined);
254
289
  lines.push(`Next layer: ${next.join("; ")}.`);
255
- if (surface === "edit") {
256
- lines.push(`Behavior changes here are traced to these requirements; kb_check({sourceFiles:[${jsonString(relativePath)}], includeImpactDiagnostics:true, includeWorkingTreeDiff:true}) reports ownership drift after the edit.`);
290
+ if (surface === "edit")
291
+ lines.push(impactCheckLine(relativePath));
292
+ return truncate(lines.join("\n"), input.maxChars ?? MAX_SNIPPET_CHARS);
293
+ }
294
+ // implements REQ-claude-code-kibi-plugin-v1
295
+ /**
296
+ * Reduced edit snippet for a file whose descriptive snippet (requirements,
297
+ * symbols, tests, next-layer calls) was already shown this session: which
298
+ * symbol the edit lands in, what the lead requirement must keep true and the
299
+ * decision behind it, and the impact check. The descriptive parts are not
300
+ * repeated. Undefined when no symbol of the file implements a requirement
301
+ * and the edit lands in no symbol.
302
+ */
303
+ export function editKnowledgeUpdate(input) {
304
+ const { relativePath, symbols, summarize } = input;
305
+ const focus = focusedSymbols(symbols, input.focus);
306
+ const { requirementIds } = requirementOwners(symbols, focus);
307
+ const leadId = requirementIds[0];
308
+ const lines = [];
309
+ const focused = focusUpdate(relativePath, symbols, input.focus);
310
+ if (focused) {
311
+ lines.push(focused);
257
312
  }
313
+ else if (leadId) {
314
+ lines.push(`Kibi: this edit to ${relativePath} changes code that implements ${formatList(requirementIds, 2)}.`);
315
+ }
316
+ if (!leadId)
317
+ return lines[0];
318
+ lines.push(...requirementGroundingLines(leadId, summarize));
319
+ lines.push(impactCheckLine(relativePath));
258
320
  return truncate(lines.join("\n"), input.maxChars ?? MAX_SNIPPET_CHARS);
259
321
  }
260
322
  // implements REQ-claude-code-kibi-plugin-v1
@@ -274,8 +336,9 @@ export function focusUpdate(relativePath, symbols, focus) {
274
336
  // implements REQ-agent-core-edit-snippets, REQ-codex-kibi-plugin-v1, REQ-zcode-kibi-plugin-v1
275
337
  /**
276
338
  * Pre-edit context for hosts without their own snippet memory: the shared
277
- * edit snippet for each linked file the call targets, shown once per file per
278
- * session, at most `maxFiles` per call.
339
+ * edit snippet for each linked file the call targets (or `noteFor`'s note for
340
+ * a file without one), shown once per file per session, at most `maxFiles`
341
+ * per call.
279
342
  */
280
343
  export function editKnowledgeContext(input) {
281
344
  const snippets = [];
@@ -283,22 +346,23 @@ export function editKnowledgeContext(input) {
283
346
  if (snippets.length >= (input.maxFiles ?? 3))
284
347
  break;
285
348
  const symbols = input.symbolsFor(relativePath);
286
- if (symbols.length === 0)
287
- continue;
288
- const snippet = fileKnowledgeSnippet({
289
- relativePath,
290
- symbols,
291
- surface: "edit",
292
- focus: input.focusFor?.(relativePath),
293
- summarize: input.summarize,
294
- maxChars: input.maxChars,
295
- });
296
- if (!snippet)
349
+ const snippet = symbols.length > 0
350
+ ? fileKnowledgeSnippet({
351
+ relativePath,
352
+ symbols,
353
+ surface: "edit",
354
+ focus: input.focusFor?.(relativePath),
355
+ summarize: input.summarize,
356
+ maxChars: input.maxChars,
357
+ })
358
+ : undefined;
359
+ const text = snippet ?? input.noteFor?.(relativePath);
360
+ if (!text)
297
361
  continue;
298
362
  const key = `${input.sessionId ?? ""}\u0000edit\u0000${relativePath}`;
299
363
  if (!claimSnippetSlot(input.stateDir, key))
300
364
  continue;
301
- snippets.push(snippet);
365
+ snippets.push(text);
302
366
  }
303
367
  return snippets.length > 0 ? snippets.join("\n\n") : undefined;
304
368
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-agent-core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Shared low-latency hook helpers for Kibi agent adapters",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",