kibi-opencode 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/prompt.d.ts CHANGED
@@ -47,6 +47,12 @@ export interface PromptContext {
47
47
  lifecycleReminder: string | null;
48
48
  e2eReminder: string | null;
49
49
  };
50
+ /** Hard-mode checkpoint block that must take prompt priority over advisory guidance. */
51
+ hardGateBlock?: {
52
+ shownPaths: string[];
53
+ remainingCount: number;
54
+ reason?: string;
55
+ };
50
56
  }
51
57
  export declare function postureGuidance(posture: RepoPosture, capability?: InitKibiCommandCapability): string | null;
52
58
  /**
package/dist/prompt.js CHANGED
@@ -112,6 +112,28 @@ function buildBootstrapRequiredBody(capability = getInitKibiCommandCapability())
112
112
  : "- 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.";
113
113
  return `This 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 user/operator to run setup or repair outside this session if bootstrap is insufficient.\n\nUse public MCP tools only: \`kb_autopilot_generate\`, \`kb_search\`, \`kb_query\`, \`kb_status\`, \`kb_find_gaps\`, \`kb_coverage\`, \`kb_graph\`, \`kb_upsert\`, \`kb_delete\`, \`kb_check\`.`;
114
114
  }
115
+ function buildHardGateBlock(block) {
116
+ const pathLines = block.shownPaths.map((path) => `- \`${path}\``);
117
+ if (block.remainingCount > 0) {
118
+ pathLines.push(`- +${block.remainingCount} more dirty files`);
119
+ }
120
+ const reasonLine = block.reason ? `Reason: ${block.reason}.` : null;
121
+ return [
122
+ "🛑 Kibi hard gate blocked",
123
+ "STOP implementation until this authoritative Kibi checkpoint is satisfied.",
124
+ reasonLine,
125
+ "Affected files:",
126
+ ...pathLines,
127
+ "MCP-only recovery steps:",
128
+ "- Run `kb_search` for impacted requirements, tests, ADRs, and facts.",
129
+ "- Run `kb_query` with `sourceFile` for each affected file.",
130
+ "- Run `kb_status` if branch or snapshot freshness matters.",
131
+ "- Run `kb_upsert` for required traceability, relationship, or fact updates.",
132
+ "- Run `kb_check` before continuing.",
133
+ ]
134
+ .filter((line) => line !== null)
135
+ .join("\n");
136
+ }
115
137
  // ── Guidance blocks by risk class ──────────────────────────────────────
116
138
  const GUIDANCE_BY_RISK = {
117
139
  safe_docs_only: null,
@@ -171,6 +193,9 @@ Root .kb/config.json exists but some configured KB targets are missing. Guidance
171
193
  * Build prompt guidance block based on posture, risk class, and cache state.
172
194
  */
173
195
  function buildContextualGuidance(context, capability = getInitKibiCommandCapability()) {
196
+ if (context.hardGateBlock) {
197
+ return `${SENTINEL}\n\n${buildHardGateBlock(context.hardGateBlock)}`;
198
+ }
174
199
  const posture = context.posture ?? "root_active";
175
200
  const riskClass = context.riskClass;
176
201
  const readyAutoBriefingAvailable = context.autoBriefResult?.showManualCue === false;
@@ -5,14 +5,16 @@ import type { RepoPosture } from "./repo-posture.js";
5
5
  * - "strict": plugin may escalate targeted checks, completion reminders, and
6
6
  * structured logging. Hooks/checks remain the hard enforcement boundary
7
7
  * regardless of mode.
8
+ * - "hard": authoritative root-KB postures may hard-block through durable
9
+ * hook/check boundaries, even when maintenance guidance is degraded.
8
10
  */
9
- export type EffectiveMode = "advisory" | "strict";
11
+ export type EffectiveMode = "advisory" | "strict" | "hard";
10
12
  /**
11
13
  * Inputs required to determine the effective smart-enforcement mode.
12
14
  */
13
15
  export interface ModeInputs {
14
16
  /** Configured smart-enforcement mode. */
15
- mode: "advisory" | "strict";
17
+ mode: "advisory" | "strict" | "hard";
16
18
  /** When true, strict mode only activates for authoritative root KB postures. */
17
19
  requireRootKbForStrict: boolean;
18
20
  /** Current repository posture from detectPosture(). */
@@ -37,5 +39,7 @@ export declare function isStrictEligible(inputs: ModeInputs): boolean;
37
39
  * - strict config + requireRootKbForStrict=false → strict may apply to all
38
40
  * postures (but hooks/checks remain hard gate regardless)
39
41
  * - maintenance-degraded → advisory regardless of config
42
+ * - hard config → hard only for authoritative root-KB postures, even when
43
+ * maintenance is degraded; non-authoritative postures stay advisory
40
44
  */
41
45
  export declare function computeEffectiveMode(inputs: ModeInputs): EffectiveMode;
@@ -30,9 +30,15 @@ export function isStrictEligible(inputs) {
30
30
  * - strict config + requireRootKbForStrict=false → strict may apply to all
31
31
  * postures (but hooks/checks remain hard gate regardless)
32
32
  * - maintenance-degraded → advisory regardless of config
33
+ * - hard config → hard only for authoritative root-KB postures, even when
34
+ * maintenance is degraded; non-authoritative postures stay advisory
33
35
  */
36
+ // implements REQ-opencode-worktree-hard-enforcement-v1
34
37
  export function computeEffectiveMode(inputs) {
35
- // implements REQ-opencode-smart-enforcement-v1
38
+ // implements REQ-opencode-smart-enforcement-v1, REQ-opencode-worktree-hard-enforcement-v1
39
+ if (inputs.mode === "hard") {
40
+ return STRICT_ELIGIBLE_POSTURES.has(inputs.posture) ? "hard" : "advisory";
41
+ }
36
42
  // Maintenance-degraded always forces advisory
37
43
  if (inputs.maintenanceDegraded) {
38
44
  return "advisory";
@@ -0,0 +1,19 @@
1
+ export interface PendingBriefMarkerIssue {
2
+ filePath: string;
3
+ reason: "parse" | "schema" | "delete";
4
+ }
5
+ export interface PendingBriefMarkersResult {
6
+ entityIds: string[];
7
+ relationships: Array<{
8
+ from: string;
9
+ to: string;
10
+ type: string;
11
+ }>;
12
+ markerPaths: string[];
13
+ issues: PendingBriefMarkerIssue[];
14
+ }
15
+ export declare function loadPendingBriefMarkers(workspaceRoot: string, branch: string): PendingBriefMarkersResult;
16
+ export declare function deletePendingBriefMarkers(markerPaths: string[]): Promise<{
17
+ deletedCount: number;
18
+ issues: PendingBriefMarkerIssue[];
19
+ }>;
@@ -0,0 +1,101 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ // implements REQ-opencode-kibi-briefing-v2
4
+ export function loadPendingBriefMarkers(workspaceRoot, branch) {
5
+ const pendingDir = path.join(workspaceRoot, ".kb", "briefs", "pending");
6
+ if (!fs.existsSync(pendingDir)) {
7
+ return { entityIds: [], relationships: [], markerPaths: [], issues: [] };
8
+ }
9
+ let entries;
10
+ try {
11
+ entries = fs.readdirSync(pendingDir, { withFileTypes: true });
12
+ }
13
+ catch {
14
+ return { entityIds: [], relationships: [], markerPaths: [], issues: [] };
15
+ }
16
+ const issues = [];
17
+ const markerPaths = [];
18
+ const entityIds = [];
19
+ const seenEntityIds = new Set();
20
+ const relationshipMap = new Map();
21
+ for (const entry of entries
22
+ .filter((dirent) => dirent.isFile() && dirent.name.endsWith(".json"))
23
+ .sort((a, b) => a.name.localeCompare(b.name))) {
24
+ const filePath = path.join(pendingDir, entry.name);
25
+ let payload;
26
+ try {
27
+ payload = parsePendingBriefMarker(JSON.parse(fs.readFileSync(filePath, "utf8")));
28
+ }
29
+ catch (error) {
30
+ issues.push({
31
+ filePath,
32
+ reason: error instanceof SyntaxError ? "parse" : "schema",
33
+ });
34
+ continue;
35
+ }
36
+ if (payload.branch !== branch) {
37
+ continue;
38
+ }
39
+ markerPaths.push(filePath);
40
+ for (const entityId of payload.entityIds) {
41
+ if (seenEntityIds.has(entityId)) {
42
+ continue;
43
+ }
44
+ seenEntityIds.add(entityId);
45
+ entityIds.push(entityId);
46
+ }
47
+ for (const relationship of payload.relationships) {
48
+ relationshipMap.set(`${relationship.type}\u0000${relationship.from}\u0000${relationship.to}`, relationship);
49
+ }
50
+ }
51
+ return {
52
+ entityIds,
53
+ relationships: [...relationshipMap.values()],
54
+ markerPaths,
55
+ issues,
56
+ };
57
+ }
58
+ // implements REQ-opencode-kibi-briefing-v2
59
+ export async function deletePendingBriefMarkers(markerPaths) {
60
+ let deletedCount = 0;
61
+ const issues = [];
62
+ for (const markerPath of markerPaths) {
63
+ try {
64
+ const existedBeforeDelete = fs.existsSync(markerPath);
65
+ await fs.promises.rm(markerPath, { force: true });
66
+ if (existedBeforeDelete && !fs.existsSync(markerPath)) {
67
+ deletedCount += 1;
68
+ }
69
+ }
70
+ catch {
71
+ issues.push({ filePath: markerPath, reason: "delete" });
72
+ }
73
+ }
74
+ return { deletedCount, issues };
75
+ }
76
+ function parsePendingBriefMarker(value) {
77
+ if (!value || typeof value !== "object") {
78
+ throw new Error("Invalid marker payload");
79
+ }
80
+ const record = value;
81
+ const branch = typeof record.branch === "string" ? record.branch.trim() : "";
82
+ const entityIds = Array.isArray(record.entityIds)
83
+ ? record.entityIds.filter((item) => typeof item === "string" && item.length > 0)
84
+ : null;
85
+ const relationships = Array.isArray(record.relationships)
86
+ ? record.relationships
87
+ .filter((item) => !!item &&
88
+ typeof item === "object" &&
89
+ typeof item.from === "string" &&
90
+ typeof item.to === "string" &&
91
+ typeof item.type === "string" &&
92
+ item.from.length > 0 &&
93
+ item.to.length > 0 &&
94
+ item.type.length > 0)
95
+ .map((item) => ({ from: item.from, to: item.to, type: item.type }))
96
+ : [];
97
+ if (!branch || !entityIds) {
98
+ throw new Error("Invalid marker schema");
99
+ }
100
+ return { branch, entityIds, relationships };
101
+ }
@@ -0,0 +1,21 @@
1
+ import { type RepoPosture } from "./repo-posture.js";
2
+ export interface ResolveWorkContextInput {
3
+ inputDirectory: string;
4
+ inputWorktree: string;
5
+ filePath?: string;
6
+ sessionId?: string;
7
+ agentIdentity?: string;
8
+ }
9
+ export interface WorkContext {
10
+ worktreeRoot: string;
11
+ kibiAuthorityRoot: string;
12
+ branch: string;
13
+ repoRelativePath: string;
14
+ posture: RepoPosture;
15
+ isAuthoritative: boolean;
16
+ isLinkedWorktree: boolean;
17
+ sessionId: string | undefined;
18
+ agentIdentity: string;
19
+ }
20
+ declare const resolveWorkContext: (input: ResolveWorkContextInput) => WorkContext;
21
+ export { resolveWorkContext };
@@ -0,0 +1,197 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
3
+ import { detectPosture } from "./repo-posture.js";
4
+ function safeStat(path) {
5
+ try {
6
+ return statSync(path);
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ function readFirstLine(path) {
13
+ try {
14
+ const [firstLine] = readFileSync(path, "utf8").split(/\r?\n/, 1);
15
+ const trimmed = firstLine?.trim();
16
+ return trimmed && trimmed.length > 0 ? trimmed : null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function readLinkedGitDir(worktreeRoot, gitFilePath) {
23
+ const firstLine = readFirstLine(gitFilePath);
24
+ if (!firstLine?.startsWith("gitdir:")) {
25
+ return null;
26
+ }
27
+ const rawGitDir = firstLine.slice("gitdir:".length).trim();
28
+ if (rawGitDir.length === 0) {
29
+ return null;
30
+ }
31
+ return isAbsolute(rawGitDir)
32
+ ? resolve(rawGitDir)
33
+ : resolve(worktreeRoot, rawGitDir);
34
+ }
35
+ function readCommonGitDir(gitDir) {
36
+ const rawCommonDir = readFirstLine(join(gitDir, "commondir"));
37
+ if (!rawCommonDir) {
38
+ return gitDir;
39
+ }
40
+ return isAbsolute(rawCommonDir)
41
+ ? resolve(rawCommonDir)
42
+ : resolve(gitDir, rawCommonDir);
43
+ }
44
+ function directorySearchStart(candidatePath) {
45
+ const resolved = resolve(candidatePath);
46
+ const stats = safeStat(resolved);
47
+ return stats?.isDirectory() ? resolved : dirname(resolved);
48
+ }
49
+ function findGitMetadata(candidatePath) {
50
+ let current = directorySearchStart(candidatePath);
51
+ while (true) {
52
+ const dotGitPath = join(current, ".git");
53
+ const dotGitStats = safeStat(dotGitPath);
54
+ if (dotGitStats?.isFile()) {
55
+ const gitDir = readLinkedGitDir(current, dotGitPath);
56
+ if (gitDir) {
57
+ return {
58
+ worktreeRoot: current,
59
+ gitDir,
60
+ commonGitDir: readCommonGitDir(gitDir),
61
+ isLinkedWorktree: true,
62
+ };
63
+ }
64
+ }
65
+ if (dotGitStats?.isDirectory()) {
66
+ return {
67
+ worktreeRoot: current,
68
+ gitDir: dotGitPath,
69
+ commonGitDir: dotGitPath,
70
+ isLinkedWorktree: false,
71
+ };
72
+ }
73
+ const parent = dirname(current);
74
+ if (parent === current) {
75
+ return null;
76
+ }
77
+ current = parent;
78
+ }
79
+ }
80
+ function hasRootKbConfig(root) {
81
+ return existsSync(join(root, ".kb", "config.json"));
82
+ }
83
+ function uniqueResolved(paths) {
84
+ const seen = new Set();
85
+ const result = [];
86
+ for (const path of paths) {
87
+ if (!path) {
88
+ continue;
89
+ }
90
+ const resolved = resolve(path);
91
+ if (seen.has(resolved)) {
92
+ continue;
93
+ }
94
+ seen.add(resolved);
95
+ result.push(resolved);
96
+ }
97
+ return result;
98
+ }
99
+ function authorityRootFromCommonGitDir(commonGitDir) {
100
+ return basename(commonGitDir) === ".git" ? dirname(commonGitDir) : null;
101
+ }
102
+ function authorityRootFromLinkedGitDir(gitDir) {
103
+ const normalizedGitDir = resolve(gitDir);
104
+ const marker = `${sep}.git${sep}worktrees${sep}`;
105
+ const markerIndex = normalizedGitDir.indexOf(marker);
106
+ if (markerIndex < 0) {
107
+ return null;
108
+ }
109
+ return normalizedGitDir.slice(0, markerIndex);
110
+ }
111
+ function ancestorKbRoots(start) {
112
+ const roots = [];
113
+ let current = resolve(start);
114
+ while (true) {
115
+ if (hasRootKbConfig(current)) {
116
+ roots.push(current);
117
+ }
118
+ const parent = dirname(current);
119
+ if (parent === current) {
120
+ return roots;
121
+ }
122
+ current = parent;
123
+ }
124
+ }
125
+ function resolveAuthorityRoot(git, inputDirectory) {
126
+ if (!git.isLinkedWorktree) {
127
+ return git.worktreeRoot;
128
+ }
129
+ const candidates = uniqueResolved([
130
+ authorityRootFromCommonGitDir(git.commonGitDir),
131
+ authorityRootFromLinkedGitDir(git.gitDir),
132
+ ...ancestorKbRoots(git.worktreeRoot),
133
+ inputDirectory,
134
+ git.worktreeRoot,
135
+ ]);
136
+ return (candidates.find((candidate) => hasRootKbConfig(candidate)) ??
137
+ candidates[0] ??
138
+ git.worktreeRoot);
139
+ }
140
+ function resolveBranch(git) {
141
+ if (!git) {
142
+ return "unknown";
143
+ }
144
+ const head = readFirstLine(join(git.gitDir, "HEAD"));
145
+ if (!head) {
146
+ return "unknown";
147
+ }
148
+ if (!head.startsWith("ref:")) {
149
+ return "HEAD";
150
+ }
151
+ const ref = head.slice("ref:".length).trim();
152
+ if (!ref.startsWith("refs/heads/")) {
153
+ return "HEAD";
154
+ }
155
+ const branch = ref.slice("refs/heads/".length);
156
+ return branch === "master" ? "main" : branch;
157
+ }
158
+ function normalizeRepoRelativePath(fromRoot, targetPath) {
159
+ const rawRelativePath = relative(fromRoot, targetPath);
160
+ if (rawRelativePath.length === 0) {
161
+ return ".";
162
+ }
163
+ return rawRelativePath.split(sep).join("/");
164
+ }
165
+ function authoritativeForPosture(posture) {
166
+ return posture === "root_active" || posture === "hybrid_root_plus_vendored";
167
+ }
168
+ // implements REQ-opencode-worktree-hard-enforcement-v1
169
+ const resolveWorkContext = function resolveWorkContext(input) {
170
+ const declaredWorktreeRoot = resolve(input.inputWorktree || input.inputDirectory);
171
+ const declaredDirectory = resolve(input.inputDirectory || input.inputWorktree);
172
+ const absoluteFilePath = input.filePath
173
+ ? isAbsolute(input.filePath)
174
+ ? resolve(input.filePath)
175
+ : resolve(declaredWorktreeRoot, input.filePath)
176
+ : undefined;
177
+ const git = (absoluteFilePath ? findGitMetadata(absoluteFilePath) : null) ??
178
+ findGitMetadata(declaredWorktreeRoot);
179
+ const worktreeRoot = git?.worktreeRoot ?? declaredWorktreeRoot;
180
+ const kibiAuthorityRoot = git
181
+ ? resolveAuthorityRoot(git, declaredDirectory)
182
+ : declaredWorktreeRoot;
183
+ const posture = detectPosture(kibiAuthorityRoot).state;
184
+ const repoRelativePath = normalizeRepoRelativePath(worktreeRoot, absoluteFilePath ?? worktreeRoot);
185
+ return {
186
+ worktreeRoot,
187
+ kibiAuthorityRoot,
188
+ branch: resolveBranch(git),
189
+ repoRelativePath,
190
+ posture,
191
+ isAuthoritative: authoritativeForPosture(posture),
192
+ isLinkedWorktree: git?.isLinkedWorktree ?? false,
193
+ sessionId: input.sessionId,
194
+ agentIdentity: input.agentIdentity ?? "unknown",
195
+ };
196
+ };
197
+ export { resolveWorkContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-opencode",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "Kibi OpenCode plugin - thin adapter to integrate Kibi with OpenCode sessions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -62,7 +62,7 @@
62
62
  "@opencode-ai/plugin": "^1.4.7",
63
63
  "@opentui/core": "^0.1.99",
64
64
  "@opentui/solid": "^0.1.99",
65
- "kibi-cli": "^0.10.1"
65
+ "kibi-cli": "^0.11.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "latest",