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 +4 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/enforcement-policy.d.ts +71 -0
- package/dist/enforcement-policy.js +269 -0
- package/dist/enforcement-scope.d.ts +15 -0
- package/dist/enforcement-scope.js +36 -0
- package/dist/file-operation-reminders.d.ts +13 -0
- package/dist/file-operation-reminders.js +24 -37
- package/dist/guidance-cache.d.ts +3 -0
- package/dist/guidance-cache.js +1 -1
- package/dist/kibi-checkpoint-runner.d.ts +83 -0
- package/dist/kibi-checkpoint-runner.js +254 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +407 -164
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +25 -0
- package/dist/smart-enforcement.d.ts +6 -2
- package/dist/smart-enforcement.js +7 -1
- package/dist/work-context-resolver.d.ts +21 -0
- package/dist/work-context-resolver.js +197 -0
- package/package.json +1 -1
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`
|
|
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
|
-
- **
|
|
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 `
|
|
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
|
|
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
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 =
|
|
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
|
|
29
|
+
reminderKindsToMark = addUniqueReminderKind(reminderKindsToMark, "e2e_delete");
|
|
45
30
|
}
|
|
46
31
|
else {
|
|
47
|
-
reminderKindsToMark
|
|
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
|
}
|
package/dist/guidance-cache.d.ts
CHANGED
|
@@ -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.
|
package/dist/guidance-cache.js
CHANGED
|
@@ -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.
|