kibi-agent-core 0.4.0 → 0.6.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 +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/knowledge-index.d.ts +5 -1
- package/dist/knowledge-index.js +22 -1
- package/dist/requirement-candidates.d.ts +24 -0
- package/dist/requirement-candidates.js +225 -0
- package/dist/snippets.d.ts +11 -4
- package/dist/snippets.js +34 -12
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# kibi-agent-core
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d960810: A file with no linked symbols is no longer read as a file no rule covers. Before such a file is edited, the Claude Code and Codex hooks name the current requirements whose titles share the file's path words, and a `kb_check` scoped to the file reports the same candidates, so an agent changing `coach-dashboard.html` is pointed at the coach inbox requirement instead of concluding there is no knowledge-base impact. Agents are also told that recording the behavior a change adds is part of finishing it and needs no permission.
|
|
8
|
+
- `kibi-agent-core`: new `requirement-candidates` module (`pathTopicWords`, `rankRequirementCandidates`, `requirementCandidatesForPath`); `unownedSourceNote` takes the candidates, says that no link does not mean no rule, and drops candidates rather than exceed the snippet budget.
|
|
9
|
+
- `kibi-cli`: an impact-enabled `kb_check` emits `source_file_unowned` (`info`, non-blocking) for each file in `sourceFiles` that no manifest symbol links to a requirement, including files Kibi does not extract symbols from (HTML templates), with `evidence.candidates`. The unlinked-symbol `symbol_semantic_review_needed` suggestion now says recording the behavior needs no permission.
|
|
10
|
+
- `kibi-claude`, `kibi-codex`: the unlinked-file edit note names candidate requirements. The Claude Code session-start context no longer says unlinked files get no context, and says recording new behavior needs no permission; ZCode's session-start context says the same.
|
|
11
|
+
- `kibi-usage` skill 2.19.0: record new behavior without asking; confirm the candidates an unlinked-file note names.
|
|
12
|
+
|
|
13
|
+
## 0.5.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- cd4723e: Rules a person wrote or imported are no longer treated as an agent's own default choice when they carry no `firmness`. A requirement or ADR without `firmness` now reads as `firm` unless an agent wrote it, so an agent asks before replacing it, and bootstrap records the rules it imports from your documents as firm with a reference to the passage. The usage skill now treats any request that conflicts with a recorded rule as a question: the agent names the rule and asks, whatever the rule's firmness, and changes it only when the person says so. Run `kibi migrate --yes` to move a KB to schema 10; existing supersedes are acknowledged and no source file is rewritten.
|
|
18
|
+
- `effectiveFirmness` takes the entity's origin: an absent value is `default` for `origin.kind: agent` and `firm` otherwise (human, import, migration or no origin). `kb_query` rows and `kb_search` answers show the derived value.
|
|
19
|
+
- Hook snippets label a current firm decision `[firm: ask before changing]` (was `[firm]`) and no longer label retired decisions. The Claude Code and ZCode session-start context adds one sentence: a request that conflicts with a recorded requirement or ADR is a question, so the agent names the rule and asks before changing the code or the rule.
|
|
20
|
+
- `decision-firm-superseded` applies to origin-derived firm targets; `decision-firm-unreferenced` still applies only to an explicit `firmness: firm`.
|
|
21
|
+
- KB schema 10: the `firmness-from-origin-v10` step and the automatic `firm_supersede_acknowledge` plan action record pre-existing supersedes of such decisions in `.kb/manifest.json` `firmSupersedeAcknowledged` (`FROM -> TO`); only listed edges are exempt, and never an explicitly firm target.
|
|
22
|
+
- Bootstrap intent-claim and normative-markdown requirement steps write `origin: {kind: import, ref}` and `firmness: firm`.
|
|
23
|
+
- Skills: `kibi-usage` 2.17.0 (conflicting request means ask; firmness from origin; write firmness explicitly), `kibi-bootstrap` 3.13.0 (imported rules are firm imports, including hand-authored follow-ups).
|
|
24
|
+
|
|
3
25
|
## 0.4.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -71,7 +71,11 @@ export type EntitySummary = {
|
|
|
71
71
|
id: string;
|
|
72
72
|
title?: string;
|
|
73
73
|
status?: string;
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Requirement or ADR firmness: as authored, else what the origin implies
|
|
76
|
+
* (`default` when an agent wrote it, `firm` otherwise). Absent when the
|
|
77
|
+
* front matter could not be read whole.
|
|
78
|
+
*/
|
|
75
79
|
firmness?: string;
|
|
76
80
|
/**
|
|
77
81
|
* Frontmatter `links` (plain string entries read as `relates_to`) merged
|
package/dist/knowledge-index.js
CHANGED
|
@@ -714,10 +714,24 @@ export function readEntitySummary(workspaceRoot, entityId) {
|
|
|
714
714
|
const summary = { id: entityId };
|
|
715
715
|
const authored = [];
|
|
716
716
|
let inLinks = false;
|
|
717
|
+
let inOrigin = false;
|
|
718
|
+
let originKind;
|
|
719
|
+
let closed = false;
|
|
717
720
|
let pending;
|
|
718
721
|
for (const line of lines.slice(1)) {
|
|
719
|
-
if (line.trim() === "---")
|
|
722
|
+
if (line.trim() === "---") {
|
|
723
|
+
closed = true;
|
|
720
724
|
break;
|
|
725
|
+
}
|
|
726
|
+
// implements REQ-kb-decision-firmness-from-origin
|
|
727
|
+
const flowOrigin = /^origin:\s*\{.*\bkind:\s*["']?([a-z]+)/.exec(line);
|
|
728
|
+
if (flowOrigin)
|
|
729
|
+
originKind = flowOrigin[1];
|
|
730
|
+
const nestedKind = inOrigin ? /^\s+kind:\s*["']?([a-z]+)/.exec(line) : null;
|
|
731
|
+
if (nestedKind)
|
|
732
|
+
originKind = nestedKind[1];
|
|
733
|
+
if (/^\S/.test(line))
|
|
734
|
+
inOrigin = /^origin:\s*$/.test(line);
|
|
721
735
|
const match = /^(title|status|firmness):\s*(.*)$/.exec(line);
|
|
722
736
|
if (match?.[1] === "title" && match[2])
|
|
723
737
|
summary.title = unquote(match[2]);
|
|
@@ -754,6 +768,13 @@ export function readEntitySummary(workspaceRoot, entityId) {
|
|
|
754
768
|
pending.bare = unquote(body);
|
|
755
769
|
}
|
|
756
770
|
flushLink(authored, pending);
|
|
771
|
+
// A decision without authored firmness is a person's (firm) unless an
|
|
772
|
+
// agent wrote it; with the front matter cut off, the origin is unknown.
|
|
773
|
+
// implements REQ-kb-decision-firmness-from-origin
|
|
774
|
+
if (summary.firmness === undefined &&
|
|
775
|
+
closed &&
|
|
776
|
+
(lane[1] === "requirements" || lane[1] === "adr"))
|
|
777
|
+
summary.firmness = originKind === "agent" ? "default" : "firm";
|
|
757
778
|
const links = [];
|
|
758
779
|
const seen = new Set();
|
|
759
780
|
for (const link of [
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const MAX_REQUIREMENT_CANDIDATES = 3;
|
|
2
|
+
/**
|
|
3
|
+
* The topic words of a workspace path: its directory and file-name words,
|
|
4
|
+
* without layout, language and role words (`src/app/pages/coach-dashboard/
|
|
5
|
+
* coach-dashboard.html` -> `coach`, `dashboard`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function pathTopicWords(relativePath: string): string[];
|
|
8
|
+
export type RequirementHead = {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Current requirements ranked by how many of the path's topic words their
|
|
15
|
+
* title shares, then by how often the title uses them, then by id. Only
|
|
16
|
+
* requirements sharing at least one word are returned.
|
|
17
|
+
*/
|
|
18
|
+
export declare function rankRequirementCandidates(relativePath: string, requirements: readonly RequirementHead[], limit?: number): RequirementHead[];
|
|
19
|
+
/**
|
|
20
|
+
* Candidate requirements for a file no symbol links, read from the canonical
|
|
21
|
+
* requirement documents (`.kb/requirements/*.md`). Empty when the KB has none
|
|
22
|
+
* or is too large to scan from a hook.
|
|
23
|
+
*/
|
|
24
|
+
export declare function requirementCandidatesForPath(workspaceRoot: string, relativePath: string, limit?: number): RequirementHead[];
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// implements REQ-kb-unowned-file-candidates
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Requirements that may govern a file no symbol links yet, found by the words
|
|
6
|
+
* its path shares with their titles. A file without links is not a file
|
|
7
|
+
* without rules: `coach-dashboard.html` may implement "The coach inbox must
|
|
8
|
+
* list the analysis requests assigned to the coach." The match is lexical and
|
|
9
|
+
* cheap enough for a hook; it names candidates to confirm, never owners.
|
|
10
|
+
*/
|
|
11
|
+
const REQUIREMENTS_DIR = path.join(".kb", "requirements");
|
|
12
|
+
/** Bytes of each requirement read for its front matter. */
|
|
13
|
+
const HEAD_BYTES = 4096;
|
|
14
|
+
/** A KB larger than this is not scanned from a hook. */
|
|
15
|
+
const MAX_SCANNED = 2000;
|
|
16
|
+
export const MAX_REQUIREMENT_CANDIDATES = 3;
|
|
17
|
+
/** Path words that name a layout, a language or a role, not a behavior. */
|
|
18
|
+
const LAYOUT_WORDS = new Set([
|
|
19
|
+
"src",
|
|
20
|
+
"app",
|
|
21
|
+
"apps",
|
|
22
|
+
"lib",
|
|
23
|
+
"libs",
|
|
24
|
+
"source",
|
|
25
|
+
"page",
|
|
26
|
+
"pages",
|
|
27
|
+
"component",
|
|
28
|
+
"components",
|
|
29
|
+
"view",
|
|
30
|
+
"views",
|
|
31
|
+
"screen",
|
|
32
|
+
"screens",
|
|
33
|
+
"package",
|
|
34
|
+
"packages",
|
|
35
|
+
"main",
|
|
36
|
+
"index",
|
|
37
|
+
"shared",
|
|
38
|
+
"common",
|
|
39
|
+
"core",
|
|
40
|
+
"util",
|
|
41
|
+
"utils",
|
|
42
|
+
"helper",
|
|
43
|
+
"helpers",
|
|
44
|
+
"public",
|
|
45
|
+
"static",
|
|
46
|
+
"asset",
|
|
47
|
+
"assets",
|
|
48
|
+
"web",
|
|
49
|
+
"client",
|
|
50
|
+
"server",
|
|
51
|
+
"frontend",
|
|
52
|
+
"backend",
|
|
53
|
+
"module",
|
|
54
|
+
"modules",
|
|
55
|
+
"feature",
|
|
56
|
+
"features",
|
|
57
|
+
"internal",
|
|
58
|
+
"service",
|
|
59
|
+
"services",
|
|
60
|
+
"test",
|
|
61
|
+
"tests",
|
|
62
|
+
"spec",
|
|
63
|
+
"specs",
|
|
64
|
+
"dist",
|
|
65
|
+
"build",
|
|
66
|
+
"html",
|
|
67
|
+
"htm",
|
|
68
|
+
"tsx",
|
|
69
|
+
"jsx",
|
|
70
|
+
"mjs",
|
|
71
|
+
"cjs",
|
|
72
|
+
"vue",
|
|
73
|
+
"svelte",
|
|
74
|
+
"css",
|
|
75
|
+
"scss",
|
|
76
|
+
"less",
|
|
77
|
+
"json",
|
|
78
|
+
"yaml",
|
|
79
|
+
"yml",
|
|
80
|
+
"java",
|
|
81
|
+
"swift",
|
|
82
|
+
"php",
|
|
83
|
+
]);
|
|
84
|
+
/** Words of a title that carry no topic. */
|
|
85
|
+
const FILLER_WORDS = new Set([
|
|
86
|
+
"the",
|
|
87
|
+
"and",
|
|
88
|
+
"for",
|
|
89
|
+
"with",
|
|
90
|
+
"must",
|
|
91
|
+
"should",
|
|
92
|
+
"shall",
|
|
93
|
+
"may",
|
|
94
|
+
"can",
|
|
95
|
+
"are",
|
|
96
|
+
"is",
|
|
97
|
+
"its",
|
|
98
|
+
"their",
|
|
99
|
+
"that",
|
|
100
|
+
"this",
|
|
101
|
+
"from",
|
|
102
|
+
"into",
|
|
103
|
+
"when",
|
|
104
|
+
"each",
|
|
105
|
+
"every",
|
|
106
|
+
"all",
|
|
107
|
+
"not",
|
|
108
|
+
"req",
|
|
109
|
+
"auto",
|
|
110
|
+
]);
|
|
111
|
+
/** Lowercase word stems of an identifier, path segment or title. */
|
|
112
|
+
function words(text) {
|
|
113
|
+
return text
|
|
114
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.split(/[^a-z0-9]+/)
|
|
117
|
+
.filter((word) => word.length >= 3 && !/\d/.test(word))
|
|
118
|
+
.map((word) => word.length > 4 && word.endsWith("s") && !word.endsWith("ss")
|
|
119
|
+
? word.slice(0, -1)
|
|
120
|
+
: word);
|
|
121
|
+
}
|
|
122
|
+
// implements REQ-kb-unowned-file-candidates
|
|
123
|
+
/**
|
|
124
|
+
* The topic words of a workspace path: its directory and file-name words,
|
|
125
|
+
* without layout, language and role words (`src/app/pages/coach-dashboard/
|
|
126
|
+
* coach-dashboard.html` -> `coach`, `dashboard`).
|
|
127
|
+
*/
|
|
128
|
+
export function pathTopicWords(relativePath) {
|
|
129
|
+
const segments = relativePath.split(/[\\/]/).filter(Boolean);
|
|
130
|
+
const last = segments.pop() ?? "";
|
|
131
|
+
const stem = last.includes(".") ? last.slice(0, last.indexOf(".")) : last;
|
|
132
|
+
const roles = last.includes(".") ? last.slice(last.indexOf(".") + 1) : "";
|
|
133
|
+
const found = [...segments, stem, roles]
|
|
134
|
+
.flatMap(words)
|
|
135
|
+
.filter((word) => !LAYOUT_WORDS.has(word) && !FILLER_WORDS.has(word));
|
|
136
|
+
return [...new Set(found)];
|
|
137
|
+
}
|
|
138
|
+
function unquote(value) {
|
|
139
|
+
const trimmed = value.trim();
|
|
140
|
+
return /^(["']).*\1$/.test(trimmed) ? trimmed.slice(1, -1) : trimmed;
|
|
141
|
+
}
|
|
142
|
+
function readHead(file) {
|
|
143
|
+
let head;
|
|
144
|
+
try {
|
|
145
|
+
const descriptor = fs.openSync(file, "r");
|
|
146
|
+
try {
|
|
147
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
148
|
+
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
149
|
+
head = buffer.subarray(0, bytes).toString("utf8");
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
fs.closeSync(descriptor);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const lines = head.split(/\r?\n/);
|
|
159
|
+
if (lines[0]?.trim() !== "---")
|
|
160
|
+
return undefined;
|
|
161
|
+
const fields = {};
|
|
162
|
+
for (const line of lines.slice(1)) {
|
|
163
|
+
if (line.trim() === "---")
|
|
164
|
+
break;
|
|
165
|
+
const match = /^(id|title|status):\s*(.+)$/.exec(line);
|
|
166
|
+
if (match?.[1] && match[2])
|
|
167
|
+
fields[match[1]] = unquote(match[2]);
|
|
168
|
+
}
|
|
169
|
+
if (!fields.id || !fields.title)
|
|
170
|
+
return undefined;
|
|
171
|
+
return {
|
|
172
|
+
id: fields.id,
|
|
173
|
+
title: fields.title,
|
|
174
|
+
...(fields.status ? { status: fields.status } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const RETIRED_STATUS = /supersed|deprecat|reject|obsolete|retired|archived|removed/i;
|
|
178
|
+
// implements REQ-kb-unowned-file-candidates
|
|
179
|
+
/**
|
|
180
|
+
* Current requirements ranked by how many of the path's topic words their
|
|
181
|
+
* title shares, then by how often the title uses them, then by id. Only
|
|
182
|
+
* requirements sharing at least one word are returned.
|
|
183
|
+
*/
|
|
184
|
+
export function rankRequirementCandidates(relativePath, requirements, limit = MAX_REQUIREMENT_CANDIDATES) {
|
|
185
|
+
const topic = new Set(pathTopicWords(relativePath));
|
|
186
|
+
if (topic.size === 0)
|
|
187
|
+
return [];
|
|
188
|
+
const scored = [];
|
|
189
|
+
for (const head of requirements) {
|
|
190
|
+
if (head.status !== undefined && RETIRED_STATUS.test(head.status))
|
|
191
|
+
continue;
|
|
192
|
+
const titleWords = words(head.title).filter((word) => topic.has(word));
|
|
193
|
+
const shared = new Set(titleWords).size;
|
|
194
|
+
if (shared > 0)
|
|
195
|
+
scored.push({ head, shared, uses: titleWords.length });
|
|
196
|
+
}
|
|
197
|
+
scored.sort((a, b) => b.shared - a.shared ||
|
|
198
|
+
b.uses - a.uses ||
|
|
199
|
+
a.head.id.localeCompare(b.head.id));
|
|
200
|
+
return scored.slice(0, limit).map((entry) => entry.head);
|
|
201
|
+
}
|
|
202
|
+
// implements REQ-kb-unowned-file-candidates
|
|
203
|
+
/**
|
|
204
|
+
* Candidate requirements for a file no symbol links, read from the canonical
|
|
205
|
+
* requirement documents (`.kb/requirements/*.md`). Empty when the KB has none
|
|
206
|
+
* or is too large to scan from a hook.
|
|
207
|
+
*/
|
|
208
|
+
export function requirementCandidatesForPath(workspaceRoot, relativePath, limit = MAX_REQUIREMENT_CANDIDATES) {
|
|
209
|
+
if (pathTopicWords(relativePath).length === 0)
|
|
210
|
+
return [];
|
|
211
|
+
const directory = path.join(workspaceRoot, REQUIREMENTS_DIR);
|
|
212
|
+
let names;
|
|
213
|
+
try {
|
|
214
|
+
names = fs.readdirSync(directory).filter((name) => name.endsWith(".md"));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
if (names.length > MAX_SCANNED)
|
|
220
|
+
return [];
|
|
221
|
+
const heads = names
|
|
222
|
+
.map((name) => readHead(path.join(directory, name)))
|
|
223
|
+
.filter((head) => head !== undefined);
|
|
224
|
+
return rankRequirementCandidates(relativePath, heads, limit);
|
|
225
|
+
}
|
package/dist/snippets.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type EntitySummary, type IndexedSymbol } from "./knowledge-index.js";
|
|
2
|
+
import type { RequirementHead } from "./requirement-candidates.js";
|
|
2
3
|
export declare const MAX_SNIPPET_CHARS = 1200;
|
|
3
4
|
export type LineRange = {
|
|
4
5
|
start: number;
|
|
@@ -26,12 +27,18 @@ export declare function editFocus(absolutePath: string, toolInput: unknown): Lin
|
|
|
26
27
|
export declare function isRetiredStatus(status: string | undefined): boolean;
|
|
27
28
|
/**
|
|
28
29
|
* `ID (status) [firmness]: title`, with the status shown only when it is
|
|
29
|
-
* retired and the firmness only
|
|
30
|
-
*
|
|
30
|
+
* retired and the firmness only on a current decision: `[firm: ask before
|
|
31
|
+
* changing]` says what a firm rule asks of the agent where it reads it, and
|
|
32
|
+
* `[exploratory]` marks a guess. An unlabeled current decision is `default`,
|
|
33
|
+
* an agent's choice.
|
|
31
34
|
*/
|
|
32
35
|
export declare function describeEntity(summary: EntitySummary): string;
|
|
33
|
-
/**
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Discovery note for a source file none of whose symbols implement a
|
|
38
|
+
* requirement. No link is not no rule, so the note names the current
|
|
39
|
+
* requirements whose titles share the path's words, when there are any.
|
|
40
|
+
*/
|
|
41
|
+
export declare function unownedSourceNote(relativePath: string, candidates?: readonly RequirementHead[]): string;
|
|
35
42
|
/** Shown instead of the discovery note when the manifest itself could not be read. */
|
|
36
43
|
export declare function unreadableManifestNote(relativePath: string): string;
|
|
37
44
|
/** Link types through which a requirement is grounded in semantic facts. */
|
package/dist/snippets.js
CHANGED
|
@@ -100,16 +100,22 @@ 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, REQ-kb-decision-firmness
|
|
103
|
+
// implements REQ-agent-core-edit-snippets, REQ-claude-code-kibi-plugin-v1, REQ-opencode-kibi-plugin-v1, REQ-kb-decision-firmness, REQ-kb-decision-firmness-from-origin
|
|
104
104
|
/**
|
|
105
105
|
* `ID (status) [firmness]: title`, with the status shown only when it is
|
|
106
|
-
* retired and the firmness only
|
|
107
|
-
*
|
|
106
|
+
* retired and the firmness only on a current decision: `[firm: ask before
|
|
107
|
+
* changing]` says what a firm rule asks of the agent where it reads it, and
|
|
108
|
+
* `[exploratory]` marks a guess. An unlabeled current decision is `default`,
|
|
109
|
+
* an agent's choice.
|
|
108
110
|
*/
|
|
109
111
|
export function describeEntity(summary) {
|
|
110
112
|
const retired = isRetiredStatus(summary.status) ? ` (${summary.status})` : "";
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
// A retired decision binds nothing, however firmly it was once made.
|
|
114
|
+
const firmness = !retired &&
|
|
115
|
+
(summary.firmness === "firm" || summary.firmness === "exploratory")
|
|
116
|
+
? summary.firmness === "firm"
|
|
117
|
+
? " [firm: ask before changing]"
|
|
118
|
+
: " [exploratory]"
|
|
113
119
|
: "";
|
|
114
120
|
const notable = `${retired}${firmness}`;
|
|
115
121
|
return summary.title
|
|
@@ -119,13 +125,29 @@ export function describeEntity(summary) {
|
|
|
119
125
|
function jsonString(value) {
|
|
120
126
|
return JSON.stringify(value);
|
|
121
127
|
}
|
|
122
|
-
// implements REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
// implements REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1, REQ-kb-unowned-file-candidates
|
|
129
|
+
/**
|
|
130
|
+
* Discovery note for a source file none of whose symbols implement a
|
|
131
|
+
* requirement. No link is not no rule, so the note names the current
|
|
132
|
+
* requirements whose titles share the path's words, when there are any.
|
|
133
|
+
*/
|
|
134
|
+
export function unownedSourceNote(relativePath, candidates = []) {
|
|
135
|
+
const head = `Kibi: no symbol in ${relativePath} is linked to a requirement yet; that does not mean no recorded rule covers the behavior it implements.`;
|
|
136
|
+
const search = `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 + scenario + symbol implements link) as part of the change.`;
|
|
137
|
+
// Fewer candidates rather than a note over the snippet budget.
|
|
138
|
+
for (let shown = candidates.length; shown > 0; shown--) {
|
|
139
|
+
const named = candidates
|
|
140
|
+
.slice(0, shown)
|
|
141
|
+
.map((candidate) => `${candidate.id}: ${truncate(candidate.title, MAX_TITLE)}`);
|
|
142
|
+
const note = [
|
|
143
|
+
head,
|
|
144
|
+
`Requirements whose titles share this path's words: ${named.join("; ")}. kb_query({id:${jsonString(candidates[0]?.id ?? "")}}) shows whether one describes the behavior being changed; one that does is traced by an implements link from the symbol that implements it (for markup, the component that renders it).`,
|
|
145
|
+
search,
|
|
146
|
+
].join("\n");
|
|
147
|
+
if (note.length <= MAX_SNIPPET_CHARS)
|
|
148
|
+
return note;
|
|
149
|
+
}
|
|
150
|
+
return [head, search].join("\n");
|
|
129
151
|
}
|
|
130
152
|
// implements REQ-kibi-hooks-flow-manifest, REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1
|
|
131
153
|
/** Shown instead of the discovery note when the manifest itself could not be read. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-agent-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Shared low-latency hook helpers for Kibi agent adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"types": "./dist/path-policy.d.ts",
|
|
27
27
|
"default": "./dist/path-policy.js"
|
|
28
28
|
},
|
|
29
|
+
"./requirement-candidates": {
|
|
30
|
+
"types": "./dist/requirement-candidates.d.ts",
|
|
31
|
+
"default": "./dist/requirement-candidates.js"
|
|
32
|
+
},
|
|
29
33
|
"./snippets": {
|
|
30
34
|
"types": "./dist/snippets.d.ts",
|
|
31
35
|
"default": "./dist/snippets.js"
|