kibi-opencode 0.13.0 → 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/README.md CHANGED
@@ -26,11 +26,11 @@ The plugin now uses a posture-aware, low-token smart-enforcement model before em
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
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
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.
29
- - **Effective mode gating**: `strict` is only possible for `root_active` and `hybrid_root_plus_vendored` when `requireRootKbForStrict` is enabled; `maintenanceDegraded` overrides everything back to `advisory`
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
32
32
  - **Runtime maintenance overlay**: static `maintenanceDegraded` from posture is merged with a latched runtime overlay (sync disabled/unavailable/failing) so degraded state is consistently reflected in prompts, logs, and mode decisions
33
- - **Advisory in editor, hard in hooks**: OpenCode guidance remains non-blocking; git hooks and KB validation checks stay the durable enforcement boundary
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
36
  ### Dynamic Contextual Guidance
@@ -176,7 +176,7 @@ PP|| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance
176
176
  | `guidance.sessionSummary.enabled` | boolean | `true` | Enable periodic session summary logs |
177
177
  | `guidance.sessionSummary.logIntervalMs` | number | `1800000` | Session summary interval (30 min) |
178
178
  | `guidance.smartEnforcement.enabled` | boolean | `true` | Enable posture-aware, risk-aware guidance routing |
179
- | `guidance.smartEnforcement.mode` | string | `"advisory"` | Smart-enforcement mode: `advisory` or `strict` |
179
+ | `guidance.smartEnforcement.mode` | string | `"advisory"` | Smart-enforcement mode: `advisory`, `strict`, or `hard` |
180
180
  | `guidance.smartEnforcement.preflightTtlMs` | number | `600000` | Guidance/cache TTL for repeated prompt suppression |
181
181
  | `guidance.smartEnforcement.idleResetMs` | number | `1800000` | Idle window before smart-enforcement state resets |
182
182
  | `guidance.smartEnforcement.degradedMode` | string | `"warn-once"` | Degraded-mode logging policy: `warn-once` or `structured-only` |
@@ -188,7 +188,7 @@ PP|| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance
188
188
 
189
189
  Per ADR-016, prompt text injection uses only `experimental.chat.system.transform`. The `chat.params` hook is reserved for model option enrichment (temperature, topP, etc.) and never carries prompt text.
190
190
 
191
- Smart enforcement keeps the plugin advisory-only: prompt injection can warn, explain, or remind, but it does not block the editor. Hard failures still belong to CLI hooks (`pre-commit`) and explicit check commands.
191
+ Smart enforcement is `advisory` by default: prompt injection can warn, explain, or remind without blocking the editor. Opt-in `strict` escalates validation reminders, and `hard` can block the prompt for authoritative roots and linked worktrees until the Kibi checkpoint passes. Hard failures outside the plugin surface still belong to CLI hooks (`pre-commit`) and explicit check commands.
192
192
 
193
193
  ### Logging Policy
194
194
 
package/dist/config.d.ts CHANGED
@@ -36,7 +36,7 @@ export interface KibiConfig {
36
36
  };
37
37
  smartEnforcement: {
38
38
  enabled: boolean;
39
- mode: "advisory" | "strict";
39
+ mode: "advisory" | "strict" | "hard";
40
40
  preflightTtlMs: number;
41
41
  idleResetMs: number;
42
42
  degradedMode: "warn-once" | "structured-only";
package/dist/config.js CHANGED
@@ -57,7 +57,7 @@ function readJsonIfExists(filePath) {
57
57
  return null;
58
58
  }
59
59
  }
60
- // implements REQ-opencode-kibi-plugin-v1
60
+ // implements REQ-opencode-kibi-plugin-v1, REQ-opencode-worktree-hard-enforcement-v1
61
61
  export function validateAndMerge(obj) {
62
62
  if (!obj || typeof obj !== "object") {
63
63
  logger.warn("Config is not an object, using defaults");
@@ -150,7 +150,7 @@ export function validateAndMerge(obj) {
150
150
  };
151
151
  if (typeof se.enabled === "boolean")
152
152
  out.guidance.smartEnforcement.enabled = se.enabled;
153
- if (se.mode === "advisory" || se.mode === "strict")
153
+ if (se.mode === "advisory" || se.mode === "strict" || se.mode === "hard")
154
154
  out.guidance.smartEnforcement.mode = se.mode;
155
155
  if (typeof se.preflightTtlMs === "number")
156
156
  out.guidance.smartEnforcement.preflightTtlMs = se.preflightTtlMs;
@@ -0,0 +1,71 @@
1
+ import type { E2eCoverageSignal } from "./e2e-coverage-signals.js";
2
+ import type { FileLifecycle, ReminderKind } from "./file-operation-state.js";
3
+ import type { PathKind } from "./path-kind.js";
4
+ import type { RepoPosture } from "./repo-posture.js";
5
+ import type { RiskClass } from "./risk-classifier.js";
6
+ import type { EffectiveMode } from "./smart-enforcement.js";
7
+ import type { WorkContext } from "./work-context-resolver.js";
8
+ export interface PolicyLinkedEntityResult {
9
+ ids: string[];
10
+ source: "symbols" | "doc-path" | "none";
11
+ }
12
+ export interface EnforcementLifecycleEvent {
13
+ normalizedPath: string;
14
+ lifecycle: FileLifecycle;
15
+ }
16
+ export type CheckpointEvidence = boolean | {
17
+ hasCheckpoint?: boolean;
18
+ kbSearch?: boolean;
19
+ sourceFileQuery?: boolean;
20
+ kbStatus?: boolean;
21
+ kbCheck?: boolean;
22
+ kbUpsert?: boolean;
23
+ };
24
+ export interface EnforcementPolicyInput {
25
+ /** Resolved work context for the current file/prompt cycle. */
26
+ resolvedContext?: WorkContext | undefined;
27
+ /** Alias accepted by callers/tests that already carry WorkContext as workContext. */
28
+ workContext?: WorkContext | undefined;
29
+ /** Effective enforcement mode after config/posture resolution. */
30
+ effectiveMode: EffectiveMode;
31
+ /** Coalesced lifecycle events for this prompt cycle. */
32
+ lifecycleEvents: EnforcementLifecycleEvent[];
33
+ /** Path kinds aligned by index with lifecycleEvents. */
34
+ pathKinds: PathKind[];
35
+ /** Linked entity lookups aligned by index with lifecycleEvents. */
36
+ linkedEntityResults?: PolicyLinkedEntityResult[] | undefined;
37
+ /** Convenience linked-ID-only input aligned by index with lifecycleEvents. */
38
+ linkedEntityIds?: string[][] | undefined;
39
+ /** E2e signals aligned by index with lifecycleEvents. */
40
+ e2eSignals?: E2eCoverageSignal[] | undefined;
41
+ /** Semantic risk for the current focus event. */
42
+ currentSemanticRisk?: RiskClass | undefined;
43
+ /** Whether a Kibi checkpoint already happened in this cycle. */
44
+ checkpointEvidence?: CheckpointEvidence | undefined;
45
+ /** Posture fallback when resolvedContext is unavailable. */
46
+ posture?: RepoPosture | undefined;
47
+ }
48
+ interface EnforcementPolicyResultBase {
49
+ affectedPaths: string[];
50
+ dirtyFileCount: number;
51
+ e2eReminder: string | null;
52
+ reminderKindsToMark: ReminderKind[];
53
+ }
54
+ export type EnforcementPolicyResult = (EnforcementPolicyResultBase & {
55
+ kind: "skip_non_authoritative";
56
+ reason: string;
57
+ text: null;
58
+ }) | (EnforcementPolicyResultBase & {
59
+ kind: "advisory_guidance";
60
+ text: string;
61
+ }) | (EnforcementPolicyResultBase & {
62
+ kind: "hard_block";
63
+ text: string;
64
+ shownPaths: string[];
65
+ remainingCount: number;
66
+ }) | (EnforcementPolicyResultBase & {
67
+ kind: "checkpoint_passed";
68
+ text: null;
69
+ });
70
+ export declare function computeEnforcementPolicy(input: EnforcementPolicyInput): EnforcementPolicyResult;
71
+ export {};
@@ -0,0 +1,269 @@
1
+ const NO_E2E_SIGNAL = {
2
+ level: "none",
3
+ evidence: [],
4
+ reminderText: null,
5
+ };
6
+ const DEFAULT_LINKED_ENTITY_RESULT = {
7
+ ids: [],
8
+ source: "none",
9
+ };
10
+ const HARD_BLOCK_PATH_LIMIT = 5;
11
+ const AUTHORITATIVE_POSTURES = new Set([
12
+ "root_active",
13
+ "hybrid_root_plus_vendored",
14
+ ]);
15
+ const ADVISORY_EDIT_KINDS = new Set([
16
+ "code",
17
+ "requirement",
18
+ "scenario",
19
+ "test",
20
+ "adr",
21
+ "fact",
22
+ "flag",
23
+ "event",
24
+ "symbol",
25
+ "kb",
26
+ ]);
27
+ function normalizeCheckpointEvidence(evidence) {
28
+ if (typeof evidence === "boolean") {
29
+ return evidence;
30
+ }
31
+ if (!evidence) {
32
+ return false;
33
+ }
34
+ return (evidence.hasCheckpoint === true ||
35
+ evidence.kbSearch === true ||
36
+ evidence.sourceFileQuery === true ||
37
+ evidence.kbStatus === true ||
38
+ evidence.kbCheck === true ||
39
+ evidence.kbUpsert === true);
40
+ }
41
+ function isAuthoritative(input) {
42
+ const context = input.resolvedContext ?? input.workContext;
43
+ if (context) {
44
+ return context.isAuthoritative;
45
+ }
46
+ return input.posture ? AUTHORITATIVE_POSTURES.has(input.posture) : true;
47
+ }
48
+ function normalizeEvents(input) {
49
+ return input.lifecycleEvents.map((event, index) => {
50
+ const linkedEntityResult = input.linkedEntityResults?.[index] ??
51
+ (input.linkedEntityIds?.[index]
52
+ ? { ids: input.linkedEntityIds[index] ?? [], source: "symbols" }
53
+ : DEFAULT_LINKED_ENTITY_RESULT);
54
+ return {
55
+ ...event,
56
+ pathKind: input.pathKinds[index] ?? "unknown",
57
+ linkedEntityResult,
58
+ e2eSignal: input.e2eSignals?.[index] ?? NO_E2E_SIGNAL,
59
+ };
60
+ });
61
+ }
62
+ function isIgnoredKind(pathKind) {
63
+ return pathKind === "ignored";
64
+ }
65
+ function isRelevantEvent(event, effectiveMode) {
66
+ if (isIgnoredKind(event.pathKind)) {
67
+ return false;
68
+ }
69
+ if (effectiveMode === "hard") {
70
+ return true;
71
+ }
72
+ if (event.lifecycle === "created") {
73
+ return event.pathKind === "code";
74
+ }
75
+ if (event.lifecycle === "edited") {
76
+ return ADVISORY_EDIT_KINDS.has(event.pathKind);
77
+ }
78
+ return true;
79
+ }
80
+ function uniqueReminderKinds(events) {
81
+ const kinds = [];
82
+ const add = (kind) => {
83
+ if (!kinds.includes(kind)) {
84
+ kinds.push(kind);
85
+ }
86
+ };
87
+ for (const event of events) {
88
+ if (event.lifecycle === "deleted") {
89
+ add("kibi_delete");
90
+ }
91
+ else {
92
+ add("kibi_write");
93
+ }
94
+ if (event.e2eSignal.level !== "none" && event.e2eSignal.reminderText !== null) {
95
+ add(event.lifecycle === "deleted" ? "e2e_delete" : "e2e_write");
96
+ }
97
+ }
98
+ return kinds;
99
+ }
100
+ function firstE2eReminder(events) {
101
+ return (events.find((event) => event.e2eSignal.level !== "none" &&
102
+ event.e2eSignal.reminderText !== null)?.e2eSignal.reminderText ?? null);
103
+ }
104
+ function lifecycleLabel(lifecycle) {
105
+ switch (lifecycle) {
106
+ case "created":
107
+ return "created";
108
+ case "edited":
109
+ return "edited";
110
+ case "deleted":
111
+ return "deleted";
112
+ }
113
+ }
114
+ function advisoryText(event) {
115
+ if (event.lifecycle === "created") {
116
+ return "- New file detected. Add or update the necessary Kibi entities and traceability before completing this task.";
117
+ }
118
+ if (event.lifecycle === "edited") {
119
+ return `- Edited file detected. Review Kibi traceability for ${event.normalizedPath} before completing this task.`;
120
+ }
121
+ const ids = event.linkedEntityResult.ids;
122
+ if (ids.length > 0) {
123
+ return `- Deleted file had linked Kibi entities: ${ids.join(", ")}. Update Kibi to keep traceability accurate.`;
124
+ }
125
+ return "- Deleted file had no linked Kibi entities. Update Kibi if this removal changes documented behavior or traceability.";
126
+ }
127
+ function collectUnique(values) {
128
+ const seen = new Set();
129
+ const result = [];
130
+ for (const value of values) {
131
+ if (seen.has(value)) {
132
+ continue;
133
+ }
134
+ seen.add(value);
135
+ result.push(value);
136
+ }
137
+ return result;
138
+ }
139
+ function linkedIdsText(events) {
140
+ const ids = collectUnique(events.flatMap((event) => event.linkedEntityResult.ids));
141
+ if (ids.length === 0) {
142
+ return null;
143
+ }
144
+ return `Linked IDs detected: ${ids.join(", ")}.`;
145
+ }
146
+ function e2eEvidenceText(events) {
147
+ const evidence = collectUnique(events.flatMap((event) => event.e2eSignal.evidence));
148
+ if (evidence.length === 0) {
149
+ return null;
150
+ }
151
+ return `Existing e2e evidence: ${evidence.join(", ")}.`;
152
+ }
153
+ function hardBlockText(events) {
154
+ const shownEvents = events.slice(0, HARD_BLOCK_PATH_LIMIT);
155
+ const shownPaths = shownEvents.map((event) => event.normalizedPath);
156
+ const remainingCount = Math.max(0, events.length - shownEvents.length);
157
+ const pathLines = shownEvents.map((event) => {
158
+ const ids = event.linkedEntityResult.ids;
159
+ const linkedSuffix = ids.length > 0 ? `; linked: ${ids.join(", ")}` : "";
160
+ return `- \`${event.normalizedPath}\` (${lifecycleLabel(event.lifecycle)}, ${event.pathKind}${linkedSuffix})`;
161
+ });
162
+ if (remainingCount > 0) {
163
+ pathLines.push(`- +${remainingCount} more dirty files`);
164
+ }
165
+ const deletedWithoutLinks = events.some((event) => event.lifecycle === "deleted" && event.linkedEntityResult.ids.length === 0);
166
+ const representativePath = events[0]?.normalizedPath ?? "<changed-file>";
167
+ const evidenceNotes = [linkedIdsText(events), e2eEvidenceText(events)].filter((note) => note !== null);
168
+ const deletionCleanup = deletedWithoutLinks
169
+ ? "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."
170
+ : "Use `kb_upsert` when traceability, relationships, or source-linked facts need updates.";
171
+ return {
172
+ text: [
173
+ "🛑 **Hard Kibi checkpoint required**",
174
+ "Changed relevant files require Kibi verification before continuing:",
175
+ ...pathLines,
176
+ ...evidenceNotes,
177
+ "MCP-only checkpoint instructions:",
178
+ "- Run `kb_search` to discover impacted requirements, tests, ADRs, and facts.",
179
+ `- Run \`kb_query\` with \`sourceFile\` (example sourceFile: \"${representativePath}\") for each listed path.`,
180
+ "- Run `kb_status` if branch or snapshot freshness matters.",
181
+ `- ${deletionCleanup}`,
182
+ "- Run `kb_check` before completing the task.",
183
+ ].join("\n"),
184
+ shownPaths,
185
+ remainingCount,
186
+ };
187
+ }
188
+ // implements REQ-opencode-worktree-hard-enforcement-v1
189
+ export function computeEnforcementPolicy(input) {
190
+ const allEvents = normalizeEvents(input);
191
+ const relevantEvents = allEvents.filter((event) => isRelevantEvent(event, input.effectiveMode));
192
+ const affectedPaths = relevantEvents.map((event) => event.normalizedPath);
193
+ const e2eReminder = firstE2eReminder(relevantEvents);
194
+ const reminderKindsToMark = uniqueReminderKinds(relevantEvents);
195
+ if (relevantEvents.length === 0) {
196
+ return {
197
+ kind: "checkpoint_passed",
198
+ affectedPaths,
199
+ dirtyFileCount: 0,
200
+ e2eReminder,
201
+ reminderKindsToMark,
202
+ text: null,
203
+ };
204
+ }
205
+ if (input.effectiveMode === "hard") {
206
+ if (!isAuthoritative(input)) {
207
+ return {
208
+ kind: "skip_non_authoritative",
209
+ reason: "Hard enforcement only applies to authoritative Kibi roots.",
210
+ affectedPaths,
211
+ dirtyFileCount: relevantEvents.length,
212
+ e2eReminder,
213
+ reminderKindsToMark: [],
214
+ text: null,
215
+ };
216
+ }
217
+ if (normalizeCheckpointEvidence(input.checkpointEvidence)) {
218
+ return {
219
+ kind: "checkpoint_passed",
220
+ affectedPaths,
221
+ dirtyFileCount: relevantEvents.length,
222
+ e2eReminder,
223
+ reminderKindsToMark,
224
+ text: null,
225
+ };
226
+ }
227
+ const { text, shownPaths, remainingCount } = hardBlockText(relevantEvents);
228
+ return {
229
+ kind: "hard_block",
230
+ affectedPaths,
231
+ dirtyFileCount: relevantEvents.length,
232
+ e2eReminder,
233
+ reminderKindsToMark,
234
+ text,
235
+ shownPaths,
236
+ remainingCount,
237
+ };
238
+ }
239
+ if (!isAuthoritative(input)) {
240
+ return {
241
+ kind: "skip_non_authoritative",
242
+ reason: "Lifecycle guidance is skipped outside authoritative Kibi roots.",
243
+ affectedPaths,
244
+ dirtyFileCount: relevantEvents.length,
245
+ e2eReminder,
246
+ reminderKindsToMark: [],
247
+ text: null,
248
+ };
249
+ }
250
+ const firstRelevantEvent = relevantEvents[0];
251
+ if (!firstRelevantEvent) {
252
+ return {
253
+ kind: "checkpoint_passed",
254
+ affectedPaths,
255
+ dirtyFileCount: 0,
256
+ e2eReminder,
257
+ reminderKindsToMark,
258
+ text: null,
259
+ };
260
+ }
261
+ return {
262
+ kind: "advisory_guidance",
263
+ affectedPaths,
264
+ dirtyFileCount: relevantEvents.length,
265
+ e2eReminder,
266
+ reminderKindsToMark,
267
+ text: advisoryText(firstRelevantEvent),
268
+ };
269
+ }
@@ -0,0 +1,15 @@
1
+ export interface EnforcementScopeInput {
2
+ sessionId?: string | undefined;
3
+ agentIdentity?: string | undefined;
4
+ worktreeRoot: string;
5
+ branch: string;
6
+ dirtyRelevantFingerprint: string;
7
+ }
8
+ /**
9
+ * Build a deterministic scope key for hard-enforcement decisions.
10
+ */
11
+ export declare function buildEnforcementScopeKey(input: EnforcementScopeInput): string;
12
+ /**
13
+ * Hash dirty relevant inputs into a stable, order-insensitive fingerprint.
14
+ */
15
+ export declare function buildDirtyRelevantFingerprint(values: Iterable<string | null | undefined>): string;
@@ -0,0 +1,36 @@
1
+ // implements REQ-opencode-worktree-hard-enforcement-v1
2
+ import { createHash } from "node:crypto";
3
+ import { resolve } from "node:path";
4
+ function normalizeComponent(value, fallback) {
5
+ const trimmed = value?.trim();
6
+ return trimmed && trimmed.length > 0 ? trimmed : fallback;
7
+ }
8
+ /**
9
+ * Build a deterministic scope key for hard-enforcement decisions.
10
+ */
11
+ // implements REQ-opencode-worktree-hard-enforcement-v1
12
+ export function buildEnforcementScopeKey(input) {
13
+ return JSON.stringify({
14
+ sessionId: normalizeComponent(input.sessionId, "unknown"),
15
+ agentIdentity: normalizeComponent(input.agentIdentity, "unknown"),
16
+ worktreeRoot: resolve(input.worktreeRoot),
17
+ branch: normalizeComponent(input.branch, "unknown"),
18
+ dirtyRelevantFingerprint: normalizeComponent(input.dirtyRelevantFingerprint, "clean"),
19
+ });
20
+ }
21
+ /**
22
+ * Hash dirty relevant inputs into a stable, order-insensitive fingerprint.
23
+ */
24
+ // implements REQ-opencode-worktree-hard-enforcement-v1
25
+ export function buildDirtyRelevantFingerprint(values) {
26
+ const normalized = [...values]
27
+ .map((value) => value?.trim() ?? "")
28
+ .filter((value) => value.length > 0)
29
+ .sort();
30
+ if (normalized.length === 0) {
31
+ return "clean";
32
+ }
33
+ return createHash("sha256")
34
+ .update(JSON.stringify(normalized))
35
+ .digest("hex");
36
+ }
@@ -3,6 +3,9 @@ import type { PathKind } from "./path-kind.js";
3
3
  import type { RiskClass } from "./risk-classifier.js";
4
4
  import type { ReminderKind } from "./file-operation-state.js";
5
5
  import type { E2eCoverageSignal } from "./e2e-coverage-signals.js";
6
+ import { type CheckpointEvidence, type EnforcementLifecycleEvent, type EnforcementPolicyResult } from "./enforcement-policy.js";
7
+ import type { EffectiveMode } from "./smart-enforcement.js";
8
+ import type { WorkContext } from "./work-context-resolver.js";
6
9
  export interface LinkedEntityResult {
7
10
  ids: string[];
8
11
  source: "symbols" | "doc-path" | "none";
@@ -15,10 +18,20 @@ export interface DeriveFileOperationReminderParams {
15
18
  e2eSignal: E2eCoverageSignal;
16
19
  currentSemanticRisk: RiskClass;
17
20
  posture: RepoPosture;
21
+ effectiveMode?: EffectiveMode;
22
+ workContext?: WorkContext;
23
+ resolvedContext?: WorkContext;
24
+ lifecycleEvents?: EnforcementLifecycleEvent[];
25
+ pathKinds?: PathKind[];
26
+ linkedEntityResults?: LinkedEntityResult[];
27
+ e2eSignals?: E2eCoverageSignal[];
28
+ checkpointEvidence?: CheckpointEvidence;
18
29
  }
19
30
  export interface DeriveFileOperationReminderResult {
20
31
  lifecycleReminder: string | null;
21
32
  e2eReminder: string | null;
22
33
  reminderKindsToMark: ReminderKind[];
34
+ policyDecision: EnforcementPolicyResult["kind"];
35
+ policyResult: EnforcementPolicyResult;
23
36
  }
24
37
  export declare function deriveFileOperationReminder(params: DeriveFileOperationReminderParams): DeriveFileOperationReminderResult;
@@ -1,55 +1,42 @@
1
- // ── Lifecycle reminder text ─────────────────────────────────────
2
- const NEW_FILE_REMINDER = "- New file detected. Add or update the necessary Kibi entities and traceability before completing this task.";
3
- const DELETED_WITH_IDS_REMINDER = (ids) => `- Deleted file had linked Kibi entities: ${ids}. Update Kibi to keep traceability accurate.`;
4
- const DELETED_NO_IDS_REMINDER = "- Deleted file had no linked Kibi entities. Update Kibi if this removal changes documented behavior or traceability.";
1
+ import { computeEnforcementPolicy, } from "./enforcement-policy.js";
2
+ function addUniqueReminderKind(kinds, kind) {
3
+ return kinds.includes(kind) ? kinds : [...kinds, kind];
4
+ }
5
5
  // ── Main exported function ────────────────────────────────────
6
6
  // implements REQ-opencode-file-context-guidance-v1
7
7
  export function deriveFileOperationReminder(params) {
8
- const { lifecycle, pathKind, linkedEntityResult, e2eSignal, posture, } = params;
9
- // Check if posture allows lifecycle reminders
10
- const isAuthoritativePosture = posture === "root_active" || posture === "hybrid_root_plus_vendored";
11
- // Derive lifecycle reminder
12
- let lifecycleReminder = null;
13
- const reminderKindsToMark = [];
14
- if (isAuthoritativePosture) {
15
- if (lifecycle === "created") {
16
- // Only emit create reminder for code files (not documentation, not KB docs)
17
- if (pathKind === "code") {
18
- lifecycleReminder = NEW_FILE_REMINDER;
19
- reminderKindsToMark.push("kibi_write");
20
- }
21
- }
22
- else if (lifecycle === "edited") {
23
- // No generic lifecycle reminder for edited files
24
- // Existing semantic risk guidance remains primary
25
- }
26
- else if (lifecycle === "deleted") {
27
- const ids = linkedEntityResult.ids;
28
- if (ids.length > 0) {
29
- lifecycleReminder = DELETED_WITH_IDS_REMINDER(ids.join(", "));
30
- reminderKindsToMark.push("kibi_delete");
31
- }
32
- else {
33
- lifecycleReminder = DELETED_NO_IDS_REMINDER;
34
- reminderKindsToMark.push("kibi_delete");
35
- }
36
- }
37
- }
8
+ const { normalizedPath, lifecycle, pathKind, linkedEntityResult, e2eSignal, currentSemanticRisk, posture, } = params;
9
+ const policyResult = computeEnforcementPolicy({
10
+ resolvedContext: params.resolvedContext,
11
+ workContext: params.workContext,
12
+ effectiveMode: params.effectiveMode ?? "advisory",
13
+ lifecycleEvents: params.lifecycleEvents ?? [{ normalizedPath, lifecycle }],
14
+ pathKinds: params.pathKinds ?? [pathKind],
15
+ linkedEntityResults: params.linkedEntityResults ?? [linkedEntityResult],
16
+ e2eSignals: params.e2eSignals ?? [e2eSignal],
17
+ currentSemanticRisk,
18
+ checkpointEvidence: params.checkpointEvidence,
19
+ posture,
20
+ });
21
+ const lifecycleReminder = policyResult.text;
22
+ let reminderKindsToMark = [...policyResult.reminderKindsToMark];
38
23
  // Derive e2e reminder (only when e2e signal exists)
39
24
  // E2e reminders are NOT posture-gated - they're always relevant
40
- let e2eReminder = null;
25
+ let e2eReminder = policyResult.e2eReminder;
41
26
  if (e2eSignal.level !== "none" && e2eSignal.reminderText !== null) {
42
27
  e2eReminder = e2eSignal.reminderText;
43
28
  if (lifecycle === "deleted") {
44
- reminderKindsToMark.push("e2e_delete");
29
+ reminderKindsToMark = addUniqueReminderKind(reminderKindsToMark, "e2e_delete");
45
30
  }
46
31
  else {
47
- reminderKindsToMark.push("e2e_write");
32
+ reminderKindsToMark = addUniqueReminderKind(reminderKindsToMark, "e2e_write");
48
33
  }
49
34
  }
50
35
  return {
51
36
  lifecycleReminder,
52
37
  e2eReminder,
53
38
  reminderKindsToMark,
39
+ policyDecision: policyResult.kind,
40
+ policyResult,
54
41
  };
55
42
  }
@@ -3,6 +3,8 @@ import type { RiskClass } from "./risk-classifier.js";
3
3
  /**
4
4
  * Cache key uniquely identifies a preflight context by combining
5
5
  * workspace root, branch, posture, risk class, and file bucket.
6
+ * Hard-enforcement callers may additionally pass scopeKey to prevent
7
+ * cross-session/worktree dirty-state cache hits. Advisory callers omit it.
6
8
  */
7
9
  export interface CacheKey {
8
10
  workspaceRoot: string;
@@ -10,6 +12,7 @@ export interface CacheKey {
10
12
  posture: RepoPosture;
11
13
  riskClass: RiskClass;
12
14
  fileBucket: string;
15
+ scopeKey?: string;
13
16
  }
14
17
  /**
15
18
  * Cache entry tracking when a preflight was last satisfied.
@@ -3,7 +3,7 @@
3
3
  * Serializes a CacheKey into a deterministic string for use as a Map key.
4
4
  */
5
5
  function serializeKey(key) {
6
- return `${key.workspaceRoot}\0${key.branch}\0${key.posture}\0${key.riskClass}\0${key.fileBucket}`;
6
+ return `${key.workspaceRoot}\0${key.branch}\0${key.posture}\0${key.riskClass}\0${key.fileBucket}\0${key.scopeKey ?? ""}`;
7
7
  }
8
8
  /**
9
9
  * In-memory cache tracking satisfied preflight checks per unique context.