kibi-opencode 0.14.0 → 0.15.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/README.md +18 -12
- package/dist/config.d.ts +0 -3
- package/dist/config.js +0 -9
- package/dist/e2e-coverage-signals.js +1 -3
- package/dist/enforcement-policy.d.ts +2 -0
- package/dist/enforcement-policy.js +18 -3
- package/dist/enforcement-scope.js +1 -3
- package/dist/file-entity-links.js +2 -6
- package/dist/file-operation-reminders.d.ts +4 -4
- package/dist/file-operation-state.js +4 -2
- package/dist/init-kibi-alias.js +0 -1
- package/dist/init-kibi-capability.js +1 -5
- package/dist/kb-freshness-state.d.ts +62 -0
- package/dist/kb-freshness-state.js +215 -0
- package/dist/kibi-checkpoint-runner.d.ts +1 -1
- package/dist/kibi-checkpoint-runner.js +6 -3
- package/dist/knowledge-classifier.js +2 -1
- package/dist/meaningful-change-classifier.d.ts +30 -0
- package/dist/meaningful-change-classifier.js +103 -0
- package/dist/plugin-startup.js +0 -1
- package/dist/plugin.js +101 -391
- package/dist/prompt.d.ts +6 -5
- package/dist/prompt.js +48 -84
- package/dist/reconcile-engine.d.ts +15 -1
- package/dist/reconcile-engine.js +1 -3
- package/dist/scheduler.js +16 -4
- package/dist/session-edit-state.js +2 -1
- package/dist/source-linked-guidance.js +3 -1
- package/dist/startup-notifier.d.ts +7 -1
- package/dist/startup-notifier.js +51 -17
- package/dist/tui.js +1 -403
- package/dist/tui.jsx +1 -113
- package/dist/version-metadata.d.ts +13 -0
- package/dist/version-metadata.js +78 -0
- package/dist/version-metadata.json +1 -0
- package/package.json +5 -7
- package/dist/brief-delivery-reasons.d.ts +0 -12
- package/dist/brief-delivery-reasons.js +0 -132
- package/dist/brief-intent.d.ts +0 -41
- package/dist/brief-intent.js +0 -142
- package/dist/briefing-runtime.d.ts +0 -24
- package/dist/briefing-runtime.js +0 -277
- package/dist/graph-narrator.d.ts +0 -25
- package/dist/graph-narrator.js +0 -408
- package/dist/idle-brief-audit.d.ts +0 -36
- package/dist/idle-brief-audit.js +0 -186
- package/dist/idle-brief-paths.d.ts +0 -6
- package/dist/idle-brief-paths.js +0 -120
- package/dist/idle-brief-reader.d.ts +0 -37
- package/dist/idle-brief-reader.js +0 -163
- package/dist/idle-brief-runtime.d.ts +0 -55
- package/dist/idle-brief-runtime.js +0 -496
- package/dist/idle-brief-store.d.ts +0 -113
- package/dist/idle-brief-store.js +0 -262
- package/dist/session-fingerprint.d.ts +0 -11
- package/dist/session-fingerprint.js +0 -21
- package/dist/tui-brief-delivery.d.ts +0 -67
- package/dist/tui-brief-delivery.js +0 -279
- package/dist/tui-brief-view-model.d.ts +0 -63
- package/dist/tui-brief-view-model.js +0 -209
- package/dist/utils/brief-marker.d.ts +0 -19
- package/dist/utils/brief-marker.js +0 -101
package/README.md
CHANGED
|
@@ -24,8 +24,8 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
|
|
|
24
24
|
|
|
25
25
|
- **Repo posture detection**: distinguishes `root_active`, `root_partial`, `root_uninitialized`, `vendored_only`, and `hybrid_root_plus_vendored`
|
|
26
26
|
- **Risk classification**: separates `safe_docs_only`, `safe_test_only`, `kb_doc_structural`, `req_policy_candidate`, `behavior_candidate`, `traceability_candidate`, and `manual_kb_edit`
|
|
27
|
-
- **Source-linked
|
|
28
|
-
- **Start-task risky cue**: authoritative risky edits also add a compact
|
|
27
|
+
- **Source-linked Kibi links**: risky code edits (`behavior_candidate`, `traceability_candidate`) prepend a concise list of existing Kibi links (e.g., `- Existing Kibi links: REQ-001, REQ-002`) when 1-3 concrete source-linked KB hits are found in `documentation/symbols.yaml`. Skip on cache hit.
|
|
28
|
+
- **Start-task risky cue**: authoritative risky edits also add a compact Kibi context cue so agents can start with explicit source-linked guidance before acting, while staying inside the same single prompt block and token budget.
|
|
29
29
|
- **Effective mode gating**: `advisory` (default) never blocks; `strict` escalates checks and reminders for `root_active` and `hybrid_root_plus_vendored` postures when `requireRootKbForStrict` is enabled; `hard` fails closed for authoritative roots and linked git worktrees, injecting a stop-state with MCP recovery steps until the Kibi checkpoint passes. `maintenanceDegraded` overrides everything back to `advisory`.
|
|
30
30
|
- **Low-token prompt policy**: docs-only and test-only edits avoid unnecessary discovery prompts; vendored-only repos suppress operational bootstrap nudges; at most one contextual block is injected per prompt (≤120 words, ≤5 bullets)
|
|
31
31
|
- **Completion reminder**: when `completionReminder` is enabled, risky code edits append a single prompt-visible `kb_check` reminder exactly once per cached context
|
|
@@ -33,6 +33,20 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
|
|
|
33
33
|
- **Three-mode enforcement**: `advisory` by default keeps guidance non-blocking; opt-in `strict` adds elevated validation cues; `hard` blocks the prompt for authoritative workspaces until the checkpoint cycle succeeds. Git hooks and KB validation checks remain the durable enforcement boundary for all modes.
|
|
34
34
|
- **Structured observability**: posture, risk, cache, degraded-mode, targeted-check, and guidance events flow through structured plugin logs
|
|
35
35
|
|
|
36
|
+
### KB Freshness Contract
|
|
37
|
+
|
|
38
|
+
Meaningful code, test, requirement, scenario, and symbol changes now require explicit
|
|
39
|
+
KB impact resolution before task completion. When source or documentation files are
|
|
40
|
+
modified, the plugin evaluates KB freshness evidence and surfaces a visible
|
|
41
|
+
`🧠 **Kibi freshness required**` block when KB impact is unresolved.
|
|
42
|
+
|
|
43
|
+
**Allowed resolutions:**
|
|
44
|
+
- **KB updated**: Run source-linked discovery via `kb_search`, create or update entities
|
|
45
|
+
with `kb_upsert` (or remove with `kb_delete`), then run `kb_check`.
|
|
46
|
+
- **No KB impact**: Include a no-impact rationale in the final report after source-linked
|
|
47
|
+
discovery via `kb_search` or `kb_query(sourceFile=...)` and `kb_check`.
|
|
48
|
+
- **Deferred/Failed**: Do not claim task completion.
|
|
49
|
+
|
|
36
50
|
### Dynamic Contextual Guidance
|
|
37
51
|
|
|
38
52
|
The plugin provides context-aware prompt guidance based on recent edits and workspace state:
|
|
@@ -113,13 +127,6 @@ The plugin injects guidance into OpenCode sessions to improve agent grounding. U
|
|
|
113
127
|
|
|
114
128
|
OpenCode exposes Kibi MCP prompts as slash commands. The \`/init-kibi\` command triggers the \`kb_autopilot_generate\` workflow to assist in retroactive bootstrap using only public MCP tools.
|
|
115
129
|
|
|
116
|
-
When the plugin detects an authoritative risky edit (`behavior_candidate` or `traceability_candidate` risk class), it automatically renders a Kibi briefing before the prompt. The plugin uses two complementary paths: the `file.edited` event hook as a fast-path hint, and prompt-cycle reconciliation as an authoritative fallback for programmatic edits that bypass the event bus. Briefings are rendered directly into the prompt to ensure immediate visibility.
|
|
117
|
-
|
|
118
|
-
- **Immediate delivery**: Briefings are rendered-first into the prompt guidance block headed `🧠 **Kibi briefing available**` and TUI toasts titled `Kibi Knowledge Update`. Unread briefings automatically open the interactive TUI via the `kibi.brief` route.
|
|
119
|
-
- **Narrative structure**: Delivery favors user-facing prose with `What changed` and `Why it matters`, plus conditional `Project knowledge impact` / `Interpretation note` sections when evidence or caveats exist.
|
|
120
|
-
- **TL;DR fallback**: If a full briefing is unavailable, fallback output still preserves `What changed` / `Why it matters` framing while keeping the manual command cue available.
|
|
121
|
-
- **Manual command**: Use `/brief-kibi` or the `kibi.open_latest_brief` command at any time to trigger an on-demand briefing if auto-delivery is skipped or fails.
|
|
122
|
-
|
|
123
130
|
### Discovery-first MCP guidance
|
|
124
131
|
|
|
125
132
|
Agent-visible guidance is intentionally limited to the curated public MCP surface:
|
|
@@ -165,7 +172,6 @@ Config files (project overrides global):
|
|
|
165
172
|
| `ux.toastFailures` | boolean | `true` | Show failure toasts for sync/check issues |
|
|
166
173
|
| `ux.toastSuccesses` | boolean | `false` | Show success toasts for sync/check completion |
|
|
167
174
|
| `ux.toastCooldownMs` | number | `10000` | Cooldown between repeated UX toasts |
|
|
168
|
-
| `ux.briefs.autoSubmit` | boolean | `true` | **Deprecated/No-op**: Auto-submission is no longer needed with render-first briefing |
|
|
169
175
|
PP|| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
170
176
|
| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
171
177
|
| `guidance.warnOnKbEdits` | boolean | `true` | Enable loud warnings for .kb/** edits |
|
|
@@ -220,7 +226,7 @@ The plugin uses the official OpenCode toast APIs with automatic capability detec
|
|
|
220
226
|
1. **Legacy transport**: `client.tui.toast(payload)` — used when available in plugin context
|
|
221
227
|
2. **SDK transport**: `client.tui.showToast({ body: payload })` — used as fallback
|
|
222
228
|
3. **No capability**: Returns `{ status: "unavailable", reason: "missing-capability" }`
|
|
223
|
-
The `sendToast` helper returns a discriminated `SendToastResult` union and never throws. There is no raw HTTP fallback.
|
|
229
|
+
The `sendToast` helper returns a discriminated `SendToastResult` union and never throws. There is no raw HTTP fallback.
|
|
224
230
|
The `experimental.chat.system.transform` hook handles prompt injection (see [Hook Policy](#hook-policy)). The `chat.params` hook is compatibility-only and never carries prompt text.
|
|
225
231
|
|
|
226
232
|
### Hook Modes
|
|
@@ -301,7 +307,7 @@ The OpenCode plugin respects the repository ignore policy used by Kibi's discove
|
|
|
301
307
|
- The plugin's background sync and file-event handling skip paths matched by repository `.gitignore` files, nested `.gitignore` files, and `.git/info/exclude`.
|
|
302
308
|
- The plugin also treats a curated set of tool/runtime directories as never-relevant for KB sync (for example: `.sisyphus`, `.opencode`, `.kb`, `.git`, `node_modules`, `vendor`, `third_party`).
|
|
303
309
|
|
|
304
|
-
When a file event occurs for an ignored path, the plugin skips processing and will not surface candidate guidance for that file. This avoids noisy
|
|
310
|
+
When a file event occurs for an ignored path, the plugin skips processing and will not surface candidate guidance for that file. This avoids noisy sync triggers and prevents build/editor artifacts from triggering KB sync work.
|
|
305
311
|
|
|
306
312
|
- **E2e reminder evidence**: File-operation reminders use exact Kibi graph evidence first (`covered_by` links to `[e2e]`-tagged entities or `/e2e/`-sourced entities) and narrow path heuristics second. Package-level e2e tests do not trigger "authoritative evidence" flags at the file level.
|
|
307
313
|
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -11,9 +11,6 @@ const DEFAULTS = {
|
|
|
11
11
|
toastFailures: true,
|
|
12
12
|
toastSuccesses: false,
|
|
13
13
|
toastCooldownMs: 10000,
|
|
14
|
-
briefs: {
|
|
15
|
-
autoSubmit: true,
|
|
16
|
-
},
|
|
17
14
|
},
|
|
18
15
|
guidance: {
|
|
19
16
|
dynamic: true,
|
|
@@ -103,12 +100,6 @@ export function validateAndMerge(obj) {
|
|
|
103
100
|
out.ux.toastSuccesses = u.toastSuccesses;
|
|
104
101
|
if (typeof u.toastCooldownMs === "number")
|
|
105
102
|
out.ux.toastCooldownMs = u.toastCooldownMs;
|
|
106
|
-
if (u.briefs && typeof u.briefs === "object") {
|
|
107
|
-
const b = u.briefs;
|
|
108
|
-
out.ux.briefs = { autoSubmit: true };
|
|
109
|
-
if (typeof b.autoSubmit === "boolean")
|
|
110
|
-
out.ux.briefs.autoSubmit = b.autoSubmit;
|
|
111
|
-
}
|
|
112
103
|
}
|
|
113
104
|
if (typeof src.logLevel === "string")
|
|
114
105
|
out.logLevel = src.logLevel;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { E2eCoverageSignal } from "./e2e-coverage-signals.js";
|
|
2
2
|
import type { FileLifecycle, ReminderKind } from "./file-operation-state.js";
|
|
3
|
+
import type { KbFreshnessEvidence } from "./kb-freshness-state.js";
|
|
3
4
|
import type { PathKind } from "./path-kind.js";
|
|
4
5
|
import type { RepoPosture } from "./repo-posture.js";
|
|
5
6
|
import type { RiskClass } from "./risk-classifier.js";
|
|
@@ -20,6 +21,7 @@ export type CheckpointEvidence = boolean | {
|
|
|
20
21
|
kbStatus?: boolean;
|
|
21
22
|
kbCheck?: boolean;
|
|
22
23
|
kbUpsert?: boolean;
|
|
24
|
+
freshness?: KbFreshnessEvidence;
|
|
23
25
|
};
|
|
24
26
|
export interface EnforcementPolicyInput {
|
|
25
27
|
/** Resolved work context for the current file/prompt cycle. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evaluateKbFreshness } from "./kb-freshness-state.js";
|
|
1
2
|
const NO_E2E_SIGNAL = {
|
|
2
3
|
level: "none",
|
|
3
4
|
evidence: [],
|
|
@@ -31,6 +32,11 @@ function normalizeCheckpointEvidence(evidence) {
|
|
|
31
32
|
if (!evidence) {
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
35
|
+
// Freshness evidence is more authoritative than legacy booleans
|
|
36
|
+
if (evidence.freshness) {
|
|
37
|
+
return evaluateKbFreshness(evidence.freshness).allowsCompletion;
|
|
38
|
+
}
|
|
39
|
+
// Legacy boolean check
|
|
34
40
|
return (evidence.hasCheckpoint === true ||
|
|
35
41
|
evidence.kbSearch === true ||
|
|
36
42
|
evidence.sourceFileQuery === true ||
|
|
@@ -49,7 +55,10 @@ function normalizeEvents(input) {
|
|
|
49
55
|
return input.lifecycleEvents.map((event, index) => {
|
|
50
56
|
const linkedEntityResult = input.linkedEntityResults?.[index] ??
|
|
51
57
|
(input.linkedEntityIds?.[index]
|
|
52
|
-
? {
|
|
58
|
+
? {
|
|
59
|
+
ids: input.linkedEntityIds[index] ?? [],
|
|
60
|
+
source: "symbols",
|
|
61
|
+
}
|
|
53
62
|
: DEFAULT_LINKED_ENTITY_RESULT);
|
|
54
63
|
return {
|
|
55
64
|
...event,
|
|
@@ -91,7 +100,8 @@ function uniqueReminderKinds(events) {
|
|
|
91
100
|
else {
|
|
92
101
|
add("kibi_write");
|
|
93
102
|
}
|
|
94
|
-
if (event.e2eSignal.level !== "none" &&
|
|
103
|
+
if (event.e2eSignal.level !== "none" &&
|
|
104
|
+
event.e2eSignal.reminderText !== null) {
|
|
95
105
|
add(event.lifecycle === "deleted" ? "e2e_delete" : "e2e_write");
|
|
96
106
|
}
|
|
97
107
|
}
|
|
@@ -162,7 +172,8 @@ function hardBlockText(events) {
|
|
|
162
172
|
if (remainingCount > 0) {
|
|
163
173
|
pathLines.push(`- +${remainingCount} more dirty files`);
|
|
164
174
|
}
|
|
165
|
-
const deletedWithoutLinks = events.some((event) => event.lifecycle === "deleted" &&
|
|
175
|
+
const deletedWithoutLinks = events.some((event) => event.lifecycle === "deleted" &&
|
|
176
|
+
event.linkedEntityResult.ids.length === 0);
|
|
166
177
|
const representativePath = events[0]?.normalizedPath ?? "<changed-file>";
|
|
167
178
|
const evidenceNotes = [linkedIdsText(events), e2eEvidenceText(events)].filter((note) => note !== null);
|
|
168
179
|
const deletionCleanup = deletedWithoutLinks
|
|
@@ -180,6 +191,10 @@ function hardBlockText(events) {
|
|
|
180
191
|
"- Run `kb_status` if branch or snapshot freshness matters.",
|
|
181
192
|
`- ${deletionCleanup}`,
|
|
182
193
|
"- Run `kb_check` before completing the task.",
|
|
194
|
+
"KB freshness resolution:",
|
|
195
|
+
"- **KB updated**: run `kb_search` for discovery, then `kb_upsert`/`kb_delete` for mutations, then `kb_check`.",
|
|
196
|
+
"- **No KB impact**: provide a no-impact rationale in your final report after source-linked discovery via `kb_search` or `kb_query(sourceFile=...)` and `kb_check`.",
|
|
197
|
+
"- **Deferred/failed**: do not claim task completion.",
|
|
183
198
|
].join("\n"),
|
|
184
199
|
shownPaths,
|
|
185
200
|
remainingCount,
|
|
@@ -30,7 +30,5 @@ export function buildDirtyRelevantFingerprint(values) {
|
|
|
30
30
|
if (normalized.length === 0) {
|
|
31
31
|
return "clean";
|
|
32
32
|
}
|
|
33
|
-
return createHash("sha256")
|
|
34
|
-
.update(JSON.stringify(normalized))
|
|
35
|
-
.digest("hex");
|
|
33
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
36
34
|
}
|
|
@@ -108,7 +108,6 @@ export function parseSymbolsYaml(content) {
|
|
|
108
108
|
if (target === undefined)
|
|
109
109
|
continue;
|
|
110
110
|
pendingRel.target = target.trim();
|
|
111
|
-
continue;
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
}
|
|
@@ -142,7 +141,7 @@ function resolveDocPathIdentity(relPath, syncPaths) {
|
|
|
142
141
|
// Strip glob from pattern to get the root dir prefix
|
|
143
142
|
// e.g. "documentation/requirements/**/*.md" → "documentation/requirements"
|
|
144
143
|
const rootDir = pattern.replace(/\/\*\*\/.*$/, "").replace(/\/+$/, "");
|
|
145
|
-
if (normalizedRel.startsWith(rootDir
|
|
144
|
+
if (normalizedRel.startsWith(`${rootDir}/`)) {
|
|
146
145
|
return entityId;
|
|
147
146
|
}
|
|
148
147
|
}
|
|
@@ -168,10 +167,7 @@ function normalizeFilePath(worktree, filePath) {
|
|
|
168
167
|
const absPath = path.isAbsolute(filePath)
|
|
169
168
|
? filePath
|
|
170
169
|
: path.resolve(worktree, filePath);
|
|
171
|
-
return path
|
|
172
|
-
.relative(worktree, absPath)
|
|
173
|
-
.split(path.sep)
|
|
174
|
-
.join("/");
|
|
170
|
+
return path.relative(worktree, absPath).split(path.sep).join("/");
|
|
175
171
|
}
|
|
176
172
|
// ── Public API ──────────────────────────────────────────────────────
|
|
177
173
|
// implements REQ-opencode-file-context-guidance-v1
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { RepoPosture } from "./repo-posture.js";
|
|
2
|
-
import type { PathKind } from "./path-kind.js";
|
|
3
|
-
import type { RiskClass } from "./risk-classifier.js";
|
|
4
|
-
import type { ReminderKind } from "./file-operation-state.js";
|
|
5
1
|
import type { E2eCoverageSignal } from "./e2e-coverage-signals.js";
|
|
6
2
|
import { type CheckpointEvidence, type EnforcementLifecycleEvent, type EnforcementPolicyResult } from "./enforcement-policy.js";
|
|
3
|
+
import type { ReminderKind } from "./file-operation-state.js";
|
|
4
|
+
import type { PathKind } from "./path-kind.js";
|
|
5
|
+
import type { RepoPosture } from "./repo-posture.js";
|
|
6
|
+
import type { RiskClass } from "./risk-classifier.js";
|
|
7
7
|
import type { EffectiveMode } from "./smart-enforcement.js";
|
|
8
8
|
import type { WorkContext } from "./work-context-resolver.js";
|
|
9
9
|
export interface LinkedEntityResult {
|
|
@@ -37,11 +37,13 @@ export function createFileOperationState(opts) {
|
|
|
37
37
|
return "edited";
|
|
38
38
|
}
|
|
39
39
|
// created|edited + deleted -> deleted
|
|
40
|
-
if ((existing === "created" || existing === "edited") &&
|
|
40
|
+
if ((existing === "created" || existing === "edited") &&
|
|
41
|
+
incoming === "deleted") {
|
|
41
42
|
return "deleted";
|
|
42
43
|
}
|
|
43
44
|
// deleted + created|edited -> deleted
|
|
44
|
-
if (existing === "deleted" &&
|
|
45
|
+
if (existing === "deleted" &&
|
|
46
|
+
(incoming === "created" || incoming === "edited")) {
|
|
45
47
|
return "deleted";
|
|
46
48
|
}
|
|
47
49
|
// Fallback: use incoming
|
package/dist/init-kibi-alias.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { buildInitKibiAlias } from "./init-kibi-alias.js";
|
|
6
6
|
export const INIT_KIBI_COMMAND_NAME = "init-kibi";
|
|
@@ -97,7 +97,6 @@ function readPackageVersion(filePath) {
|
|
|
97
97
|
return undefined;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
// implements REQ-opencode-kibi-briefing-v2
|
|
101
100
|
export function findSdkPackageJsonForPluginRoot(pluginRoot) {
|
|
102
101
|
const scopeRoot = path.dirname(pluginRoot);
|
|
103
102
|
const candidates = [
|
|
@@ -151,7 +150,6 @@ function isRecord(value) {
|
|
|
151
150
|
function isCommandMap(value) {
|
|
152
151
|
return isRecord(value);
|
|
153
152
|
}
|
|
154
|
-
// implements REQ-opencode-kibi-briefing-v2
|
|
155
153
|
export function detectInitKibiCommandCapability(input) {
|
|
156
154
|
const { pluginVersion, pluginHooksDts, sdkTypesDts } = input;
|
|
157
155
|
if (typeof pluginHooksDts !== "string" || pluginHooksDts.length === 0) {
|
|
@@ -171,7 +169,6 @@ export function detectInitKibiCommandCapability(input) {
|
|
|
171
169
|
pluginVersion: pluginVersion ?? "unknown",
|
|
172
170
|
};
|
|
173
171
|
}
|
|
174
|
-
// implements REQ-opencode-kibi-briefing-v2
|
|
175
172
|
export function getInitKibiCommandCapability() {
|
|
176
173
|
if (cachedCapability?.supported) {
|
|
177
174
|
return cachedCapability;
|
|
@@ -179,7 +176,6 @@ export function getInitKibiCommandCapability() {
|
|
|
179
176
|
cachedCapability = detectInitKibiCommandCapability(resolveHostCapabilityInputs());
|
|
180
177
|
return cachedCapability;
|
|
181
178
|
}
|
|
182
|
-
// implements REQ-opencode-kibi-briefing-v2
|
|
183
179
|
export function registerInitKibiCommand(configInput, capability = getInitKibiCommandCapability()) {
|
|
184
180
|
if (!capability.supported) {
|
|
185
181
|
return capability;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB freshness state machine, evidence model, and in-memory evidence store.
|
|
3
|
+
*
|
|
4
|
+
* This is Wave 1 — the foundation module. No MCP tools, no filesystem or
|
|
5
|
+
* `.kb/` persistence, no entity mutation. Pure in-memory state.
|
|
6
|
+
*
|
|
7
|
+
* @module kb-freshness-state
|
|
8
|
+
*/
|
|
9
|
+
export type KbFreshnessDecision = "updated" | "no-impact" | "deferred" | "failed";
|
|
10
|
+
export type KbFreshnessStateKind = "clean" | "unknown-impact" | "evidence-required" | "updated" | "no-impact-accepted" | "deferred" | "check-failed";
|
|
11
|
+
export interface KbFreshnessEvidence {
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
agentIdentity: string;
|
|
14
|
+
worktree: string;
|
|
15
|
+
branch: string;
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
changedFiles: string[];
|
|
18
|
+
kbStatus: boolean;
|
|
19
|
+
sourceLinkedDiscovery: boolean;
|
|
20
|
+
kbMutation: boolean;
|
|
21
|
+
kbCheck: boolean;
|
|
22
|
+
decision?: KbFreshnessDecision;
|
|
23
|
+
rationale?: string;
|
|
24
|
+
checkRules?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface KbFreshnessEvaluation {
|
|
27
|
+
state: KbFreshnessStateKind;
|
|
28
|
+
requiresEvidence: boolean;
|
|
29
|
+
allowsCompletion: boolean;
|
|
30
|
+
reason: string;
|
|
31
|
+
missingEvidence: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate KB freshness from collected evidence.
|
|
35
|
+
*
|
|
36
|
+
* Rules (in priority order):
|
|
37
|
+
* 1. No changed files → `clean`, allows completion.
|
|
38
|
+
* 2. Changed files but no decision → `evidence-required`, blocks completion.
|
|
39
|
+
* 3. `updated` → requires `kbMutation` + `kbCheck`.
|
|
40
|
+
* 4. `no-impact` → requires `rationale` + `sourceLinkedDiscovery` + `kbCheck`.
|
|
41
|
+
* 5. `deferred` → blocks completion.
|
|
42
|
+
* 6. `failed` → `check-failed`, blocks completion.
|
|
43
|
+
*/
|
|
44
|
+
export declare function evaluateKbFreshness(evidence: KbFreshnessEvidence): KbFreshnessEvaluation;
|
|
45
|
+
export interface KbFreshnessScope {
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
agentIdentity: string;
|
|
48
|
+
worktree: string;
|
|
49
|
+
branch: string;
|
|
50
|
+
fingerprint: string;
|
|
51
|
+
}
|
|
52
|
+
export interface KbFreshnessEvidenceStore {
|
|
53
|
+
recordToolEvidence(scope: KbFreshnessScope, toolName: string): void;
|
|
54
|
+
recordNoImpact(scope: KbFreshnessScope, rationale: string): void;
|
|
55
|
+
getEvidence(scope: KbFreshnessScope, changedFiles: string[]): KbFreshnessEvidence;
|
|
56
|
+
resetScope(scope: KbFreshnessScope): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an in-memory evidence store scoped by
|
|
60
|
+
* (sessionId | agentIdentity | worktree | branch | fingerprint).
|
|
61
|
+
*/
|
|
62
|
+
export declare function createKbFreshnessEvidenceStore(): KbFreshnessEvidenceStore;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB freshness state machine, evidence model, and in-memory evidence store.
|
|
3
|
+
*
|
|
4
|
+
* This is Wave 1 — the foundation module. No MCP tools, no filesystem or
|
|
5
|
+
* `.kb/` persistence, no entity mutation. Pure in-memory state.
|
|
6
|
+
*
|
|
7
|
+
* @module kb-freshness-state
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Evaluation
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate KB freshness from collected evidence.
|
|
14
|
+
*
|
|
15
|
+
* Rules (in priority order):
|
|
16
|
+
* 1. No changed files → `clean`, allows completion.
|
|
17
|
+
* 2. Changed files but no decision → `evidence-required`, blocks completion.
|
|
18
|
+
* 3. `updated` → requires `kbMutation` + `kbCheck`.
|
|
19
|
+
* 4. `no-impact` → requires `rationale` + `sourceLinkedDiscovery` + `kbCheck`.
|
|
20
|
+
* 5. `deferred` → blocks completion.
|
|
21
|
+
* 6. `failed` → `check-failed`, blocks completion.
|
|
22
|
+
*/
|
|
23
|
+
export function evaluateKbFreshness(evidence) {
|
|
24
|
+
// Rule 1: No changed files
|
|
25
|
+
if (evidence.changedFiles.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
state: "clean",
|
|
28
|
+
requiresEvidence: false,
|
|
29
|
+
allowsCompletion: true,
|
|
30
|
+
reason: "No files changed",
|
|
31
|
+
missingEvidence: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Rule 2: No decision recorded yet
|
|
35
|
+
if (evidence.decision === undefined) {
|
|
36
|
+
return {
|
|
37
|
+
state: "evidence-required",
|
|
38
|
+
requiresEvidence: true,
|
|
39
|
+
allowsCompletion: false,
|
|
40
|
+
reason: "Session modified files without KB tool evidence",
|
|
41
|
+
missingEvidence: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Rule 5: Deferred
|
|
45
|
+
if (evidence.decision === "deferred") {
|
|
46
|
+
return {
|
|
47
|
+
state: "deferred",
|
|
48
|
+
requiresEvidence: false,
|
|
49
|
+
allowsCompletion: false,
|
|
50
|
+
reason: "KB freshness check was explicitly deferred",
|
|
51
|
+
missingEvidence: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Rule 6: Failed
|
|
55
|
+
if (evidence.decision === "failed") {
|
|
56
|
+
return {
|
|
57
|
+
state: "check-failed",
|
|
58
|
+
requiresEvidence: false,
|
|
59
|
+
allowsCompletion: false,
|
|
60
|
+
reason: "KB freshness check failed",
|
|
61
|
+
missingEvidence: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Rule 3: Updated
|
|
65
|
+
if (evidence.decision === "updated") {
|
|
66
|
+
const missingEvidence = [];
|
|
67
|
+
if (!evidence.kbMutation) {
|
|
68
|
+
missingEvidence.push("kbMutation");
|
|
69
|
+
}
|
|
70
|
+
if (!evidence.kbCheck) {
|
|
71
|
+
missingEvidence.push("kbCheck");
|
|
72
|
+
}
|
|
73
|
+
if (missingEvidence.length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
state: "evidence-required",
|
|
76
|
+
requiresEvidence: true,
|
|
77
|
+
allowsCompletion: false,
|
|
78
|
+
reason: "KB was updated but required evidence (kbMutation, kbCheck) is missing",
|
|
79
|
+
missingEvidence,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
state: "updated",
|
|
84
|
+
requiresEvidence: false,
|
|
85
|
+
allowsCompletion: true,
|
|
86
|
+
reason: "KB updated and verified via kb_check",
|
|
87
|
+
missingEvidence: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Rule 4: No-impact
|
|
91
|
+
if (evidence.decision === "no-impact") {
|
|
92
|
+
const missingEvidence = [];
|
|
93
|
+
if (!evidence.rationale || evidence.rationale === "") {
|
|
94
|
+
missingEvidence.push("rationale");
|
|
95
|
+
}
|
|
96
|
+
if (!evidence.sourceLinkedDiscovery) {
|
|
97
|
+
missingEvidence.push("sourceLinkedDiscovery");
|
|
98
|
+
}
|
|
99
|
+
if (!evidence.kbCheck) {
|
|
100
|
+
missingEvidence.push("kbCheck");
|
|
101
|
+
}
|
|
102
|
+
if (missingEvidence.length > 0) {
|
|
103
|
+
return {
|
|
104
|
+
state: "evidence-required",
|
|
105
|
+
requiresEvidence: true,
|
|
106
|
+
allowsCompletion: false,
|
|
107
|
+
reason: "No-impact claim needs rationale, sourceLinkedDiscovery, and kbCheck",
|
|
108
|
+
missingEvidence,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
state: "no-impact-accepted",
|
|
113
|
+
requiresEvidence: false,
|
|
114
|
+
allowsCompletion: true,
|
|
115
|
+
reason: "No-impact accepted with rationale and KB check passed",
|
|
116
|
+
missingEvidence: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Exhaustive guard — all decision branches handled above
|
|
120
|
+
const _exhaustive = evidence.decision;
|
|
121
|
+
throw new Error(`Unexpected decision: ${_exhaustive}`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Map of tool names → evidence side effects.
|
|
125
|
+
* Each entry lists the evidence fields to set when that tool is used.
|
|
126
|
+
*/
|
|
127
|
+
const TOOL_EFFECTS = {
|
|
128
|
+
kb_status: ["kbStatus"],
|
|
129
|
+
kb_search: ["sourceLinkedDiscovery"],
|
|
130
|
+
kb_query: ["sourceLinkedDiscovery"],
|
|
131
|
+
kb_graph: ["sourceLinkedDiscovery"],
|
|
132
|
+
kb_find_gaps: ["sourceLinkedDiscovery"],
|
|
133
|
+
kb_coverage: ["sourceLinkedDiscovery"],
|
|
134
|
+
kb_upsert: ["kbMutation"],
|
|
135
|
+
kb_delete: ["kbMutation"],
|
|
136
|
+
kb_check: ["kbCheck"],
|
|
137
|
+
};
|
|
138
|
+
function buildScopeKey(scope) {
|
|
139
|
+
const parts = [
|
|
140
|
+
scope.sessionId ?? "",
|
|
141
|
+
scope.agentIdentity,
|
|
142
|
+
scope.worktree,
|
|
143
|
+
scope.branch,
|
|
144
|
+
scope.fingerprint,
|
|
145
|
+
];
|
|
146
|
+
return parts.join("|");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create an in-memory evidence store scoped by
|
|
150
|
+
* (sessionId | agentIdentity | worktree | branch | fingerprint).
|
|
151
|
+
*/
|
|
152
|
+
export function createKbFreshnessEvidenceStore() {
|
|
153
|
+
const store = new Map();
|
|
154
|
+
function getOrCreate(scope) {
|
|
155
|
+
const key = buildScopeKey(scope);
|
|
156
|
+
if (!store.has(key)) {
|
|
157
|
+
store.set(key, {
|
|
158
|
+
kbStatus: false,
|
|
159
|
+
sourceLinkedDiscovery: false,
|
|
160
|
+
kbMutation: false,
|
|
161
|
+
kbCheck: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return store.get(key);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
recordToolEvidence(scope, toolName) {
|
|
168
|
+
const record = getOrCreate(scope);
|
|
169
|
+
const effects = TOOL_EFFECTS[toolName];
|
|
170
|
+
if (!effects) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const field of effects) {
|
|
174
|
+
if (field === "kbMutation") {
|
|
175
|
+
record.kbMutation = true;
|
|
176
|
+
record.decision = "updated";
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
record[field] = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
recordNoImpact(scope, rationale) {
|
|
184
|
+
const record = getOrCreate(scope);
|
|
185
|
+
record.decision = "no-impact";
|
|
186
|
+
record.rationale = rationale;
|
|
187
|
+
},
|
|
188
|
+
getEvidence(scope, changedFiles) {
|
|
189
|
+
const record = getOrCreate(scope);
|
|
190
|
+
return {
|
|
191
|
+
...(scope.sessionId ? { sessionId: scope.sessionId } : {}),
|
|
192
|
+
agentIdentity: scope.agentIdentity,
|
|
193
|
+
worktree: scope.worktree,
|
|
194
|
+
branch: scope.branch,
|
|
195
|
+
fingerprint: scope.fingerprint,
|
|
196
|
+
changedFiles,
|
|
197
|
+
kbStatus: record.kbStatus,
|
|
198
|
+
sourceLinkedDiscovery: record.sourceLinkedDiscovery,
|
|
199
|
+
kbMutation: record.kbMutation,
|
|
200
|
+
kbCheck: record.kbCheck,
|
|
201
|
+
...(record.decision !== undefined ? { decision: record.decision } : {}),
|
|
202
|
+
...(record.rationale !== undefined
|
|
203
|
+
? { rationale: record.rationale }
|
|
204
|
+
: {}),
|
|
205
|
+
...(record.checkRules !== undefined
|
|
206
|
+
? { checkRules: record.checkRules }
|
|
207
|
+
: {}),
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
resetScope(scope) {
|
|
211
|
+
const key = buildScopeKey(scope);
|
|
212
|
+
store.delete(key);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type KibiConfig } from "./config.js";
|
|
2
|
-
import { type EnforcementLifecycleEvent, type PolicyLinkedEntityResult } from "./enforcement-policy.js";
|
|
3
2
|
import type { E2eCoverageSignal } from "./e2e-coverage-signals.js";
|
|
3
|
+
import { type EnforcementLifecycleEvent, type PolicyLinkedEntityResult } from "./enforcement-policy.js";
|
|
4
4
|
import type { PathKind } from "./path-kind.js";
|
|
5
5
|
import { type CheckRunner, type SyncRunMetadata, type SyncRunner, type TimeoutHandle } from "./scheduler.js";
|
|
6
6
|
import type { WorkContext } from "./work-context-resolver.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
2
2
|
import { DEFAULTS } from "./config.js";
|
|
3
|
-
import { buildEnforcementScopeKey } from "./enforcement-scope.js";
|
|
4
3
|
import { computeEnforcementPolicy, } from "./enforcement-policy.js";
|
|
4
|
+
import { buildEnforcementScopeKey } from "./enforcement-scope.js";
|
|
5
5
|
import { createSyncScheduler, } from "./scheduler.js";
|
|
6
6
|
const HARD_CHECKPOINT_TIMEOUT_MS = 30_000;
|
|
7
7
|
const CHECKPOINT_REASON = "smart-enforcement.checkpoint";
|
|
@@ -69,7 +69,8 @@ export class KibiCheckpointRunner {
|
|
|
69
69
|
if (context) {
|
|
70
70
|
return this.passed.has(this.scopeKey(context, normalizedFingerprint));
|
|
71
71
|
}
|
|
72
|
-
return (this.passedScopeKeysByFingerprint.get(normalizedFingerprint)?.size ??
|
|
72
|
+
return ((this.passedScopeKeysByFingerprint.get(normalizedFingerprint)?.size ??
|
|
73
|
+
0) > 0);
|
|
73
74
|
}
|
|
74
75
|
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
75
76
|
async runCheckpoint(context, fingerprint) {
|
|
@@ -193,7 +194,9 @@ export class KibiCheckpointRunner {
|
|
|
193
194
|
const policy = computeEnforcementPolicy({
|
|
194
195
|
resolvedContext: context.workContext,
|
|
195
196
|
effectiveMode: "hard",
|
|
196
|
-
lifecycleEvents: context.lifecycleEvents ?? [
|
|
197
|
+
lifecycleEvents: context.lifecycleEvents ?? [
|
|
198
|
+
defaultLifecycleEvent(context),
|
|
199
|
+
],
|
|
197
200
|
pathKinds: context.pathKinds ?? ["code"],
|
|
198
201
|
linkedEntityResults: context.linkedEntityResults ?? [
|
|
199
202
|
{ ids: [], source: "none" },
|
|
@@ -73,6 +73,7 @@ const TEST_CUES = [
|
|
|
73
73
|
* Analyze a comment or prose block and suggest most appropriate entity type.
|
|
74
74
|
*/
|
|
75
75
|
export function classifyKnowledge(text) {
|
|
76
|
+
// implements REQ-opencode-kibi-plugin-v1
|
|
76
77
|
if (!text || text.trim().length < 50) {
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
@@ -94,7 +95,7 @@ export function classifyKnowledge(text) {
|
|
|
94
95
|
bestMatch = {
|
|
95
96
|
type: "fact",
|
|
96
97
|
confidence: factScore >= 3 ? "high" : "medium",
|
|
97
|
-
reasoning:
|
|
98
|
+
reasoning: "Contains strict domain fact cues (invariants, properties, limits, cardinalities) for the strict fact lane",
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
if (reqScore > maxMatches) {
|