kibi-agent-core 0.6.0 → 0.7.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 +11 -0
- package/dist/requirement-candidates.d.ts +15 -6
- package/dist/requirement-candidates.js +101 -15
- package/dist/snippets.js +12 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# kibi-agent-core
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b9ec91d: The Claude Code hooks now act on recorded decisions while the work happens, not only after it.
|
|
8
|
+
- **Firm requirements are confirmed before the edit.** When an `Edit`, `MultiEdit`, `Write` or in-place shell write lands in a symbol that implements a current firm requirement (marked `firmness: firm`, or written by a person rather than an agent), the `PreToolUse` hook returns `permissionDecision: "ask"` with the symbol and requirement in the prompt, so the person decides before the code changes. Once an edit is allowed, that requirement is not asked about again in the session; a declined edit is asked again. The agent is told that a declined edit means the requirement stands. In a `claude -p` run with nobody to answer, Claude Code refuses the edit and the agent reads the reason. Set `KIBI_CONFIRM_FIRM_EDITS=0` to turn the prompt off.
|
|
9
|
+
- **Stated rules are captured.** A new `UserPromptSubmit` hook tells the agent, when a prompt reads as a rule ("must", "never", "from now on"), that recording it with `kb_upsert` is part of the change. At `Stop`, a session that changed source files and wrote nothing to the knowledge base is asked once to record the decision or say there was none.
|
|
10
|
+
- **An unlinked file is no longer an all-clear.** The discovery note for a file no symbol links ranks candidate requirements by the latest prompt's words as well as the path's, and, when nothing matches, says that nothing recorded is known to cover the file and that a decision made for it is recorded before the work is reported done (Codex shows the same note).
|
|
11
|
+
|
|
12
|
+
`kibi-agent-core`: `requestTopicWords`; `rankRequirementCandidates` and `requirementCandidatesForPath` take optional request words (a requirement sharing only request words needs two of them).
|
|
13
|
+
|
|
3
14
|
## 0.6.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
|
@@ -10,15 +10,24 @@ export type RequirementHead = {
|
|
|
10
10
|
title: string;
|
|
11
11
|
status?: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Topic words of the person's request (`Users must not delete the last
|
|
15
|
+
* owner` -> `user`, `delete`, `last`, `owner`), for ranking requirements an
|
|
16
|
+
* unlinked file's path alone does not reach.
|
|
17
|
+
*/
|
|
18
|
+
export declare function requestTopicWords(text: string, limit?: number): string[];
|
|
13
19
|
/**
|
|
14
20
|
* Current requirements ranked by how many of the path's topic words their
|
|
15
|
-
* title shares, then by how
|
|
16
|
-
*
|
|
21
|
+
* title shares, then by how many of the request's topic words it shares, then
|
|
22
|
+
* by how often the title uses them, then by id. A requirement sharing a path
|
|
23
|
+
* word is returned; one sharing only request words needs two of them, since a
|
|
24
|
+
* request names many words and one in common is chance.
|
|
17
25
|
*/
|
|
18
|
-
export declare function rankRequirementCandidates(relativePath: string, requirements: readonly RequirementHead[], limit?: number): RequirementHead[];
|
|
26
|
+
export declare function rankRequirementCandidates(relativePath: string, requirements: readonly RequirementHead[], limit?: number, requestWords?: readonly string[]): RequirementHead[];
|
|
19
27
|
/**
|
|
20
28
|
* Candidate requirements for a file no symbol links, read from the canonical
|
|
21
|
-
* requirement documents (`.kb/requirements/*.md`)
|
|
22
|
-
*
|
|
29
|
+
* requirement documents (`.kb/requirements/*.md`), ranked by the path's words
|
|
30
|
+
* and, when given, the request's. Empty when the KB has none or is too large
|
|
31
|
+
* to scan from a hook.
|
|
23
32
|
*/
|
|
24
|
-
export declare function requirementCandidatesForPath(workspaceRoot: string, relativePath: string, limit?: number): RequirementHead[];
|
|
33
|
+
export declare function requirementCandidatesForPath(workspaceRoot: string, relativePath: string, limit?: number, requestWords?: readonly string[]): RequirementHead[];
|
|
@@ -175,38 +175,124 @@ function readHead(file) {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
const RETIRED_STATUS = /supersed|deprecat|reject|obsolete|retired|archived|removed/i;
|
|
178
|
-
// implements REQ-
|
|
178
|
+
// implements REQ-claude-request-topic-candidates
|
|
179
|
+
/**
|
|
180
|
+
* Topic words of the person's request (`Users must not delete the last
|
|
181
|
+
* owner` -> `user`, `delete`, `last`, `owner`), for ranking requirements an
|
|
182
|
+
* unlinked file's path alone does not reach.
|
|
183
|
+
*/
|
|
184
|
+
export function requestTopicWords(text, limit = 40) {
|
|
185
|
+
const found = words(text).filter((word) => !FILLER_WORDS.has(word) && !REQUEST_FILLER_WORDS.has(word));
|
|
186
|
+
return [...new Set(found)].slice(0, limit);
|
|
187
|
+
}
|
|
188
|
+
/** Request words that carry no topic of their own. */
|
|
189
|
+
const REQUEST_FILLER_WORDS = new Set([
|
|
190
|
+
"please",
|
|
191
|
+
"could",
|
|
192
|
+
"would",
|
|
193
|
+
"you",
|
|
194
|
+
"your",
|
|
195
|
+
"our",
|
|
196
|
+
"have",
|
|
197
|
+
"has",
|
|
198
|
+
"make",
|
|
199
|
+
"sure",
|
|
200
|
+
"change",
|
|
201
|
+
"update",
|
|
202
|
+
"add",
|
|
203
|
+
"fix",
|
|
204
|
+
"now",
|
|
205
|
+
"need",
|
|
206
|
+
"want",
|
|
207
|
+
"like",
|
|
208
|
+
"also",
|
|
209
|
+
"just",
|
|
210
|
+
"then",
|
|
211
|
+
"than",
|
|
212
|
+
"there",
|
|
213
|
+
"here",
|
|
214
|
+
"what",
|
|
215
|
+
"which",
|
|
216
|
+
"why",
|
|
217
|
+
"how",
|
|
218
|
+
"will",
|
|
219
|
+
"use",
|
|
220
|
+
"using",
|
|
221
|
+
"new",
|
|
222
|
+
"file",
|
|
223
|
+
"code",
|
|
224
|
+
"never",
|
|
225
|
+
"always",
|
|
226
|
+
"only",
|
|
227
|
+
"any",
|
|
228
|
+
"but",
|
|
229
|
+
"they",
|
|
230
|
+
"them",
|
|
231
|
+
"was",
|
|
232
|
+
"were",
|
|
233
|
+
"been",
|
|
234
|
+
"being",
|
|
235
|
+
"does",
|
|
236
|
+
"don",
|
|
237
|
+
"doesn",
|
|
238
|
+
"let",
|
|
239
|
+
"get",
|
|
240
|
+
"set",
|
|
241
|
+
"one",
|
|
242
|
+
"out",
|
|
243
|
+
"about",
|
|
244
|
+
"over",
|
|
245
|
+
"same",
|
|
246
|
+
"still",
|
|
247
|
+
"too",
|
|
248
|
+
"very",
|
|
249
|
+
]);
|
|
250
|
+
// implements REQ-kb-unowned-file-candidates, REQ-claude-request-topic-candidates
|
|
179
251
|
/**
|
|
180
252
|
* Current requirements ranked by how many of the path's topic words their
|
|
181
|
-
* title shares, then by how
|
|
182
|
-
*
|
|
253
|
+
* title shares, then by how many of the request's topic words it shares, then
|
|
254
|
+
* by how often the title uses them, then by id. A requirement sharing a path
|
|
255
|
+
* word is returned; one sharing only request words needs two of them, since a
|
|
256
|
+
* request names many words and one in common is chance.
|
|
183
257
|
*/
|
|
184
|
-
export function rankRequirementCandidates(relativePath, requirements, limit = MAX_REQUIREMENT_CANDIDATES) {
|
|
258
|
+
export function rankRequirementCandidates(relativePath, requirements, limit = MAX_REQUIREMENT_CANDIDATES, requestWords = []) {
|
|
185
259
|
const topic = new Set(pathTopicWords(relativePath));
|
|
186
|
-
|
|
260
|
+
const request = new Set(requestWords.filter((word) => !topic.has(word)));
|
|
261
|
+
if (topic.size === 0 && request.size === 0)
|
|
187
262
|
return [];
|
|
188
263
|
const scored = [];
|
|
189
264
|
for (const head of requirements) {
|
|
190
265
|
if (head.status !== undefined && RETIRED_STATUS.test(head.status))
|
|
191
266
|
continue;
|
|
192
|
-
const titleWords = words(head.title)
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
267
|
+
const titleWords = words(head.title);
|
|
268
|
+
const pathHits = titleWords.filter((word) => topic.has(word));
|
|
269
|
+
const requestHits = titleWords.filter((word) => request.has(word));
|
|
270
|
+
const shared = new Set(pathHits).size;
|
|
271
|
+
const asked = new Set(requestHits).size;
|
|
272
|
+
if (shared > 0 || asked >= 2) {
|
|
273
|
+
scored.push({
|
|
274
|
+
head,
|
|
275
|
+
shared,
|
|
276
|
+
asked,
|
|
277
|
+
uses: pathHits.length + requestHits.length,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
196
280
|
}
|
|
197
281
|
scored.sort((a, b) => b.shared - a.shared ||
|
|
282
|
+
b.asked - a.asked ||
|
|
198
283
|
b.uses - a.uses ||
|
|
199
284
|
a.head.id.localeCompare(b.head.id));
|
|
200
285
|
return scored.slice(0, limit).map((entry) => entry.head);
|
|
201
286
|
}
|
|
202
|
-
// implements REQ-kb-unowned-file-candidates
|
|
287
|
+
// implements REQ-kb-unowned-file-candidates, REQ-claude-request-topic-candidates
|
|
203
288
|
/**
|
|
204
289
|
* Candidate requirements for a file no symbol links, read from the canonical
|
|
205
|
-
* requirement documents (`.kb/requirements/*.md`)
|
|
206
|
-
*
|
|
290
|
+
* requirement documents (`.kb/requirements/*.md`), ranked by the path's words
|
|
291
|
+
* and, when given, the request's. Empty when the KB has none or is too large
|
|
292
|
+
* to scan from a hook.
|
|
207
293
|
*/
|
|
208
|
-
export function requirementCandidatesForPath(workspaceRoot, relativePath, limit = MAX_REQUIREMENT_CANDIDATES) {
|
|
209
|
-
if (pathTopicWords(relativePath).length === 0)
|
|
294
|
+
export function requirementCandidatesForPath(workspaceRoot, relativePath, limit = MAX_REQUIREMENT_CANDIDATES, requestWords = []) {
|
|
295
|
+
if (pathTopicWords(relativePath).length === 0 && requestWords.length < 2)
|
|
210
296
|
return [];
|
|
211
297
|
const directory = path.join(workspaceRoot, REQUIREMENTS_DIR);
|
|
212
298
|
let names;
|
|
@@ -221,5 +307,5 @@ export function requirementCandidatesForPath(workspaceRoot, relativePath, limit
|
|
|
221
307
|
const heads = names
|
|
222
308
|
.map((name) => readHead(path.join(directory, name)))
|
|
223
309
|
.filter((head) => head !== undefined);
|
|
224
|
-
return rankRequirementCandidates(relativePath, heads, limit);
|
|
310
|
+
return rankRequirementCandidates(relativePath, heads, limit, requestWords);
|
|
225
311
|
}
|
package/dist/snippets.js
CHANGED
|
@@ -125,7 +125,7 @@ export function describeEntity(summary) {
|
|
|
125
125
|
function jsonString(value) {
|
|
126
126
|
return JSON.stringify(value);
|
|
127
127
|
}
|
|
128
|
-
// implements REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1, REQ-kb-unowned-file-candidates
|
|
128
|
+
// implements REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1, REQ-kb-unowned-file-candidates, REQ-claude-request-topic-candidates
|
|
129
129
|
/**
|
|
130
130
|
* Discovery note for a source file none of whose symbols implement a
|
|
131
131
|
* requirement. No link is not no rule, so the note names the current
|
|
@@ -141,13 +141,22 @@ export function unownedSourceNote(relativePath, candidates = []) {
|
|
|
141
141
|
.map((candidate) => `${candidate.id}: ${truncate(candidate.title, MAX_TITLE)}`);
|
|
142
142
|
const note = [
|
|
143
143
|
head,
|
|
144
|
-
`Requirements whose titles share this path
|
|
144
|
+
`Requirements whose titles share words with this path or the request: ${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
145
|
search,
|
|
146
146
|
].join("\n");
|
|
147
147
|
if (note.length <= MAX_SNIPPET_CHARS)
|
|
148
148
|
return note;
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
if (candidates.length > 0)
|
|
151
|
+
return [head, search].join("\n");
|
|
152
|
+
// implements REQ-claude-request-topic-candidates
|
|
153
|
+
// Nothing found is not an all-clear: say that nothing recorded is known to
|
|
154
|
+
// cover the file, and that what this change decides for it is recorded now.
|
|
155
|
+
return [
|
|
156
|
+
head,
|
|
157
|
+
"No requirement title matches this file, so nothing recorded is known to cover it.",
|
|
158
|
+
`kb_search({query:"<behavior being changed>", sourceLocations:[{path:${jsonString(relativePath)}}]}) finds a rule recorded under other words. A decision this change makes for this code (a rule the person states, a limit, who may do what) is recorded with kb_upsert (requirement + scenario + symbol implements link) before the work is reported done.`,
|
|
159
|
+
].join("\n");
|
|
151
160
|
}
|
|
152
161
|
// implements REQ-kibi-hooks-flow-manifest, REQ-claude-code-kibi-plugin-v1, REQ-codex-kibi-plugin-v1
|
|
153
162
|
/** Shown instead of the discovery note when the manifest itself could not be read. */
|