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.
Files changed (62) hide show
  1. package/README.md +18 -12
  2. package/dist/config.d.ts +0 -3
  3. package/dist/config.js +0 -9
  4. package/dist/e2e-coverage-signals.js +1 -3
  5. package/dist/enforcement-policy.d.ts +2 -0
  6. package/dist/enforcement-policy.js +18 -3
  7. package/dist/enforcement-scope.js +1 -3
  8. package/dist/file-entity-links.js +2 -6
  9. package/dist/file-operation-reminders.d.ts +4 -4
  10. package/dist/file-operation-state.js +4 -2
  11. package/dist/init-kibi-alias.js +0 -1
  12. package/dist/init-kibi-capability.js +1 -5
  13. package/dist/kb-freshness-state.d.ts +62 -0
  14. package/dist/kb-freshness-state.js +215 -0
  15. package/dist/kibi-checkpoint-runner.d.ts +1 -1
  16. package/dist/kibi-checkpoint-runner.js +6 -3
  17. package/dist/knowledge-classifier.js +2 -1
  18. package/dist/meaningful-change-classifier.d.ts +30 -0
  19. package/dist/meaningful-change-classifier.js +103 -0
  20. package/dist/plugin-startup.js +0 -1
  21. package/dist/plugin.js +101 -391
  22. package/dist/prompt.d.ts +6 -5
  23. package/dist/prompt.js +48 -84
  24. package/dist/reconcile-engine.d.ts +15 -1
  25. package/dist/reconcile-engine.js +1 -3
  26. package/dist/scheduler.js +16 -4
  27. package/dist/session-edit-state.js +2 -1
  28. package/dist/source-linked-guidance.js +3 -1
  29. package/dist/startup-notifier.d.ts +7 -1
  30. package/dist/startup-notifier.js +51 -17
  31. package/dist/tui.js +1 -403
  32. package/dist/tui.jsx +1 -113
  33. package/dist/version-metadata.d.ts +13 -0
  34. package/dist/version-metadata.js +78 -0
  35. package/dist/version-metadata.json +1 -0
  36. package/package.json +5 -7
  37. package/dist/brief-delivery-reasons.d.ts +0 -12
  38. package/dist/brief-delivery-reasons.js +0 -132
  39. package/dist/brief-intent.d.ts +0 -41
  40. package/dist/brief-intent.js +0 -142
  41. package/dist/briefing-runtime.d.ts +0 -24
  42. package/dist/briefing-runtime.js +0 -277
  43. package/dist/graph-narrator.d.ts +0 -25
  44. package/dist/graph-narrator.js +0 -408
  45. package/dist/idle-brief-audit.d.ts +0 -36
  46. package/dist/idle-brief-audit.js +0 -186
  47. package/dist/idle-brief-paths.d.ts +0 -6
  48. package/dist/idle-brief-paths.js +0 -120
  49. package/dist/idle-brief-reader.d.ts +0 -37
  50. package/dist/idle-brief-reader.js +0 -163
  51. package/dist/idle-brief-runtime.d.ts +0 -55
  52. package/dist/idle-brief-runtime.js +0 -496
  53. package/dist/idle-brief-store.d.ts +0 -113
  54. package/dist/idle-brief-store.js +0 -262
  55. package/dist/session-fingerprint.d.ts +0 -11
  56. package/dist/session-fingerprint.js +0 -21
  57. package/dist/tui-brief-delivery.d.ts +0 -67
  58. package/dist/tui-brief-delivery.js +0 -279
  59. package/dist/tui-brief-view-model.d.ts +0 -63
  60. package/dist/tui-brief-view-model.js +0 -209
  61. package/dist/utils/brief-marker.d.ts +0 -19
  62. 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 micro-briefs**: 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 `/brief-kibi` cue so agents can start with an explicit Kibi briefing before acting, while staying inside the same single prompt block and token budget.
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. For rich briefing delivery, the plugin uses the `kibi.brief` route and the `kibi.open_latest_brief` command for manual retrieval.
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 briefings and prevents build/editor artifacts from triggering KB sync work.
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
@@ -15,9 +15,6 @@ export interface KibiConfig {
15
15
  toastFailures: boolean;
16
16
  toastSuccesses: boolean;
17
17
  toastCooldownMs: number;
18
- briefs?: {
19
- autoSubmit: boolean;
20
- };
21
18
  };
22
19
  guidance: {
23
20
  dynamic: boolean;
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;
@@ -19,9 +19,7 @@ function readTestDoc(worktree, testId) {
19
19
  const content = readFileSync(fullPath, "utf8");
20
20
  return parseTestDoc(content, testId);
21
21
  }
22
- catch {
23
- continue;
24
- }
22
+ catch { }
25
23
  }
26
24
  }
27
25
  return null;
@@ -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
- ? { ids: input.linkedEntityIds[index] ?? [], source: "symbols" }
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" && event.e2eSignal.reminderText !== null) {
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" && event.linkedEntityResult.ids.length === 0);
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") && incoming === "deleted") {
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" && (incoming === "created" || incoming === "edited")) {
45
+ if (existing === "deleted" &&
46
+ (incoming === "created" || incoming === "edited")) {
45
47
  return "deleted";
46
48
  }
47
49
  // Fallback: use incoming
@@ -11,7 +11,6 @@
11
11
  * - "kb_check"
12
12
  * - "sequential" or similar ordering language
13
13
  */
14
- // implements REQ-opencode-kibi-briefing-v2
15
14
  export function buildInitKibiAlias() {
16
15
  const lines = [
17
16
  "# /init-kibi: Interactive Activation",
@@ -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 ?? 0) > 0;
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 ?? [defaultLifecycleEvent(context)],
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: 'Contains strict domain fact cues (invariants, properties, limits, cardinalities) for the strict fact lane',
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) {