kibi-opencode 0.19.4 → 2.0.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.
Files changed (38) hide show
  1. package/README.md +8 -6
  2. package/dist/auto-update.d.ts +2 -0
  3. package/dist/auto-update.js +5 -4
  4. package/dist/e2e-coverage-signals.js +3 -3
  5. package/dist/enforcement-policy.d.ts +10 -0
  6. package/dist/enforcement-policy.js +23 -23
  7. package/dist/file-entity-links.d.ts +4 -0
  8. package/dist/file-entity-links.js +6 -4
  9. package/dist/file-filter.d.ts +9 -9
  10. package/dist/file-filter.js +16 -68
  11. package/dist/kibi-bootstrap-alias.d.ts +2 -0
  12. package/dist/kibi-bootstrap-alias.js +14 -0
  13. package/dist/kibi-bootstrap-capability.d.ts +34 -0
  14. package/dist/{init-kibi-capability.js → kibi-bootstrap-capability.js} +26 -16
  15. package/dist/meaningful-change-classifier.d.ts +1 -0
  16. package/dist/meaningful-change-classifier.js +40 -10
  17. package/dist/path-kind.js +21 -39
  18. package/dist/plugin-startup.js +3 -3
  19. package/dist/plugin.d.ts +4 -1
  20. package/dist/plugin.js +81 -374
  21. package/dist/prompt.d.ts +4 -4
  22. package/dist/prompt.js +24 -60
  23. package/dist/repo-posture.js +31 -84
  24. package/dist/risk-classifier.d.ts +3 -3
  25. package/dist/risk-classifier.js +9 -6
  26. package/dist/scheduler.js +2 -2
  27. package/dist/session-edit-state.d.ts +4 -0
  28. package/dist/session-edit-state.js +5 -3
  29. package/dist/smart-enforcement-events.d.ts +100 -0
  30. package/dist/smart-enforcement-events.js +414 -0
  31. package/dist/version-metadata.json +1 -1
  32. package/dist/work-context-resolver.d.ts +10 -0
  33. package/dist/work-context-resolver.js +15 -17
  34. package/dist/workspace-health.js +8 -56
  35. package/package.json +6 -4
  36. package/dist/init-kibi-alias.d.ts +0 -14
  37. package/dist/init-kibi-alias.js +0 -43
  38. package/dist/init-kibi-capability.d.ts +0 -32
package/README.md CHANGED
@@ -52,7 +52,7 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
52
52
 
53
53
  - **Repo posture detection**: distinguishes `root_active`, `root_partial`, `root_uninitialized`, `vendored_only`, and `hybrid_root_plus_vendored`
54
54
  - **Risk classification**: separates `safe_docs_only`, `safe_test_only`, `kb_doc_structural`, `req_policy_candidate`, `behavior_candidate`, `traceability_candidate`, and `manual_kb_edit`
55
- - **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.
55
+ - **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 `.kb/symbols.yaml`. Skip on cache hit.
56
56
  - **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.
57
57
  - **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 capability-based recovery steps until the Kibi checkpoint passes. `maintenanceDegraded` overrides everything back to `advisory`.
58
58
  - **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)
@@ -155,9 +155,11 @@ The plugin injects guidance into OpenCode sessions to improve agent grounding. U
155
155
 
156
156
  ### Bootstrap Command
157
157
 
158
- OpenCode exposes Kibi MCP prompts as slash commands. The \`/init-kibi\` command triggers the \`kb_autopilot_generate\` workflow to assist in retroactive bootstrap. Agents use visible MCP tools when available, a trusted project-local CLI JSON route with `--input` when MCP is unavailable, and stop for operator action when neither interface is available.
158
+ OpenCode exposes the canonical bootstrap skill as `/kibi-bootstrap`. The
159
+ command routes an explicit request to the read-only planner and its typed
160
+ approval/apply workflow.
159
161
 
160
- OpenCode may show Kibi MCP tools with the configured server prefix. For example, canonical MCP names such as `kb_autopilot_generate`, `kb_upsert`, and `kb_check` can appear to agents as `kibi_kb_autopilot_generate`, `kibi_kb_upsert`, and `kibi_kb_check`. Use the visible `kibi_kb_*` identifier when OpenCode requires an exact tool name; it maps to the same MCP operation.
162
+ OpenCode may show Kibi MCP tools with the configured server prefix. For example, canonical MCP names such as `kb_plan_bootstrap`, `kb_upsert`, and `kb_check` can appear to agents as `kibi_kb_plan_bootstrap`, `kibi_kb_upsert`, and `kibi_kb_check`. Use the visible `kibi_kb_*` identifier when OpenCode requires an exact tool name; it maps to the same MCP operation.
161
163
 
162
164
  ### Discovery-first capability guidance
163
165
 
@@ -301,7 +303,7 @@ Disable specific features while keeping others:
301
303
 
302
304
  ## Troubleshooting
303
305
 
304
- If you see a false "workspace needs Kibi bootstrap" warning even though your workspace is already initialized with `.kb/config.json` pointing at relocated `kibi-docs/*` paths, this indicates a stale plugin cache. See [the main troubleshooting docs](../../docs/troubleshooting.md#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
306
+ If you see a false "workspace needs Kibi bootstrap" warning even though `.kb/manifest.json` and the canonical knowledge lanes exist, this usually means a stale plugin cache. Leftover `.kb/config.json` custom paths are ignored. See [the main troubleshooting docs](../../docs/troubleshooting.md#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
305
307
 
306
308
  ## Architecture
307
309
 
@@ -318,7 +320,7 @@ A proposed enhancement would inject Kibi context hints into file-read results (e
318
320
 
319
321
  1. OpenCode's current plugin surface does not expose file-content interception hooks
320
322
  2. The `experimental.chat.system.transform` hook only supports system prompt injection
321
- 3. Symbol metadata from `documentation/symbols.yaml` can inform this feature once host support exists
323
+ 3. Symbol metadata from `.kb/symbols.yaml` can inform this feature once host support exists
322
324
 
323
325
  Current workaround: static system prompt guidance directs agents to query Kibi explicitly.
324
326
 
@@ -335,7 +337,7 @@ The plugin provides proactive guidance when agents perform file operations:
335
337
  The OpenCode plugin respects the repository ignore policy used by Kibi's discovery pipeline. In practice this means:
336
338
 
337
339
  - The plugin's background sync and file-event handling skip paths matched by repository `.gitignore` files, nested `.gitignore` files, and `.git/info/exclude`.
338
- - 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`).
340
+ - The plugin also treats a curated set of tool/runtime directories as never-relevant for KB sync (for example: `.sisyphus`, `.opencode`, `.git`, `node_modules`, `vendor`, `third_party`). Derived `.kb/branches`, `.kb/recovery`, `.kb/verification`, and `.kb/briefs` trees are ignored; authored knowledge lanes under `.kb/` are not.
339
341
 
340
342
  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.
341
343
 
@@ -23,9 +23,11 @@ export interface AutoUpdateRunnerDeps {
23
23
  log: (message: string, metadata?: Record<string, unknown>) => void;
24
24
  }
25
25
  export declare function findKibiOpencodePluginEntry(directory: string): KibiOpencodePluginEntry | null;
26
+ export declare function findPackageJsonUp(startUrl: string): string | null;
26
27
  export declare function getCachedPluginVersion(): string | null;
27
28
  export declare function getLatestPluginVersion(channel?: string): Promise<string | null>;
28
29
  export declare function invalidateKibiOpencodePackage(): boolean;
29
30
  export declare function runBunInstallForOpenCodePlugin(): Promise<boolean>;
30
31
  export declare function createAutoUpdateRunner(deps: AutoUpdateRunnerDeps): (input: AutoUpdateInput) => Promise<AutoUpdateResult>;
32
+ export declare function notifyAutoUpdate(message: string): Promise<void>;
31
33
  export declare const runKibiOpencodeAutoUpdate: (input: AutoUpdateInput) => Promise<AutoUpdateResult>;
@@ -188,7 +188,7 @@ function readVersionFromPackageJson(packageJsonPath) {
188
188
  const parsed = parseJsonObject(fs.readFileSync(packageJsonPath, "utf8"));
189
189
  return typeof parsed?.version === "string" ? parsed.version : null;
190
190
  }
191
- function findPackageJsonUp(startUrl) {
191
+ export function findPackageJsonUp(startUrl) {
192
192
  let current = path.dirname(fileURLToPath(startUrl));
193
193
  while (true) {
194
194
  const packageJsonPath = path.join(current, "package.json");
@@ -384,13 +384,14 @@ export function createAutoUpdateRunner(deps) {
384
384
  return { status: "updated", currentVersion, latestVersion };
385
385
  };
386
386
  }
387
+ export async function notifyAutoUpdate(message) {
388
+ logger.info("auto-update.notification", { message });
389
+ }
387
390
  export const runKibiOpencodeAutoUpdate = createAutoUpdateRunner({
388
391
  getCurrentVersion: getCachedPluginVersion,
389
392
  getLatestVersion: getLatestPluginVersion,
390
393
  invalidatePackage: invalidateKibiOpencodePackage,
391
394
  runInstall: runBunInstallForOpenCodePlugin,
392
- notify: async (message) => {
393
- logger.info("auto-update.notification", { message });
394
- },
395
+ notify: notifyAutoUpdate,
395
396
  log: (message, metadata) => logger.info(message, metadata),
396
397
  });
@@ -4,13 +4,13 @@ import * as path from "node:path";
4
4
  import { getFileLinkedTargetsByType } from "./file-entity-links.js";
5
5
  // ── TEST doc reader ──────────────────────────────────────────────
6
6
  //
7
- // Reads a TEST-*.md file from documentation/tests/ and extracts
7
+ // Reads a TEST-*.md file from .kb/tests/ and extracts
8
8
  // frontmatter tags, source, and body.
9
9
  function readTestDoc(worktree, testId) {
10
10
  // Try common locations for TEST docs
11
11
  const candidates = [
12
- `documentation/tests/${testId}.md`,
13
- `documentation/tests/${testId.toLowerCase()}.md`,
12
+ `.kb/tests/${testId}.md`,
13
+ `.kb/tests/${testId.toLowerCase()}.md`,
14
14
  ];
15
15
  for (const rel of candidates) {
16
16
  const fullPath = path.join(worktree, rel);
@@ -47,6 +47,11 @@ export interface EnforcementPolicyInput {
47
47
  /** Posture fallback when resolvedContext is unavailable. */
48
48
  posture?: RepoPosture | undefined;
49
49
  }
50
+ export interface NormalizedPolicyEvent extends EnforcementLifecycleEvent {
51
+ pathKind: PathKind;
52
+ linkedEntityResult: PolicyLinkedEntityResult;
53
+ e2eSignal: E2eCoverageSignal;
54
+ }
50
55
  interface EnforcementPolicyResultBase {
51
56
  affectedPaths: string[];
52
57
  dirtyFileCount: number;
@@ -70,4 +75,9 @@ export type EnforcementPolicyResult = (EnforcementPolicyResultBase & {
70
75
  text: null;
71
76
  });
72
77
  export declare function computeEnforcementPolicy(input: EnforcementPolicyInput): EnforcementPolicyResult;
78
+ export declare function advisoryResultFromEvents(relevantEvents: readonly NormalizedPolicyEvent[], extras: {
79
+ affectedPaths: string[];
80
+ e2eReminder: string | null;
81
+ reminderKindsToMark: ReminderKind[];
82
+ }): EnforcementPolicyResult;
73
83
  export {};
@@ -185,30 +185,20 @@ function hardBlockText(events) {
185
185
  const representativePath = events[0]?.normalizedPath ?? "<changed-file>";
186
186
  const evidenceNotes = [linkedIdsText(events), e2eEvidenceText(events)].filter((note) => note !== null);
187
187
  const deletionCleanup = deletedWithoutLinks
188
- ? "Deleted files without linked IDs still need sourceFile cleanup: use `kb_search` plus `kb_query` with `sourceFile` for the deleted path before deciding whether `kb_upsert` cleanup is needed."
189
- : "Use `kb_upsert` when traceability, relationships, or source-linked facts need updates.";
188
+ ? "Deleted paths without linked IDs still require a source-linked traceability review before cleanup."
189
+ : "Use the canonical traceability workflow when relationships or source-linked facts need updates.";
190
190
  return {
191
191
  text: [
192
192
  "🛑 **Hard Kibi checkpoint required**",
193
193
  "Changed relevant files require Kibi verification before continuing:",
194
194
  ...pathLines,
195
195
  ...evidenceNotes,
196
- "Capability-based checkpoint instructions:",
197
- "- MCP tools and the trusted project-local CLI are peer surfaces over the same 18 operations; choose by what is visible and approved here.",
198
- "- If Kibi MCP tools are visible and approved, use MCP.",
199
- "- Otherwise, in a trusted workspace, use the project-local CLI's dedicated JSON routes with `--input <file|->`.",
200
- "- If neither interface is available, Kibi operation is blocked; tell the operator to enable MCP or the trusted project-local CLI.",
201
- "- Do not infer MCP availability from config file existence. Do not read or edit `.kb/` files directly.",
202
- "- Query before mutate. Run `kb_upsert` sequentially. Run `kb_check` before completion.",
203
- "- Run `kb_search` to discover impacted requirements, tests, ADRs, and facts.",
204
- `- Run \`kb_query\` with \`sourceFile\` (example sourceFile: \"${representativePath}\") for each listed path.`,
205
- "- Run `kb_status` if branch or snapshot freshness matters.",
196
+ "- Inspect typed Kibi status `nextActions` and route to the canonical kibi-usage, kibi-freshness, or kibi-traceability skill.",
197
+ "- Use the host-visible approved Kibi peer surface; if it is unavailable, report the blocked capability.",
198
+ "- Never read or edit `.kb/` files directly.",
199
+ `- Preserve source-linked evidence for \`${representativePath}\` through the typed traceability workflow.`,
206
200
  `- ${deletionCleanup}`,
207
- "- Run `kb_check` before completing the task.",
208
- "KB freshness resolution:",
209
- "- **KB updated**: run `kb_search` for discovery, then `kb_upsert`/`kb_delete` for mutations, then `kb_check`.",
210
- "- **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`.",
211
- "- **Deferred/failed**: do not claim task completion.",
201
+ "- Follow typed check/status next actions before completing the task; unresolved or blocked state is not completion.",
212
202
  ].join("\n"),
213
203
  shownPaths,
214
204
  remainingCount,
@@ -276,23 +266,33 @@ export function computeEnforcementPolicy(input) {
276
266
  text: null,
277
267
  };
278
268
  }
269
+ return advisoryResultFromEvents(relevantEvents, {
270
+ affectedPaths,
271
+ e2eReminder,
272
+ reminderKindsToMark,
273
+ });
274
+ }
275
+ // Defensive empty-list checkpoint. `computeEnforcementPolicy` returns earlier
276
+ // when `relevantEvents` is empty; tests call this helper directly so the
277
+ // branch stays executable and covered.
278
+ export function advisoryResultFromEvents(relevantEvents, extras) {
279
279
  const firstRelevantEvent = relevantEvents[0];
280
280
  if (!firstRelevantEvent) {
281
281
  return {
282
282
  kind: "checkpoint_passed",
283
- affectedPaths,
283
+ affectedPaths: extras.affectedPaths,
284
284
  dirtyFileCount: 0,
285
- e2eReminder,
286
- reminderKindsToMark,
285
+ e2eReminder: extras.e2eReminder,
286
+ reminderKindsToMark: extras.reminderKindsToMark,
287
287
  text: null,
288
288
  };
289
289
  }
290
290
  return {
291
291
  kind: "advisory_guidance",
292
- affectedPaths,
292
+ affectedPaths: extras.affectedPaths,
293
293
  dirtyFileCount: relevantEvents.length,
294
- e2eReminder,
295
- reminderKindsToMark,
294
+ e2eReminder: extras.e2eReminder,
295
+ reminderKindsToMark: extras.reminderKindsToMark,
296
296
  text: advisoryText(firstRelevantEvent),
297
297
  };
298
298
  }
@@ -1,3 +1,7 @@
1
+ export declare function emptyLinkedTargets(): {
2
+ ids: [];
3
+ source: "none";
4
+ };
1
5
  export type SymbolsManifestRow = {
2
6
  id?: string;
3
7
  sourceFile?: string;
@@ -2,6 +2,10 @@
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { loadKbSyncPaths } from "./file-filter.js";
5
+ // ── Shared types ────────────────────────────────────────────────────
6
+ export function emptyLinkedTargets() {
7
+ return { ids: [], source: "none" };
8
+ }
5
9
  // ── Lightweight YAML parser (symbols.yaml subset) ───────────────────
6
10
  //
7
11
  // Handles:
@@ -139,14 +143,12 @@ function resolveDocPathIdentity(relPath, syncPaths) {
139
143
  if (!pattern)
140
144
  continue;
141
145
  // Strip glob from pattern to get the root dir prefix
142
- // e.g. "documentation/requirements/**/*.md" → "documentation/requirements"
146
+ // e.g. ".kb/requirements/**/*.md" → ".kb/requirements"
143
147
  const rootDir = pattern.replace(/\/\*\*\/.*$/, "").replace(/\/+$/, "");
144
148
  if (normalizedRel.startsWith(`${rootDir}/`)) {
145
149
  return entityId;
146
150
  }
147
151
  }
148
- // If no specific root matched but path starts with documentation/,
149
- // still accept (covers default configuration)
150
152
  return null;
151
153
  }
152
154
  // ── Symbols file resolution ─────────────────────────────────────────
@@ -220,7 +222,7 @@ export function getFileLinkedEntityIds(worktree, filePath) {
220
222
  return { ids: orderedIds.slice(0, 3), source: "symbols" };
221
223
  }
222
224
  catch {
223
- return { ids: [], source: "none" };
225
+ return emptyLinkedTargets();
224
226
  }
225
227
  }
226
228
  // implements REQ-opencode-file-context-guidance-v1
@@ -1,12 +1,12 @@
1
- export declare function loadKbSyncPaths(cwd?: string): {
2
- requirements: string;
3
- scenarios: string;
4
- tests: string;
5
- adr: string;
6
- flags: string;
7
- events: string;
8
- facts: string;
9
- symbols: string;
1
+ export declare function loadKbSyncPaths(_cwd?: string): {
2
+ readonly requirements: ".kb/requirements/**/*.md";
3
+ readonly scenarios: ".kb/scenarios/**/*.md";
4
+ readonly tests: ".kb/tests/**/*.md";
5
+ readonly adr: ".kb/adr/**/*.md";
6
+ readonly flags: ".kb/flags/**/*.md";
7
+ readonly events: ".kb/events/**/*.md";
8
+ readonly facts: ".kb/facts/**/*.md";
9
+ readonly symbols: ".kb/symbols.yaml";
10
10
  };
11
11
  export interface KbExistenceTarget {
12
12
  key: string;
@@ -1,8 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
1
  // implements REQ-opencode-kibi-plugin-v1
3
2
  import { createRequire } from "node:module";
4
3
  import * as path from "node:path";
5
- import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
6
4
  const _require = createRequire(import.meta.url);
7
5
  // Lightweight fallback matcher if picomatch isn't installed.
8
6
  let picomatch;
@@ -23,40 +21,20 @@ catch {
23
21
  },
24
22
  };
25
23
  }
26
- // Local copy of DEFAULT_CONFIG.paths to avoid cross-package TS rootDir issues.
27
- // Must stay in sync with DEFAULT_CONFIG.paths in packages/cli/src/utils/config.ts.
28
- const DEFAULT_SYNC_PATHS = {
29
- requirements: "documentation/requirements/**/*.md",
30
- scenarios: "documentation/scenarios/**/*.md",
31
- tests: "documentation/tests/**/*.md",
32
- adr: "documentation/adr/**/*.md",
33
- flags: "documentation/flags/**/*.md",
34
- events: "documentation/events/**/*.md",
35
- facts: "documentation/facts/**/*.md",
36
- symbols: "documentation/symbols.yaml",
24
+ // Canonical Kibi knowledge paths — must stay in sync with
25
+ // packages/cli/src/utils/kb-paths.ts CANONICAL_ENTITY_PATHS.
26
+ const CANONICAL_KB_PATHS = {
27
+ requirements: ".kb/requirements/**/*.md",
28
+ scenarios: ".kb/scenarios/**/*.md",
29
+ tests: ".kb/tests/**/*.md",
30
+ adr: ".kb/adr/**/*.md",
31
+ flags: ".kb/flags/**/*.md",
32
+ events: ".kb/events/**/*.md",
33
+ facts: ".kb/facts/**/*.md",
34
+ symbols: ".kb/symbols.yaml",
37
35
  };
38
- function loadSyncConfigLocal(cwd = process.cwd()) {
39
- const configPath = path.join(cwd, ".kb/config.json");
40
- let userConfig = {};
41
- if (existsSync(configPath)) {
42
- try {
43
- userConfig = JSON.parse(readFileSync(configPath, "utf8")) || {};
44
- }
45
- catch {
46
- userConfig = {};
47
- }
48
- }
49
- return {
50
- paths: {
51
- ...DEFAULT_SYNC_PATHS,
52
- ...(userConfig.paths ?? {}),
53
- },
54
- defaultBranch: userConfig.defaultBranch,
55
- };
56
- }
57
- export function loadKbSyncPaths(cwd = process.cwd()) {
58
- const cfg = loadSyncConfigLocal(cwd);
59
- return cfg.paths ?? DEFAULT_SYNC_PATHS;
36
+ export function loadKbSyncPaths(_cwd = process.cwd()) {
37
+ return CANONICAL_KB_PATHS;
60
38
  }
61
39
  // implements REQ-opencode-kibi-plugin-v1
62
40
  export function getKbExistenceTargets(cwd = process.cwd()) {
@@ -117,32 +95,12 @@ function normalizePattern(p) {
117
95
  // otherwise treat directory as markdown collection
118
96
  return `${p.replace(/\/+$/, "")}/**/*.md`;
119
97
  }
120
- const DEFAULT_IGNORES = [
121
- ".kb/**",
122
- ".git/**",
123
- "node_modules/**",
124
- "dist/**",
125
- "coverage/**",
126
- ".opencode/**",
127
- "**/*~",
128
- "**/~*",
129
- "**/.#*",
130
- "**/*.swp",
131
- "**/*.swo",
132
- "**/.DS_Store",
133
- ];
134
98
  // implements REQ-opencode-kibi-plugin-v1
135
99
  export function shouldHandleFile(filePath, cwd = process.cwd()) {
136
100
  const rel = path.isAbsolute(filePath)
137
101
  ? path.relative(cwd, filePath).split(path.sep).join("/")
138
102
  : filePath.split(path.sep).join("/");
139
- // Check shared ignore policy (root .gitignore, nested .gitignore, .git/info/exclude,
140
- // and hard denylist such as .sisyphus) before other pattern checks.
141
- const policy = createRepoIgnorePolicy(cwd);
142
- if (policy.isIgnored(rel))
143
- return false;
144
103
  const paths = loadKbSyncPaths(cwd);
145
- // Build include patterns from kibi paths
146
104
  const includeCandidates = [
147
105
  paths.requirements,
148
106
  paths.scenarios,
@@ -156,21 +114,11 @@ export function shouldHandleFile(filePath, cwd = process.cwd()) {
156
114
  const includePatterns = includeCandidates
157
115
  .map(normalizePattern)
158
116
  .filter((p) => Boolean(p));
159
- // default ignores then allow extension by .kb/config.json -> sync.ignore (not implemented here)
160
- const ignorePatterns = DEFAULT_IGNORES;
161
- // Compile matchers
162
- const isIgnored = ignorePatterns.some((ig) => picomatch.isMatch(rel, ig));
163
- if (isIgnored)
164
- return false;
165
- // If any include pattern matches, accept
166
117
  const included = includePatterns.some((pat) => picomatch.isMatch(rel, pat));
167
- if (included)
118
+ if (included ||
119
+ (paths.symbols !== undefined &&
120
+ (paths.symbols === rel || picomatch.isMatch(rel, paths.symbols)))) {
168
121
  return true;
169
- // If symbols path is configured as exact file and matches exactly, accept
170
- if (paths.symbols) {
171
- const sym = paths.symbols;
172
- if (sym === rel || picomatch.isMatch(rel, sym))
173
- return true;
174
122
  }
175
123
  return false;
176
124
  }
@@ -0,0 +1,2 @@
1
+ /** Builds the host-local route to the canonical kibi-bootstrap skill. */
2
+ export declare function buildKibiBootstrapAlias(): string;
@@ -0,0 +1,14 @@
1
+ /** Builds the host-local route to the canonical kibi-bootstrap skill. */
2
+ // implements REQ-KIBI-BOOTSTRAP-PLAN
3
+ export function buildKibiBootstrapAlias() {
4
+ const lines = [
5
+ "# /kibi-bootstrap",
6
+ "",
7
+ "Bootstrap Kibi knowledge for an existing repository.",
8
+ "",
9
+ "## Route",
10
+ "If Kibi infrastructure is missing, run `kibi init` first.",
11
+ "Then invoke the canonical `kibi-bootstrap` skill. OpenCode may expose this route as a native command or as `/kibi:kibi-bootstrap:mcp`; use whichever host syntax is available.",
12
+ ];
13
+ return lines.join("\n");
14
+ }
@@ -0,0 +1,34 @@
1
+ export declare const KIBI_BOOTSTRAP_COMMAND_NAME = "kibi-bootstrap";
2
+ export declare const KIBI_BOOTSTRAP_COMMAND_TEMPLATE: string;
3
+ export declare const KIBI_BOOTSTRAP_COMMAND_DESCRIPTION = "Bootstrap Kibi knowledge for an existing repository.";
4
+ export interface OpenCodeCommandDefinition {
5
+ template: string;
6
+ description?: string;
7
+ agent?: string;
8
+ model?: string;
9
+ subtask?: boolean;
10
+ }
11
+ export interface OpenCodeConfigHookInput {
12
+ command?: Record<string, OpenCodeCommandDefinition>;
13
+ [key: string]: unknown;
14
+ }
15
+ export type KibiBootstrapCommandCapability = {
16
+ supported: true;
17
+ pluginVersion: string;
18
+ } | {
19
+ supported: false;
20
+ reason: string;
21
+ pluginVersion?: string;
22
+ };
23
+ interface KibiBootstrapCapabilityDetectionInput {
24
+ pluginVersion?: string;
25
+ pluginHooksDts?: string;
26
+ sdkTypesDts?: string;
27
+ }
28
+ export declare function findSdkPackageJsonForPluginRoot(pluginRoot: string): string | undefined;
29
+ export declare function capabilityInputsOrEmpty<T extends object>(load: () => T): T | Record<string, never>;
30
+ export declare function detectKibiBootstrapCommandCapability(input: KibiBootstrapCapabilityDetectionInput): KibiBootstrapCommandCapability;
31
+ export declare function getKibiBootstrapCommandCapability(): KibiBootstrapCommandCapability;
32
+ export declare function _resetKibiBootstrapCapabilityCacheForTests(): void;
33
+ export declare function registerKibiBootstrapCommand(configInput: unknown, capability?: KibiBootstrapCommandCapability): KibiBootstrapCommandCapability;
34
+ export {};
@@ -2,10 +2,10 @@ import fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { buildInitKibiAlias } from "./init-kibi-alias.js";
6
- export const INIT_KIBI_COMMAND_NAME = "init-kibi";
7
- export const INIT_KIBI_COMMAND_TEMPLATE = buildInitKibiAlias();
8
- export const INIT_KIBI_COMMAND_DESCRIPTION = "Run the Kibi interactive activation workflow."; // implements REQ-001
5
+ import { buildKibiBootstrapAlias } from "./kibi-bootstrap-alias.js";
6
+ export const KIBI_BOOTSTRAP_COMMAND_NAME = "kibi-bootstrap";
7
+ export const KIBI_BOOTSTRAP_COMMAND_TEMPLATE = buildKibiBootstrapAlias();
8
+ export const KIBI_BOOTSTRAP_COMMAND_DESCRIPTION = "Bootstrap Kibi knowledge for an existing repository."; // implements REQ-001
9
9
  const require = createRequire(import.meta.url);
10
10
  let cachedCapability = null;
11
11
  const initialProcessCwd = process.cwd();
@@ -111,6 +111,14 @@ function hasConfigHook(pluginHooksDts) {
111
111
  function hasConfigCommandField(sdkTypesDts) {
112
112
  return /\bcommand\??:\s*\{[\s\S]*?\[key:\s*string\]:\s*\{[\s\S]*?\btemplate:\s*string\s*;/.test(sdkTypesDts);
113
113
  }
114
+ export function capabilityInputsOrEmpty(load) {
115
+ try {
116
+ return load();
117
+ }
118
+ catch {
119
+ return {};
120
+ }
121
+ }
114
122
  function resolveHostCapabilityInputs() {
115
123
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
116
124
  const dogfoodHost = resolveDogfoodHostCapabilityInputs([
@@ -125,7 +133,7 @@ function resolveHostCapabilityInputs() {
125
133
  if (dogfoodHost) {
126
134
  return dogfoodHost;
127
135
  }
128
- try {
136
+ return capabilityInputsOrEmpty(() => {
129
137
  const pluginPackageJsonPath = require.resolve("@opencode-ai/plugin/package.json");
130
138
  const pluginRoot = path.dirname(pluginPackageJsonPath);
131
139
  const sdkPackageJsonPath = findSdkPackageJsonForPluginRoot(pluginRoot) ??
@@ -139,10 +147,7 @@ function resolveHostCapabilityInputs() {
139
147
  ...(pluginHooksDts ? { pluginHooksDts } : {}),
140
148
  ...(sdkTypesDts ? { sdkTypesDts } : {}),
141
149
  };
142
- }
143
- catch {
144
- return {};
145
- }
150
+ });
146
151
  }
147
152
  function isRecord(value) {
148
153
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -150,7 +155,7 @@ function isRecord(value) {
150
155
  function isCommandMap(value) {
151
156
  return isRecord(value);
152
157
  }
153
- export function detectInitKibiCommandCapability(input) {
158
+ export function detectKibiBootstrapCommandCapability(input) {
154
159
  const { pluginVersion, pluginHooksDts, sdkTypesDts } = input;
155
160
  if (typeof pluginHooksDts !== "string" || pluginHooksDts.length === 0) {
156
161
  return buildUnsupportedReason(pluginVersion, "host Hooks definition is unavailable for config hook inspection.");
@@ -169,14 +174,19 @@ export function detectInitKibiCommandCapability(input) {
169
174
  pluginVersion: pluginVersion ?? "unknown",
170
175
  };
171
176
  }
172
- export function getInitKibiCommandCapability() {
177
+ export function getKibiBootstrapCommandCapability() {
173
178
  if (cachedCapability?.supported) {
174
179
  return cachedCapability;
175
180
  }
176
- cachedCapability = detectInitKibiCommandCapability(resolveHostCapabilityInputs());
181
+ cachedCapability = detectKibiBootstrapCommandCapability(resolveHostCapabilityInputs());
177
182
  return cachedCapability;
178
183
  }
179
- export function registerInitKibiCommand(configInput, capability = getInitKibiCommandCapability()) {
184
+ // implements REQ-KIBI-BOOTSTRAP-PLAN
185
+ export function _resetKibiBootstrapCapabilityCacheForTests() {
186
+ cachedCapability = null;
187
+ }
188
+ // implements REQ-KIBI-BOOTSTRAP-PLAN
189
+ export function registerKibiBootstrapCommand(configInput, capability = getKibiBootstrapCommandCapability()) {
180
190
  if (!capability.supported) {
181
191
  return capability;
182
192
  }
@@ -189,9 +199,9 @@ export function registerInitKibiCommand(configInput, capability = getInitKibiCom
189
199
  }
190
200
  configInput.command = {
191
201
  ...(existingCommands ?? {}),
192
- [INIT_KIBI_COMMAND_NAME]: {
193
- template: INIT_KIBI_COMMAND_TEMPLATE,
194
- description: INIT_KIBI_COMMAND_DESCRIPTION,
202
+ [KIBI_BOOTSTRAP_COMMAND_NAME]: {
203
+ template: KIBI_BOOTSTRAP_COMMAND_TEMPLATE,
204
+ description: KIBI_BOOTSTRAP_COMMAND_DESCRIPTION,
195
205
  },
196
206
  };
197
207
  return capability;
@@ -10,6 +10,7 @@ import type { RiskClass } from "./risk-classifier.js";
10
10
  * - "ignored": The change is in a build artifact or third-party path — no KB
11
11
  * impact.
12
12
  */
13
+ export declare function isSafeDocsUnknownPath(riskClass: RiskClass, pathKind: PathKind): boolean;
13
14
  export type MeaningfulChangeClass = "requires-kb-evidence" | "advisory" | "ignored";
14
15
  /**
15
16
  * Classify whether a file change requires KB freshness evidence.
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Classification for whether a file change requires KB freshness evidence.
3
+ *
4
+ * - "requires-kb-evidence": The change is meaningful for KB freshness — the KB
5
+ * snapshot should be considered stale and needs a re-sync.
6
+ * - "advisory": The change is low-risk (lockfiles, docs-only) — advisory only.
7
+ * - "ignored": The change is in a build artifact or third-party path — no KB
8
+ * impact.
9
+ */
10
+ export function isSafeDocsUnknownPath(riskClass, pathKind) {
11
+ return riskClass === "safe_docs_only" && pathKind === "unknown";
12
+ }
1
13
  // Directories whose contents are always ignored for KB freshness.
2
14
  const IGNORED_DIRECTORIES = [
3
15
  ".git",
@@ -8,6 +20,26 @@ const IGNORED_DIRECTORIES = [
8
20
  "dist",
9
21
  "coverage",
10
22
  ];
23
+ /** Derived `.kb/` runtime trees — must stay in sync with kb-paths.ts DERIVED_KB_PREFIXES. */
24
+ const DERIVED_KB_PREFIXES = [
25
+ ".kb/branches",
26
+ ".kb/recovery",
27
+ ".kb/verification",
28
+ ".kb/briefs",
29
+ ".kb/migrations",
30
+ ];
31
+ function isDerivedKbPath(normalizedPath) {
32
+ for (const prefix of DERIVED_KB_PREFIXES) {
33
+ if (normalizedPath === prefix ||
34
+ normalizedPath.startsWith(`${prefix}/`) ||
35
+ normalizedPath.includes(`/${prefix}/`) ||
36
+ normalizedPath.endsWith(`/${prefix}`)) {
37
+ return true;
38
+ }
39
+ }
40
+ return (normalizedPath === ".kb/config.json" ||
41
+ normalizedPath.endsWith("/.kb/config.json"));
42
+ }
11
43
  // Filenames that are advisory-only (lockfiles).
12
44
  const ADVISORY_FILENAMES = new Set([
13
45
  "bun.lock",
@@ -35,7 +67,7 @@ const UPGRADE_RISK_CLASSES = new Set([
35
67
  "traceability_candidate",
36
68
  "req_policy_candidate",
37
69
  ]);
38
- function isIgnoredPath(normalizedPath, pathKind) {
70
+ function isIgnoredPath(normalizedPath) {
39
71
  // Check extension-based ignored patterns.
40
72
  if (normalizedPath.endsWith(".tsbuildinfo") ||
41
73
  normalizedPath.endsWith(".map")) {
@@ -48,12 +80,11 @@ function isIgnoredPath(normalizedPath, pathKind) {
48
80
  return true;
49
81
  }
50
82
  }
51
- // Special handling for .kb/ — only ignored when pathKind is NOT "kb".
52
- // Actual KB entity files (pathKind=kb) should still require evidence.
53
- if (pathKind !== "kb") {
54
- if (normalizedPath.startsWith(".kb/") || normalizedPath.includes("/.kb/")) {
55
- return true;
56
- }
83
+ // Derived `.kb/` runtime (stores, recovery, leftover config) is ignored.
84
+ // Canonical entity lanes, symbols, relationships, and the manifest are
85
+ // authored knowledge and still require evidence.
86
+ if (isDerivedKbPath(normalizedPath)) {
87
+ return true;
57
88
  }
58
89
  return false;
59
90
  }
@@ -77,7 +108,7 @@ function getFilename(normalizedPath) {
77
108
  export function classifyMeaningfulChange(params) {
78
109
  const { normalizedPath, pathKind, lifecycle: _lifecycle, riskClass } = params;
79
110
  // 1. IGNORED paths (for freshness contract)
80
- if (isIgnoredPath(normalizedPath, pathKind)) {
111
+ if (isIgnoredPath(normalizedPath)) {
81
112
  return "ignored";
82
113
  }
83
114
  // 2. ADVISORY paths (low-risk lockfiles)
@@ -94,9 +125,8 @@ export function classifyMeaningfulChange(params) {
94
125
  if (UPGRADE_RISK_CLASSES.has(riskClass)) {
95
126
  return "requires-kb-evidence";
96
127
  }
97
- if (riskClass === "safe_docs_only" && pathKind === "unknown") {
128
+ if (isSafeDocsUnknownPath(riskClass, pathKind))
98
129
  return "advisory";
99
- }
100
130
  }
101
131
  // 5. DEFAULT — safety default for unknown paths
102
132
  return "requires-kb-evidence";