kibi-agent-core 0.3.0 → 0.4.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.
- package/CHANGELOG.md +18 -0
- package/dist/knowledge-index.d.ts +2 -0
- package/dist/knowledge-index.js +4 -1
- package/dist/snippets.d.ts +26 -3
- package/dist/snippets.js +97 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# kibi-agent-core
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5ad4319: Requirements and ADRs can now say how firmly they were decided, so a later agent knows whether it may change a decision on evidence or must ask a human first. Mark a rule a person stated `firmness: firm` (it must cite their words in `origin.ref`, or `approval_ref` on a requirement), a guess `firmness: exploratory`, and leave the rest unset (`default`). `kibi check` now blocks a firm decision without that reference (Kibi checks that it is present, not that it is genuine), a decision that is not firm superseding a firm one, and a current requirement or ADR that supersedes another without a `## Why this changed` section naming the evidence. `kb_search` and `kb_query` show the firmness of each decision; the editor hooks label firm and exploratory ones.
|
|
8
|
+
|
|
9
|
+
Migration: this is a hard rollout with KB schema 9. Run `kibi migrate --yes` once; it records every supersede edge that predates the rule in the manifest (`supersedesReasonAcknowledged`, as `FROM -> TO`), so those do not block, and an advisory `supersedes-reason-acknowledged` diagnostic counts them. No source file is rewritten and no reason is invented. New supersedes must explain themselves, including a new one on an acknowledged decision.
|
|
10
|
+
|
|
11
|
+
Technical summary: new optional `firmness` property (`firm` | `default` | `exploratory`) on `req` and `adr` in the JSON schema, the `kb_upsert` input schema, the markdown extractor and the Prolog entity schema; the schema rejects it on other types. `kb_upsert` judges firmness as it will be written: an update that omits `firmness` keeps the stored value (also in the compiled store), and a write that leaves a decision firm without a reference is rejected. `decision-firm-superseded` checks every superseder, retired ones included, so a chain cannot launder a firm decision; shard edges from deleted entities are ignored. New rules `decision-firm-unreferenced`, `decision-firm-superseded` and `supersedes-reason-missing` (canonical, blocking) and `supersedes-reason-acknowledged` (info) read authored requirements, ADRs, front-matter `supersedes` links and relationship shards, so they agree before a sync. `LATEST_KB_SCHEMA_VERSION` is 9; the `decision-firmness-v9` migration step and the `supersedes-reason-acknowledge` fragment action write only `.kb/manifest.json`. Search answer rows and `kb_query` results carry the effective firmness of requirements and ADRs; hook snippets append `[firm]` or `[exploratory]`. The `kibi-usage` skill gains a "Decision firmness" section.
|
|
12
|
+
|
|
13
|
+
## 0.3.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
3
21
|
## 0.3.0
|
|
4
22
|
|
|
5
23
|
### 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.
|
package/dist/knowledge-index.js
CHANGED
|
@@ -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;
|
package/dist/snippets.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
76
|
-
* session, at most `maxFiles`
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
182
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
|
278
|
-
* session, at most `maxFiles`
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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(
|
|
365
|
+
snippets.push(text);
|
|
302
366
|
}
|
|
303
367
|
return snippets.length > 0 ? snippets.join("\n\n") : undefined;
|
|
304
368
|
}
|