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/dist/prompt.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { CommentAnalysisResult } from "./comment-analysis.js";
2
2
  import type { KibiConfig } from "./config.js";
3
3
  import type { GuidanceCache } from "./guidance-cache.js";
4
- import { type InitKibiCommandCapability } from "./init-kibi-capability.js";
5
4
  import type { KbFreshnessEvaluation } from "./kb-freshness-state.js";
5
+ import { type KibiBootstrapCommandCapability } from "./kibi-bootstrap-capability.js";
6
6
  import type { PathKind } from "./path-kind.js";
7
7
  import type { RepoPosture } from "./repo-posture.js";
8
8
  import type { RiskClass } from "./risk-classifier.js";
@@ -55,13 +55,13 @@ export interface PromptContext {
55
55
  /** Paths changed for freshness evaluation (capped at 5) */
56
56
  freshnessChangedPaths?: string[];
57
57
  }
58
- export declare function postureGuidance(posture: RepoPosture, capability?: InitKibiCommandCapability): string | null;
58
+ export declare function postureGuidance(posture: RepoPosture, capability?: KibiBootstrapCommandCapability): string | null;
59
59
  /**
60
60
  * Build prompt with contextual guidance based on posture, risk class, and cache state.
61
61
  */
62
- export declare function buildPrompt(context?: PromptContext, capability?: InitKibiCommandCapability): string;
62
+ export declare function buildPrompt(context?: PromptContext, capability?: KibiBootstrapCommandCapability): string;
63
63
  /**
64
64
  * Inject prompt guidance if not already present.
65
65
  */
66
- export declare function injectPrompt(current: string, config: KibiConfig, context?: PromptContext, capability?: InitKibiCommandCapability): string;
66
+ export declare function injectPrompt(current: string, config: KibiConfig, context?: PromptContext, capability?: KibiBootstrapCommandCapability): string;
67
67
  export { SENTINEL };
package/dist/prompt.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { isPluginEnabled } from "./config.js";
3
- import { getInitKibiCommandCapability, } from "./init-kibi-capability.js";
3
+ import { getKibiBootstrapCommandCapability, } from "./kibi-bootstrap-capability.js";
4
4
  import { getSourceLinkedRequirementIds } from "./source-linked-guidance.js";
5
5
  const SENTINEL = "<!-- kibi-opencode -->";
6
6
  // ── Token budget enforcement ───────────────────────────────────────────
@@ -56,17 +56,17 @@ function deriveFileBucket(pathKind) {
56
56
  function getFocusEdit(context) {
57
57
  return (context.focusEdit ?? context.recentEdits[context.recentEdits.length - 1]);
58
58
  }
59
- function buildInitKibiBootstrapReference(capability = getInitKibiCommandCapability()) {
59
+ function buildKibiBootstrapReference(capability = getKibiBootstrapCommandCapability()) {
60
60
  if (capability.supported) {
61
- return "Bootstrap existing repos: when the Kibi OpenCode plugin is active and native injection is supported, `/init-kibi` is the canonical short alias; `/kibi:init-kibi:mcp` remains the namespaced MCP fallback for the retroactive initialization (`kb_autopilot_generate`) workflow.";
61
+ return "Bootstrap existing repositories with `/kibi-bootstrap`; the canonical `kibi-bootstrap` skill owns planner, approval, apply, and repair semantics.";
62
62
  }
63
- return "Bootstrap existing repos: this host does not support native `/init-kibi` injection, so Kibi must fail closed and does not register a fake native alias; use `/kibi:init-kibi:mcp` for the retroactive initialization (`kb_autopilot_generate`) workflow.";
63
+ return "This host cannot inject native `/kibi-bootstrap`; use `/kibi:kibi-bootstrap:mcp` to route to the canonical `kibi-bootstrap` skill.";
64
64
  }
65
- function buildBootstrapRequiredBody(capability = getInitKibiCommandCapability()) {
66
- const commandBullet = capability.supported
67
- ? "- When the Kibi OpenCode plugin is active and native injection is supported, use `/init-kibi` as the canonical short alias; `/kibi:init-kibi:mcp` remains the namespaced MCP fallback."
68
- : "- This host does not support native `/init-kibi` injection. Kibi must fail closed and does not register a fake native alias; use `/kibi:init-kibi:mcp` instead.";
69
- return `Kibi capability selection (MCP tools and the trusted project-local CLI are peer surfaces over the same 18 operations):\n1. If Kibi MCP tools are visible and approved, use MCP.\n2. Otherwise, in a trusted workspace, use the project-local CLI's dedicated JSON routes with \`--input <file|->\`.\n3. If neither interface is available, Kibi operation is blocked; tell the operator.\nDo not infer MCP availability from config file existence. Do not read or edit \`.kb/\` files directly. Query before mutate. Run \`kb_upsert\` sequentially. Run \`kb_check\` before completion.\n\nThis repository does not appear to have Kibi initialized. Agents should:\n${commandBullet}\n- The workflow uses \`kb_autopilot_generate\` for read-only synthesis; always preview and get approval before writes.\n- Ask the operator to handle setup or repair if bootstrap is insufficient.\n- Load \`kibi-usage\` with \`kb_skills_load\` for comprehensive guidance.`;
65
+ function buildBootstrapRequiredBody(capability = getKibiBootstrapCommandCapability()) {
66
+ const command = capability.supported
67
+ ? "Use `/kibi-bootstrap`."
68
+ : "Use `/kibi:kibi-bootstrap:mcp`; this host does not support native command injection.";
69
+ return `Kibi infrastructure is not initialized. Run \`kibi init\`, then inspect \`kb_status.bootstrap.nextAction\` and route the explicit bootstrap request to the canonical \`kibi-bootstrap\` skill. ${command}`;
70
70
  }
71
71
  function buildHardGateBlock(block) {
72
72
  const pathLines = block.shownPaths.map((path) => `- \`${path}\``);
@@ -80,18 +80,7 @@ function buildHardGateBlock(block) {
80
80
  reasonLine,
81
81
  "Affected files:",
82
82
  ...pathLines,
83
- "Capability-based recovery steps:",
84
- "- MCP tools and the trusted project-local CLI are peer surfaces over the same 18 operations; choose by what is visible and approved here.",
85
- "- If Kibi MCP tools are visible and approved, use MCP.",
86
- "- Otherwise, in a trusted workspace, use the project-local CLI's dedicated JSON routes with `--input <file|->`.",
87
- "- If neither interface is available, Kibi operation is blocked; tell the operator to enable MCP or the trusted project-local CLI.",
88
- "- Do not infer MCP availability from config file existence. Do not read or edit `.kb/` files directly.",
89
- "- Query before mutate. Run `kb_upsert` sequentially. Run `kb_check` before completion.",
90
- "- Run `kb_search` for impacted requirements, tests, ADRs, and facts.",
91
- "- Run `kb_query` with `sourceFile` for each affected file.",
92
- "- Run `kb_status` if branch or snapshot freshness matters.",
93
- "- Run `kb_upsert` for required traceability, relationship, or fact updates.",
94
- "- Run `kb_check` before continuing.",
83
+ "Inspect typed Kibi status `nextActions`, then route to the canonical kibi-usage, kibi-freshness, or kibi-traceability skill. Use the host-visible approved peer surface and never read or edit `.kb` directly.",
95
84
  ]
96
85
  .filter((line) => line !== null)
97
86
  .join("\n");
@@ -123,17 +112,13 @@ Production code: use \`implements\` (symbol→req) for requirement ownership. Te
123
112
  - \`covered_by\` is coverage evidence only
124
113
  - Prefer scenario-first: req→scenario→test when scenarios exist
125
114
  - Route durable knowledge comments to KB entities, not inline comments
126
- - documentation/symbols.yaml refresh is required when extraction output changes; do not revert as scope creep`,
115
+ - .kb/symbols.yaml refresh is required when extraction output changes; do not revert as scope creep`,
127
116
  manual_kb_edit: `⚠️ **WARNING: Direct .kb/ edits bypass validation**
128
117
 
129
- The Kibi knowledge base is managed through visible MCP tools or the trusted project-local CLI — peer surfaces over the same 18 operations. Do not read or edit \`.kb/\` files directly.
130
- - Query before mutate and run kb_upsert sequentially
131
- - Use kb_upsert to create/update entities
132
- - Use kb_delete to remove entities
133
- - Use kb_check to validate consistency`,
118
+ Do not read or edit \`.kb/\` directly. Follow typed Kibi status \`nextActions\` and route durable changes through the canonical kibi-usage, kibi-freshness, or kibi-traceability skill.`,
134
119
  };
135
120
  // ── Posture overrides ──────────────────────────────────────────────────
136
- export function postureGuidance(posture, capability = getInitKibiCommandCapability()) {
121
+ export function postureGuidance(posture, capability = getKibiBootstrapCommandCapability()) {
137
122
  // implements REQ-opencode-prompt-injection
138
123
  switch (posture) {
139
124
  case "vendored_only":
@@ -146,7 +131,7 @@ ${buildBootstrapRequiredBody(capability)}`;
146
131
  case "root_partial":
147
132
  return `⚠️ **Partial KB setup detected**
148
133
 
149
- Root .kb/config.json exists but some configured KB targets are missing. Guidance remains advisory until the user/operator restores the configured KB targets.`;
134
+ Root .kb/manifest.json exists but some canonical KB targets under .kb/ are missing. Guidance remains advisory until the user/operator restores the canonical layout.`;
150
135
  default:
151
136
  return null;
152
137
  }
@@ -155,7 +140,7 @@ Root .kb/config.json exists but some configured KB targets are missing. Guidance
155
140
  /**
156
141
  * Build prompt guidance block based on posture, risk class, and cache state.
157
142
  */
158
- function buildContextualGuidance(context, capability = getInitKibiCommandCapability()) {
143
+ function buildContextualGuidance(context, capability = getKibiBootstrapCommandCapability()) {
159
144
  if (context.hardGateBlock) {
160
145
  return `${SENTINEL}\n\n${buildHardGateBlock(context.hardGateBlock)}`;
161
146
  }
@@ -437,7 +422,7 @@ function buildCommentSuggestionGuidance(suggestion) {
437
422
  Your recent code edit contains a comment that looks like a **strict domain fact** (invariants, properties, limits, defaults, or cardinality constraints).
438
423
 
439
424
  **Action**: Route to a FACT entity in the strict fact lane:
440
- - Create \`documentation/facts/FACT-xxx.md\` with the invariant (use \`constrains\` + \`requires_property\` for contradiction-safe reasoning)
425
+ - Create \`.kb/facts/FACT-xxx.md\` with the invariant (use \`constrains\` + \`requires_property\` for contradiction-safe reasoning)
441
426
  - Bug/workaround notes: use \`fact_kind: observation\` or \`meta\` instead — these are non-blocking and excluded from contradiction inference
442
427
  - Link it to relevant requirements
443
428
 
@@ -448,7 +433,7 @@ This keeps domain truths centralized, searchable, and contradiction-safe.`;
448
433
  Your recent code edit contains a comment that looks like a **technical decision** (tradeoffs, rationale, or architecture choices).
449
434
 
450
435
  **Action**: Instead of inline comments, route this to an ADR entity:
451
- - Create \`documentation/adr/ADR-xxx.md\` documenting the decision
436
+ - Create \`.kb/adr/ADR-xxx.md\` documenting the decision
452
437
  - Include context, options considered, and the chosen approach
453
438
  - Link to constrained code symbols
454
439
 
@@ -459,7 +444,7 @@ This preserves decision context for future maintainers.`;
459
444
  Your recent code edit contains a comment that looks like **behavior intent** (system capabilities or user-facing requirements).
460
445
 
461
446
  **Action**: Instead of inline comments, route this to a REQ entity:
462
- - Create \`documentation/requirements/REQ-xxx.md\` with the behavior description
447
+ - Create \`.kb/requirements/REQ-xxx.md\` with the behavior description
463
448
  - Add SCEN and TEST entities for specification and verification
464
449
  - Link code: production uses \`implements\` (symbol→req) for ownership; test code uses \`executable_for\`; \`covered_by\` is coverage evidence only
465
450
 
@@ -478,41 +463,20 @@ Before implementing or explaining code:
478
463
  /**
479
464
  * Build the static guidance block (original behavior).
480
465
  */
481
- function buildBaseGuidance(capability = getInitKibiCommandCapability()) {
466
+ function buildBaseGuidance(capability = getKibiBootstrapCommandCapability()) {
482
467
  return `${SENTINEL}
483
- This project uses Kibi through capability-selected MCP or CLI JSON routes. Prefer storing durable knowledge in Kibi over code comments.
468
+ Use typed Kibi status and its \`nextActions\` as the workflow contract. Route general work to \`kibi-usage\`, freshness decisions to \`kibi-freshness\`, source proof to \`kibi-traceability\`, and an explicit bootstrap request to \`kibi-bootstrap\`. Use the host-visible approved Kibi peer surface; if it is unavailable, report the blocked capability. Never read or edit \`.kb/\` directly.
484
469
 
485
- Before changing behavior: use kb_search for discovery, then kb_query by sourceFile, id, type, or tags for exact follow-up; do not rely on undocumented tools.
486
-
487
- Keep changed symbols traceable: production code uses \`implements\` (symbol→req) for ownership; test code uses \`executable_for\`; \`covered_by\` is coverage evidence only. Inline \`// implements REQ-xxx\` comments remain backward-compatible.
488
-
489
- Kibi capability selection (MCP tools and the trusted project-local CLI are peer surfaces over the same 18 operations):
490
- 1. If Kibi MCP tools are visible and approved, use MCP.
491
- 2. Otherwise, in a trusted workspace, use the project-local CLI's dedicated JSON routes with \`--input <file|->\`.
492
- 3. If neither interface is available, Kibi operation is blocked; tell the operator to enable MCP or the trusted project-local CLI.
493
- Do not infer MCP availability from config file existence. Do not read or edit \`.kb/\` files directly. Query before mutate. Run \`kb_upsert\` sequentially. Run \`kb_check\` before completion.
470
+ Keep changed symbols traceable: production code uses \`implements\` (symbol→req) for ownership; test code uses \`executable_for\`; \`covered_by\` is coverage evidence only.
494
471
 
495
472
  Dogfood note for this repo: OpenCode here uses local built \`kibi-mcp\` and \`kibi-opencode\` artifacts. If you change package versions or local package wiring, run \`bun run build\` before relying on OpenCode in this workspace.
496
473
 
497
- **Kibi-first workflow:**
498
- 1. **Discover**: Run kb_search for REQ, ADR, TEST, FACT. Decompose broad queries (e.g., "Apple Sign-In").
499
- 2. **Confirm**: Run kb_query with sourceFile, id, type, or tags once you know the exact follow-up target.
500
- 3. **Inspect freshness**: Run kb_status when branch or stale-state confidence matters.
501
- 4. **Document intent**: If you are about to explain code, STOP. Route that explanation to kb_upsert instead of inline comments.
502
- 5. **Link during work**: When creating KB entities, include relationship rows: specified_by (req→scenario), implements (symbol→req for ownership), covered_by (symbol→test for coverage), executable_for (test code→test).
503
- 6. **Validate**: Run kb_check after KB mutations to catch violations early.
504
- 7. **Before completion/commit**: Kibi impact evidence is required before completion/commit. If extraction output changes, refresh documentation/symbols.yaml and do not revert that update as scope creep.
505
-
506
- MCP and CLI expose the same 18 Kibi operations. Use the exact MCP tool or dedicated CLI route; do not invent a generic runner.
507
-
508
- For comprehensive Kibi usage guidance (relationships, fact lanes, workflows), use \`kb_skills_load\` with skill id \`kibi-usage\`.
509
-
510
- ${buildInitKibiBootstrapReference(capability)}`;
474
+ ${buildKibiBootstrapReference(capability)}`;
511
475
  }
512
476
  /**
513
477
  * Build prompt with contextual guidance based on posture, risk class, and cache state.
514
478
  */
515
- export function buildPrompt(context, capability = getInitKibiCommandCapability()) {
479
+ export function buildPrompt(context, capability = getKibiBootstrapCommandCapability()) {
516
480
  if (!context) {
517
481
  return buildBaseGuidance(capability).trim();
518
482
  }
@@ -521,7 +485,7 @@ export function buildPrompt(context, capability = getInitKibiCommandCapability()
521
485
  /**
522
486
  * Inject prompt guidance if not already present.
523
487
  */
524
- export function injectPrompt(current, config, context, capability = getInitKibiCommandCapability()) {
488
+ export function injectPrompt(current, config, context, capability = getKibiBootstrapCommandCapability()) {
525
489
  if (!config.prompt.enabled || !isPluginEnabled(config)) {
526
490
  return current;
527
491
  }
@@ -1,16 +1,16 @@
1
1
  // implements REQ-opencode-smart-enforcement-v1, REQ-opencode-kibi-plugin-v1
2
2
  import { existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
- // Default sync paths — must stay in sync with file-filter.ts DEFAULT_SYNC_PATHS
5
- const DEFAULT_SYNC_PATHS = {
6
- requirements: "documentation/requirements/**/*.md",
7
- scenarios: "documentation/scenarios/**/*.md",
8
- tests: "documentation/tests/**/*.md",
9
- adr: "documentation/adr/**/*.md",
10
- flags: "documentation/flags/**/*.md",
11
- events: "documentation/events/**/*.md",
12
- facts: "documentation/facts/**/*.md",
13
- symbols: "documentation/symbols.yaml",
4
+ // Canonical Kibi knowledge targets — must stay in sync with kb-paths.ts.
5
+ const CANONICAL_KB_TARGETS = {
6
+ requirements: ".kb/requirements",
7
+ scenarios: ".kb/scenarios",
8
+ tests: ".kb/tests",
9
+ adr: ".kb/adr",
10
+ flags: ".kb/flags",
11
+ events: ".kb/events",
12
+ facts: ".kb/facts",
13
+ symbols: ".kb/symbols.yaml",
14
14
  };
15
15
  // ── helpers ────────────────────────────────────────────────────────────
16
16
  function findVendoredTrees(cwd) {
@@ -19,7 +19,7 @@ function findVendoredTrees(cwd) {
19
19
  ["kibi", "opencode.json"],
20
20
  ["kibi", "package.json"],
21
21
  ["kibi", "packages", "mcp"],
22
- ["kibi", "documentation"],
22
+ ["kibi", ".kb"],
23
23
  ];
24
24
  for (const marker of vendoredMarkers) {
25
25
  const markerPath = join(cwd, ...marker);
@@ -37,63 +37,22 @@ function findVendoredTrees(cwd) {
37
37
  }
38
38
  return [...new Set(results)];
39
39
  }
40
- /** Check if .kb/config.json exists at root. */
41
- function rootKbConfigExists(cwd) {
42
- return existsSync(join(cwd, ".kb", "config.json"));
40
+ /** Check if `.kb/manifest.json` exists at root. */
41
+ function rootKbManifestExists(cwd) {
42
+ return existsSync(join(cwd, ".kb", "manifest.json"));
43
43
  }
44
- /** Read and parse .kb/config.json. Returns null on failure. */
45
- function readRootConfig(cwd) {
46
- try {
47
- return (JSON.parse(readFileSync(join(cwd, ".kb", "config.json"), "utf8")) || null);
48
- }
49
- catch {
50
- return null;
51
- }
52
- }
53
- /** Check if all configured KB targets resolve (directories / files exist). */
44
+ /** Check if all canonical KB targets resolve (directories / files exist). */
54
45
  function rootTargetsAllResolve(cwd) {
55
- const configPath = join(cwd, ".kb", "config.json");
56
- let config = {};
57
- try {
58
- config = JSON.parse(readFileSync(configPath, "utf8")) || {};
59
- }
60
- catch {
61
- return false;
62
- }
63
- const paths = config.paths;
64
- const defaultKeys = [
65
- "requirements",
66
- "scenarios",
67
- "tests",
68
- "adr",
69
- "flags",
70
- "events",
71
- "facts",
72
- "symbols",
73
- ];
74
- for (const key of defaultKeys) {
75
- const raw = paths?.[key] ?? DEFAULT_SYNC_PATHS[key];
46
+ for (const raw of Object.values(CANONICAL_KB_TARGETS)) {
76
47
  if (!raw)
77
48
  return false;
78
- // Normalize: strip trailing slashes and glob patterns to get the root dir/file path
79
- const normalized = raw.replace(/\/+$/, "");
80
- const isFile = normalized.endsWith(".yaml") || normalized.endsWith(".yml");
49
+ const isFile = raw.endsWith(".yaml") || raw.endsWith(".yml");
81
50
  if (isFile) {
82
- if (!existsSync(resolve(cwd, normalized)))
51
+ if (!existsSync(resolve(cwd, raw)))
83
52
  return false;
84
53
  }
85
- else {
86
- // Strip first glob segment
87
- const segments = normalized.split("/");
88
- const rootSegments = [];
89
- for (const seg of segments) {
90
- if (seg.includes("*") || seg.includes("?") || seg.includes("["))
91
- break;
92
- rootSegments.push(seg);
93
- }
94
- const dirPath = rootSegments.join("/") || ".";
95
- if (!existsSync(resolve(cwd, dirPath)))
96
- return false;
54
+ else if (!existsSync(resolve(cwd, raw))) {
55
+ return false;
97
56
  }
98
57
  }
99
58
  return true;
@@ -134,54 +93,42 @@ function hasRootKibiIntent(cwd) {
134
93
  }
135
94
  // ── main classifier ────────────────────────────────────────────────────
136
95
  export function detectPosture(cwd) {
137
- const hasRootConfig = rootKbConfigExists(cwd);
96
+ const hasRootManifest = rootKbManifestExists(cwd);
138
97
  const vendoredTrees = findVendoredTrees(cwd);
139
- // 1. hybrid_root_plus_vendored — root config AND vendored subtrees
140
- if (hasRootConfig && vendoredTrees.length > 0) {
98
+ // 1. hybrid_root_plus_vendored — root manifest AND vendored subtrees
99
+ if (hasRootManifest && vendoredTrees.length > 0) {
141
100
  return {
142
101
  state: "hybrid_root_plus_vendored",
143
102
  needsBootstrap: false,
144
- reason: `Root .kb/config.json exists alongside vendored tree(s): ${vendoredTrees.join(", ")}`,
103
+ reason: `Root .kb/manifest.json exists alongside vendored tree(s): ${vendoredTrees.join(", ")}`,
145
104
  maintenanceDegraded: false,
146
105
  };
147
106
  }
148
- // 2-3. root config exists (check maintenance mode and target resolution)
149
- if (hasRootConfig) {
150
- const config = readRootConfig(cwd);
151
- const maint = config?.maintenance;
152
- const maintenanceDisabled = maint !== undefined && maint.enabled === false;
153
- // maintenance_degraded overlay: maintenance explicitly disabled → root_active
154
- if (maintenanceDisabled) {
155
- return {
156
- state: "root_active",
157
- needsBootstrap: false,
158
- reason: "Root .kb/config.json exists; maintenance mode explicitly disabled",
159
- maintenanceDegraded: true,
160
- };
161
- }
107
+ // 2-3. root manifest exists (check target resolution)
108
+ if (hasRootManifest) {
162
109
  const allResolve = rootTargetsAllResolve(cwd);
163
110
  if (allResolve) {
164
111
  return {
165
112
  state: "root_active",
166
113
  needsBootstrap: false,
167
- reason: "Root .kb/config.json exists and all configured KB targets resolve",
114
+ reason: "Root .kb/manifest.json exists and all canonical KB targets resolve",
168
115
  maintenanceDegraded: false,
169
116
  };
170
117
  }
171
- // 3. root_partial — root config exists but targets are missing
118
+ // 3. root_partial — manifest exists but targets are missing
172
119
  return {
173
120
  state: "root_partial",
174
121
  needsBootstrap: true,
175
- reason: "Root .kb/config.json exists but some configured KB targets are missing",
122
+ reason: "Root .kb/manifest.json exists but some canonical KB targets are missing",
176
123
  maintenanceDegraded: false,
177
124
  };
178
125
  }
179
- // 4. vendored_only — no root config, but vendored markers found
126
+ // 4. vendored_only — no root manifest, but vendored markers found
180
127
  if (vendoredTrees.length > 0) {
181
128
  return {
182
129
  state: "vendored_only",
183
130
  needsBootstrap: false,
184
- reason: `No root .kb/config.json, but vendored Kibi tree(s) detected: ${vendoredTrees.join(", ")}`,
131
+ reason: `No root .kb/manifest.json, but vendored Kibi tree(s) detected: ${vendoredTrees.join(", ")}`,
185
132
  maintenanceDegraded: false,
186
133
  };
187
134
  }
@@ -27,10 +27,10 @@ export interface RiskClassification {
27
27
  * Classify a file edit into a deterministic risk class.
28
28
  *
29
29
  * Classification order (first match wins):
30
- * 1. manual_kb_edit — file is under .kb/
30
+ * 1. manual_kb_edit — opaque/derived `.kb/` files (pathKind "kb")
31
31
  * 2. safe_test_only — pathKind is "test"
32
- * 3. req_policy_candidate — pathKind is "requirement"
33
- * 4. kb_doc_structural — pathKind is "scenario", "adr", or "fact"
32
+ * 3. req_policy_candidate — pathKind is "requirement" (including `.kb/requirements`)
33
+ * 4. kb_doc_structural — pathKind is "scenario", "adr", "fact", "flag", "event", or "symbol"
34
34
  * 5. safe_docs_only — pathKind is "unknown" (markdown/config/etc.)
35
35
  * 6. traceability_candidate — code with exports AND (hasDurableComment OR missing traceability)
36
36
  * 7. behavior_candidate — code with exports, already has traceability, no durable comment
@@ -11,10 +11,10 @@ const TRACEABILITY_PATTERN = /implements\s+REQ-[A-Za-z0-9_-]+/i;
11
11
  * Classify a file edit into a deterministic risk class.
12
12
  *
13
13
  * Classification order (first match wins):
14
- * 1. manual_kb_edit — file is under .kb/
14
+ * 1. manual_kb_edit — opaque/derived `.kb/` files (pathKind "kb")
15
15
  * 2. safe_test_only — pathKind is "test"
16
- * 3. req_policy_candidate — pathKind is "requirement"
17
- * 4. kb_doc_structural — pathKind is "scenario", "adr", or "fact"
16
+ * 3. req_policy_candidate — pathKind is "requirement" (including `.kb/requirements`)
17
+ * 4. kb_doc_structural — pathKind is "scenario", "adr", "fact", "flag", "event", or "symbol"
18
18
  * 5. safe_docs_only — pathKind is "unknown" (markdown/config/etc.)
19
19
  * 6. traceability_candidate — code with exports AND (hasDurableComment OR missing traceability)
20
20
  * 7. behavior_candidate — code with exports, already has traceability, no durable comment
@@ -22,11 +22,14 @@ const TRACEABILITY_PATTERN = /implements\s+REQ-[A-Za-z0-9_-]+/i;
22
22
  */
23
23
  export function classifyRisk(params) {
24
24
  const { pathKind, isUnderKb, hasMustPriority, hasDurableComment, fileContent, } = params;
25
- // 1. manual_kb_edit — direct KB manipulation is always risky
26
- if (isUnderKb) {
25
+ // 1. manual_kb_edit — derived/runtime `.kb/` files, not canonical lanes.
26
+ // Entity lanes under `.kb/` keep their typed pathKind (requirement, symbol, …).
27
+ if (isUnderKb && pathKind === "kb") {
27
28
  return {
28
29
  riskClass: "manual_kb_edit",
29
- reasons: ["File is under .kb/ — manual edits bypass KB validation"],
30
+ reasons: [
31
+ "File is opaque or derived `.kb/` runtime state — manual edits bypass KB validation",
32
+ ],
30
33
  };
31
34
  }
32
35
  // 2. safe_test_only — test files are low-risk
package/dist/scheduler.js CHANGED
@@ -256,7 +256,7 @@ async function runKibiSync(worktree) {
256
256
  ? `${error.message}${signal}`
257
257
  : signal || undefined;
258
258
  resolve({
259
- exitCode: error.code ?? 1,
259
+ exitCode: typeof error.code === "number" ? error.code : 1,
260
260
  syncCommand: "kibi sync",
261
261
  ...(truncatedOut !== undefined ? { syncStdout: truncatedOut } : {}),
262
262
  ...(truncatedErr !== undefined ? { syncStderr: truncatedErr } : {}),
@@ -285,7 +285,7 @@ async function runKibiCheck(worktree, rules) {
285
285
  const truncatedOut = truncateSyncOutput(stdout || undefined);
286
286
  const truncatedErr = truncateSyncOutput(stderr || undefined);
287
287
  resolve({
288
- exitCode: error ? (error.code ?? 1) : 0,
288
+ exitCode: error && typeof error.code === "number" ? error.code : 0,
289
289
  ...(truncatedOut !== undefined ? { stdout: truncatedOut } : {}),
290
290
  ...(truncatedErr !== undefined ? { stderr: truncatedErr } : {}),
291
291
  });
@@ -1,4 +1,8 @@
1
1
  export type EditEventKind = string;
2
+ export declare function pushEventHintIfKind(hints: Array<{
3
+ kind: string;
4
+ timestamp: number;
5
+ }>, kind: string | undefined, timestamp: number): void;
2
6
  export interface SessionEditEntry {
3
7
  /** Relative file path (relative to worktree root). */
4
8
  filePath: string;
@@ -1,6 +1,10 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ export function pushEventHintIfKind(hints, kind, timestamp) {
5
+ if (kind)
6
+ hints.push({ kind, timestamp });
7
+ }
4
8
  // ---------------------------------------------------------------------------
5
9
  // Constants
6
10
  // ---------------------------------------------------------------------------
@@ -162,9 +166,7 @@ export function createSessionEditState(opts) {
162
166
  const abs = resolveToAbsolute(rel);
163
167
  entry.currentHash = hashFile(abs);
164
168
  entry.lastReconciledAt = timestamp ?? now();
165
- if (kind) {
166
- entry.eventHints.push({ kind, timestamp: timestamp ?? now() });
167
- }
169
+ pushEventHintIfKind(entry.eventHints, kind, timestamp ?? now());
168
170
  }
169
171
  return {
170
172
  recordEventHint,
@@ -0,0 +1,100 @@
1
+ import type { CommentAnalysisResult } from "./comment-analysis.js";
2
+ import type { FileOperationState } from "./file-operation-state.js";
3
+ import type { CacheKey } from "./guidance-cache.js";
4
+ import type { KbFreshnessScope } from "./kb-freshness-state.js";
5
+ import type { PathKind } from "./path-kind.js";
6
+ import type { RuntimeDegradedOverlay } from "./plugin-startup.js";
7
+ import type { RiskClass } from "./risk-classifier.js";
8
+ import type { SyncScheduler } from "./scheduler.js";
9
+ import type { SessionEditState } from "./session-edit-state.js";
10
+ import type { WarningCategory } from "./session-tracker.js";
11
+ import type { WorkContext } from "./work-context-resolver.js";
12
+ export interface PluginEventInput {
13
+ sessionId?: string;
14
+ agentIdentity?: string;
15
+ worktree: string;
16
+ directory: string;
17
+ }
18
+ export interface PluginEvent {
19
+ type: string;
20
+ properties?: Record<string, unknown>;
21
+ }
22
+ /** Mutable cross-event state shared by the event and chat hooks. */
23
+ export interface SmartEnforcementState {
24
+ recentEdits: Array<{
25
+ path: string;
26
+ kind: PathKind;
27
+ timestamp: number;
28
+ }>;
29
+ hasRecentKbEdit: boolean;
30
+ recentCommentSuggestion: CommentAnalysisResult | null;
31
+ seenFingerprints: Set<string>;
32
+ lastRiskClass: RiskClass | null;
33
+ lastRiskFilePath: string | null;
34
+ lastRiskScopeKey: string | null;
35
+ }
36
+ export interface KibiEventEnv {
37
+ input: PluginEventInput;
38
+ cfg: {
39
+ sync: {
40
+ enabled: boolean;
41
+ };
42
+ guidance: {
43
+ warnOnKbEdits: boolean;
44
+ commentDetection: {
45
+ enabled: boolean;
46
+ minLines: number;
47
+ };
48
+ targetedChecks: {
49
+ enabled: boolean;
50
+ };
51
+ smartEnforcement: {
52
+ degradedMode: string;
53
+ };
54
+ };
55
+ };
56
+ cache: {
57
+ isSatisfied(key: CacheKey): boolean;
58
+ };
59
+ freshnessStore: {
60
+ recordToolEvidence(scope: KbFreshnessScope, tool: string): void;
61
+ };
62
+ fileFilter: {
63
+ shouldHandleFile(filePath: string, root: string): boolean;
64
+ };
65
+ log: {
66
+ info(...args: unknown[]): void;
67
+ warn(...args: unknown[]): void;
68
+ };
69
+ rootWorkContext: WorkContext;
70
+ state: SmartEnforcementState;
71
+ resolveScopedWorkContext(filePath?: string): WorkContext;
72
+ getSessionEditState(context: WorkContext): SessionEditState;
73
+ getFileOperationState(context: WorkContext): FileOperationState;
74
+ getPathKindCache(context: WorkContext): Map<string, PathKind>;
75
+ getSchedulerForContext(context: WorkContext): SyncScheduler | null;
76
+ normalizeSessionPath(filePath: string, worktree?: string): string;
77
+ resolveWorktreePath(filePath: string, worktree?: string): string;
78
+ buildRiskPathScopeKey(context: WorkContext, filePath: string): string;
79
+ buildScopedCacheKey(context: WorkContext, riskClass: RiskClass, fileBucket: string, dirtyRelevantInputs: Iterable<string | null | undefined>): CacheKey;
80
+ readFileContent(filePath: string, worktree?: string): string;
81
+ updateRecentEditsFromSession(sessionEdits: Array<{
82
+ filePath: string;
83
+ lastReconciledAt: number;
84
+ }>, scopedPathKindCache: Map<string, PathKind>): SmartEnforcementState["recentEdits"];
85
+ recordWarning(category: WarningCategory | string, filePath: string, message: string): void;
86
+ getMaintenanceDegraded(): boolean;
87
+ getEffectiveMode(): string;
88
+ posture: {
89
+ maintenanceDegraded: boolean;
90
+ state: string;
91
+ };
92
+ runtimeOverlay: RuntimeDegradedOverlay;
93
+ lintRequirementDoc(filePath: string, worktreeRoot: string): Array<{
94
+ category: string;
95
+ message: string;
96
+ }>;
97
+ }
98
+ export declare function isKbToolEventType(eventType: string): boolean;
99
+ export declare function handleKbToolEvent(env: KibiEventEnv, event: PluginEvent, sessionId: string | undefined): void;
100
+ export declare function handleFileLifecycleEvent(env: KibiEventEnv, event: PluginEvent): void;