kibi-opencode 0.13.0 → 0.15.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 +18 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/enforcement-policy.d.ts +73 -0
- package/dist/enforcement-policy.js +279 -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/kb-freshness-state.d.ts +62 -0
- package/dist/kb-freshness-state.js +211 -0
- package/dist/kibi-checkpoint-runner.d.ts +83 -0
- package/dist/kibi-checkpoint-runner.js +254 -0
- package/dist/meaningful-change-classifier.d.ts +30 -0
- package/dist/meaningful-change-classifier.js +104 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +480 -177
- package/dist/prompt.d.ts +11 -0
- package/dist/prompt.js +54 -2
- package/dist/smart-enforcement.d.ts +6 -2
- package/dist/smart-enforcement.js +7 -1
- package/dist/startup-notifier.d.ts +7 -1
- package/dist/startup-notifier.js +51 -17
- package/dist/version-metadata.d.ts +13 -0
- package/dist/version-metadata.js +78 -0
- package/dist/version-metadata.json +1 -0
- package/dist/work-context-resolver.d.ts +21 -0
- package/dist/work-context-resolver.js +197 -0
- package/package.json +4 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB freshness state machine, evidence model, and in-memory evidence store.
|
|
3
|
+
*
|
|
4
|
+
* This is Wave 1 — the foundation module. No MCP tools, no filesystem or
|
|
5
|
+
* `.kb/` persistence, no entity mutation. Pure in-memory state.
|
|
6
|
+
*
|
|
7
|
+
* @module kb-freshness-state
|
|
8
|
+
*/
|
|
9
|
+
export type KbFreshnessDecision = "updated" | "no-impact" | "deferred" | "failed";
|
|
10
|
+
export type KbFreshnessStateKind = "clean" | "unknown-impact" | "evidence-required" | "updated" | "no-impact-accepted" | "deferred" | "check-failed";
|
|
11
|
+
export interface KbFreshnessEvidence {
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
agentIdentity: string;
|
|
14
|
+
worktree: string;
|
|
15
|
+
branch: string;
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
changedFiles: string[];
|
|
18
|
+
kbStatus: boolean;
|
|
19
|
+
sourceLinkedDiscovery: boolean;
|
|
20
|
+
kbMutation: boolean;
|
|
21
|
+
kbCheck: boolean;
|
|
22
|
+
decision?: KbFreshnessDecision;
|
|
23
|
+
rationale?: string;
|
|
24
|
+
checkRules?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface KbFreshnessEvaluation {
|
|
27
|
+
state: KbFreshnessStateKind;
|
|
28
|
+
requiresEvidence: boolean;
|
|
29
|
+
allowsCompletion: boolean;
|
|
30
|
+
reason: string;
|
|
31
|
+
missingEvidence: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate KB freshness from collected evidence.
|
|
35
|
+
*
|
|
36
|
+
* Rules (in priority order):
|
|
37
|
+
* 1. No changed files → `clean`, allows completion.
|
|
38
|
+
* 2. Changed files but no decision → `evidence-required`, blocks completion.
|
|
39
|
+
* 3. `updated` → requires `kbMutation` + `kbCheck`.
|
|
40
|
+
* 4. `no-impact` → requires `rationale` + `sourceLinkedDiscovery` + `kbCheck`.
|
|
41
|
+
* 5. `deferred` → blocks completion.
|
|
42
|
+
* 6. `failed` → `check-failed`, blocks completion.
|
|
43
|
+
*/
|
|
44
|
+
export declare function evaluateKbFreshness(evidence: KbFreshnessEvidence): KbFreshnessEvaluation;
|
|
45
|
+
export interface KbFreshnessScope {
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
agentIdentity: string;
|
|
48
|
+
worktree: string;
|
|
49
|
+
branch: string;
|
|
50
|
+
fingerprint: string;
|
|
51
|
+
}
|
|
52
|
+
export interface KbFreshnessEvidenceStore {
|
|
53
|
+
recordToolEvidence(scope: KbFreshnessScope, toolName: string): void;
|
|
54
|
+
recordNoImpact(scope: KbFreshnessScope, rationale: string): void;
|
|
55
|
+
getEvidence(scope: KbFreshnessScope, changedFiles: string[]): KbFreshnessEvidence;
|
|
56
|
+
resetScope(scope: KbFreshnessScope): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an in-memory evidence store scoped by
|
|
60
|
+
* (sessionId | agentIdentity | worktree | branch | fingerprint).
|
|
61
|
+
*/
|
|
62
|
+
export declare function createKbFreshnessEvidenceStore(): KbFreshnessEvidenceStore;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KB freshness state machine, evidence model, and in-memory evidence store.
|
|
3
|
+
*
|
|
4
|
+
* This is Wave 1 — the foundation module. No MCP tools, no filesystem or
|
|
5
|
+
* `.kb/` persistence, no entity mutation. Pure in-memory state.
|
|
6
|
+
*
|
|
7
|
+
* @module kb-freshness-state
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Evaluation
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate KB freshness from collected evidence.
|
|
14
|
+
*
|
|
15
|
+
* Rules (in priority order):
|
|
16
|
+
* 1. No changed files → `clean`, allows completion.
|
|
17
|
+
* 2. Changed files but no decision → `evidence-required`, blocks completion.
|
|
18
|
+
* 3. `updated` → requires `kbMutation` + `kbCheck`.
|
|
19
|
+
* 4. `no-impact` → requires `rationale` + `sourceLinkedDiscovery` + `kbCheck`.
|
|
20
|
+
* 5. `deferred` → blocks completion.
|
|
21
|
+
* 6. `failed` → `check-failed`, blocks completion.
|
|
22
|
+
*/
|
|
23
|
+
export function evaluateKbFreshness(evidence) {
|
|
24
|
+
// Rule 1: No changed files
|
|
25
|
+
if (evidence.changedFiles.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
state: "clean",
|
|
28
|
+
requiresEvidence: false,
|
|
29
|
+
allowsCompletion: true,
|
|
30
|
+
reason: "No files changed",
|
|
31
|
+
missingEvidence: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Rule 2: No decision recorded yet
|
|
35
|
+
if (evidence.decision === undefined) {
|
|
36
|
+
return {
|
|
37
|
+
state: "evidence-required",
|
|
38
|
+
requiresEvidence: true,
|
|
39
|
+
allowsCompletion: false,
|
|
40
|
+
reason: "Session modified files without KB tool evidence",
|
|
41
|
+
missingEvidence: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Rule 5: Deferred
|
|
45
|
+
if (evidence.decision === "deferred") {
|
|
46
|
+
return {
|
|
47
|
+
state: "deferred",
|
|
48
|
+
requiresEvidence: false,
|
|
49
|
+
allowsCompletion: false,
|
|
50
|
+
reason: "KB freshness check was explicitly deferred",
|
|
51
|
+
missingEvidence: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Rule 6: Failed
|
|
55
|
+
if (evidence.decision === "failed") {
|
|
56
|
+
return {
|
|
57
|
+
state: "check-failed",
|
|
58
|
+
requiresEvidence: false,
|
|
59
|
+
allowsCompletion: false,
|
|
60
|
+
reason: "KB freshness check failed",
|
|
61
|
+
missingEvidence: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Rule 3: Updated
|
|
65
|
+
if (evidence.decision === "updated") {
|
|
66
|
+
const missingEvidence = [];
|
|
67
|
+
if (!evidence.kbMutation) {
|
|
68
|
+
missingEvidence.push("kbMutation");
|
|
69
|
+
}
|
|
70
|
+
if (!evidence.kbCheck) {
|
|
71
|
+
missingEvidence.push("kbCheck");
|
|
72
|
+
}
|
|
73
|
+
if (missingEvidence.length > 0) {
|
|
74
|
+
return {
|
|
75
|
+
state: "evidence-required",
|
|
76
|
+
requiresEvidence: true,
|
|
77
|
+
allowsCompletion: false,
|
|
78
|
+
reason: "KB was updated but required evidence (kbMutation, kbCheck) is missing",
|
|
79
|
+
missingEvidence,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
state: "updated",
|
|
84
|
+
requiresEvidence: false,
|
|
85
|
+
allowsCompletion: true,
|
|
86
|
+
reason: "KB updated and verified via kb_check",
|
|
87
|
+
missingEvidence: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Rule 4: No-impact
|
|
91
|
+
if (evidence.decision === "no-impact") {
|
|
92
|
+
const missingEvidence = [];
|
|
93
|
+
if (!evidence.rationale || evidence.rationale === "") {
|
|
94
|
+
missingEvidence.push("rationale");
|
|
95
|
+
}
|
|
96
|
+
if (!evidence.sourceLinkedDiscovery) {
|
|
97
|
+
missingEvidence.push("sourceLinkedDiscovery");
|
|
98
|
+
}
|
|
99
|
+
if (!evidence.kbCheck) {
|
|
100
|
+
missingEvidence.push("kbCheck");
|
|
101
|
+
}
|
|
102
|
+
if (missingEvidence.length > 0) {
|
|
103
|
+
return {
|
|
104
|
+
state: "evidence-required",
|
|
105
|
+
requiresEvidence: true,
|
|
106
|
+
allowsCompletion: false,
|
|
107
|
+
reason: "No-impact claim needs rationale, sourceLinkedDiscovery, and kbCheck",
|
|
108
|
+
missingEvidence,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
state: "no-impact-accepted",
|
|
113
|
+
requiresEvidence: false,
|
|
114
|
+
allowsCompletion: true,
|
|
115
|
+
reason: "No-impact accepted with rationale and KB check passed",
|
|
116
|
+
missingEvidence: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Exhaustive guard — all decision branches handled above
|
|
120
|
+
const _exhaustive = evidence.decision;
|
|
121
|
+
throw new Error(`Unexpected decision: ${_exhaustive}`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Map of tool names → evidence side effects.
|
|
125
|
+
* Each entry lists the evidence fields to set when that tool is used.
|
|
126
|
+
*/
|
|
127
|
+
const TOOL_EFFECTS = {
|
|
128
|
+
kb_status: ["kbStatus"],
|
|
129
|
+
kb_search: ["sourceLinkedDiscovery"],
|
|
130
|
+
kb_query: ["sourceLinkedDiscovery"],
|
|
131
|
+
kb_graph: ["sourceLinkedDiscovery"],
|
|
132
|
+
kb_find_gaps: ["sourceLinkedDiscovery"],
|
|
133
|
+
kb_coverage: ["sourceLinkedDiscovery"],
|
|
134
|
+
kb_upsert: ["kbMutation"],
|
|
135
|
+
kb_delete: ["kbMutation"],
|
|
136
|
+
kb_check: ["kbCheck"],
|
|
137
|
+
};
|
|
138
|
+
function buildScopeKey(scope) {
|
|
139
|
+
const parts = [
|
|
140
|
+
scope.sessionId ?? "",
|
|
141
|
+
scope.agentIdentity,
|
|
142
|
+
scope.worktree,
|
|
143
|
+
scope.branch,
|
|
144
|
+
scope.fingerprint,
|
|
145
|
+
];
|
|
146
|
+
return parts.join("|");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create an in-memory evidence store scoped by
|
|
150
|
+
* (sessionId | agentIdentity | worktree | branch | fingerprint).
|
|
151
|
+
*/
|
|
152
|
+
export function createKbFreshnessEvidenceStore() {
|
|
153
|
+
const store = new Map();
|
|
154
|
+
function getOrCreate(scope) {
|
|
155
|
+
const key = buildScopeKey(scope);
|
|
156
|
+
if (!store.has(key)) {
|
|
157
|
+
store.set(key, {
|
|
158
|
+
kbStatus: false,
|
|
159
|
+
sourceLinkedDiscovery: false,
|
|
160
|
+
kbMutation: false,
|
|
161
|
+
kbCheck: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return store.get(key);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
recordToolEvidence(scope, toolName) {
|
|
168
|
+
const record = getOrCreate(scope);
|
|
169
|
+
const effects = TOOL_EFFECTS[toolName];
|
|
170
|
+
if (!effects) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const field of effects) {
|
|
174
|
+
if (field === "kbMutation") {
|
|
175
|
+
record.kbMutation = true;
|
|
176
|
+
record.decision = "updated";
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
record[field] = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
recordNoImpact(scope, rationale) {
|
|
184
|
+
const record = getOrCreate(scope);
|
|
185
|
+
record.decision = "no-impact";
|
|
186
|
+
record.rationale = rationale;
|
|
187
|
+
},
|
|
188
|
+
getEvidence(scope, changedFiles) {
|
|
189
|
+
const record = getOrCreate(scope);
|
|
190
|
+
return {
|
|
191
|
+
...(scope.sessionId ? { sessionId: scope.sessionId } : {}),
|
|
192
|
+
agentIdentity: scope.agentIdentity,
|
|
193
|
+
worktree: scope.worktree,
|
|
194
|
+
branch: scope.branch,
|
|
195
|
+
fingerprint: scope.fingerprint,
|
|
196
|
+
changedFiles,
|
|
197
|
+
kbStatus: record.kbStatus,
|
|
198
|
+
sourceLinkedDiscovery: record.sourceLinkedDiscovery,
|
|
199
|
+
kbMutation: record.kbMutation,
|
|
200
|
+
kbCheck: record.kbCheck,
|
|
201
|
+
...(record.decision !== undefined ? { decision: record.decision } : {}),
|
|
202
|
+
...(record.rationale !== undefined ? { rationale: record.rationale } : {}),
|
|
203
|
+
...(record.checkRules !== undefined ? { checkRules: record.checkRules } : {}),
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
resetScope(scope) {
|
|
207
|
+
const key = buildScopeKey(scope);
|
|
208
|
+
store.delete(key);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type KibiConfig } from "./config.js";
|
|
2
|
+
import { type EnforcementLifecycleEvent, type PolicyLinkedEntityResult } from "./enforcement-policy.js";
|
|
3
|
+
import type { E2eCoverageSignal } from "./e2e-coverage-signals.js";
|
|
4
|
+
import type { PathKind } from "./path-kind.js";
|
|
5
|
+
import { type CheckRunner, type SyncRunMetadata, type SyncRunner, type TimeoutHandle } from "./scheduler.js";
|
|
6
|
+
import type { WorkContext } from "./work-context-resolver.js";
|
|
7
|
+
export interface KibiCheckpointContext {
|
|
8
|
+
workContext: WorkContext;
|
|
9
|
+
config?: KibiConfig;
|
|
10
|
+
filePath?: string;
|
|
11
|
+
checkRules?: string[] | undefined;
|
|
12
|
+
maintenanceDegraded?: boolean;
|
|
13
|
+
hardGuidanceText?: string | null;
|
|
14
|
+
lifecycleEvents?: EnforcementLifecycleEvent[];
|
|
15
|
+
pathKinds?: PathKind[];
|
|
16
|
+
linkedEntityResults?: PolicyLinkedEntityResult[];
|
|
17
|
+
e2eSignals?: E2eCoverageSignal[];
|
|
18
|
+
}
|
|
19
|
+
export interface KibiCheckpointRunnerOptions {
|
|
20
|
+
config?: KibiConfig;
|
|
21
|
+
runSync?: SyncRunner;
|
|
22
|
+
runCheck?: CheckRunner;
|
|
23
|
+
onRunComplete?: (meta: SyncRunMetadata) => void;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
setTimeoutFn?: (fn: () => void, ms: number) => TimeoutHandle;
|
|
26
|
+
clearTimeoutFn?: (handle: TimeoutHandle) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface KibiCheckpointMetadata {
|
|
29
|
+
fingerprint: string;
|
|
30
|
+
scopeKey: string;
|
|
31
|
+
worktree: string;
|
|
32
|
+
branch: string;
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
agentIdentity: string;
|
|
35
|
+
guidanceRendered?: boolean;
|
|
36
|
+
guidanceText?: string;
|
|
37
|
+
sync?: SyncRunMetadata;
|
|
38
|
+
reason?: string;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
checkRules?: string[] | undefined;
|
|
41
|
+
restoreInstructions?: string;
|
|
42
|
+
}
|
|
43
|
+
export type KibiCheckpointRequestResult = {
|
|
44
|
+
kind: "requested";
|
|
45
|
+
metadata: KibiCheckpointMetadata;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "skip";
|
|
48
|
+
metadata: KibiCheckpointMetadata;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "hard_block";
|
|
51
|
+
metadata: KibiCheckpointMetadata;
|
|
52
|
+
};
|
|
53
|
+
export type KibiCheckpointRunResult = {
|
|
54
|
+
kind: "passed";
|
|
55
|
+
metadata: KibiCheckpointMetadata;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "hard_block";
|
|
58
|
+
metadata: KibiCheckpointMetadata;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "skip";
|
|
61
|
+
metadata: KibiCheckpointMetadata;
|
|
62
|
+
};
|
|
63
|
+
export declare class KibiCheckpointRunner {
|
|
64
|
+
private readonly config;
|
|
65
|
+
private readonly runSync;
|
|
66
|
+
private readonly runCheck;
|
|
67
|
+
private readonly onRunComplete;
|
|
68
|
+
private readonly timeoutMs;
|
|
69
|
+
private readonly setTimeoutFn;
|
|
70
|
+
private readonly clearTimeoutFn;
|
|
71
|
+
private readonly requested;
|
|
72
|
+
private readonly passed;
|
|
73
|
+
private readonly passedScopeKeysByFingerprint;
|
|
74
|
+
constructor(options?: KibiCheckpointRunnerOptions);
|
|
75
|
+
requestCheckpoint(context: KibiCheckpointContext, fingerprint: string): KibiCheckpointRequestResult;
|
|
76
|
+
isCheckpointPassed(fingerprint: string, context?: KibiCheckpointContext): boolean;
|
|
77
|
+
runCheckpoint(context: KibiCheckpointContext, fingerprint: string): Promise<KibiCheckpointRunResult>;
|
|
78
|
+
private baseMetadata;
|
|
79
|
+
private scopeKey;
|
|
80
|
+
private renderHardGuidance;
|
|
81
|
+
private withTimeout;
|
|
82
|
+
private recordPassed;
|
|
83
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
2
|
+
import { DEFAULTS } from "./config.js";
|
|
3
|
+
import { buildEnforcementScopeKey } from "./enforcement-scope.js";
|
|
4
|
+
import { computeEnforcementPolicy, } from "./enforcement-policy.js";
|
|
5
|
+
import { createSyncScheduler, } from "./scheduler.js";
|
|
6
|
+
const HARD_CHECKPOINT_TIMEOUT_MS = 30_000;
|
|
7
|
+
const CHECKPOINT_REASON = "smart-enforcement.checkpoint";
|
|
8
|
+
const NO_E2E_SIGNAL = {
|
|
9
|
+
level: "none",
|
|
10
|
+
evidence: [],
|
|
11
|
+
reminderText: null,
|
|
12
|
+
};
|
|
13
|
+
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
14
|
+
export class KibiCheckpointRunner {
|
|
15
|
+
config;
|
|
16
|
+
runSync;
|
|
17
|
+
runCheck;
|
|
18
|
+
onRunComplete;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
setTimeoutFn;
|
|
21
|
+
clearTimeoutFn;
|
|
22
|
+
requested = new Map();
|
|
23
|
+
passed = new Map();
|
|
24
|
+
passedScopeKeysByFingerprint = new Map();
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.config = options.config;
|
|
27
|
+
this.runSync = options.runSync;
|
|
28
|
+
this.runCheck = options.runCheck;
|
|
29
|
+
this.onRunComplete = options.onRunComplete;
|
|
30
|
+
this.timeoutMs = options.timeoutMs ?? HARD_CHECKPOINT_TIMEOUT_MS;
|
|
31
|
+
this.setTimeoutFn = options.setTimeoutFn ?? setTimeout;
|
|
32
|
+
this.clearTimeoutFn = options.clearTimeoutFn ?? clearTimeout;
|
|
33
|
+
}
|
|
34
|
+
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
35
|
+
requestCheckpoint(context, fingerprint) {
|
|
36
|
+
const normalizedFingerprint = normalizeFingerprint(fingerprint);
|
|
37
|
+
const metadata = this.baseMetadata(context, normalizedFingerprint);
|
|
38
|
+
if (!context.workContext.isAuthoritative) {
|
|
39
|
+
return {
|
|
40
|
+
kind: "skip",
|
|
41
|
+
metadata: { ...metadata, reason: "non_authoritative" },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const guidanceText = this.renderHardGuidance(context);
|
|
45
|
+
if (!guidanceText) {
|
|
46
|
+
return {
|
|
47
|
+
kind: "hard_block",
|
|
48
|
+
metadata: {
|
|
49
|
+
...metadata,
|
|
50
|
+
guidanceRendered: false,
|
|
51
|
+
reason: "hard_guidance_not_rendered",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const requested = {
|
|
56
|
+
guidanceRendered: true,
|
|
57
|
+
metadata: {
|
|
58
|
+
...metadata,
|
|
59
|
+
guidanceRendered: true,
|
|
60
|
+
guidanceText,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
this.requested.set(metadata.scopeKey, requested);
|
|
64
|
+
return { kind: "requested", metadata: requested.metadata };
|
|
65
|
+
}
|
|
66
|
+
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
67
|
+
isCheckpointPassed(fingerprint, context) {
|
|
68
|
+
const normalizedFingerprint = normalizeFingerprint(fingerprint);
|
|
69
|
+
if (context) {
|
|
70
|
+
return this.passed.has(this.scopeKey(context, normalizedFingerprint));
|
|
71
|
+
}
|
|
72
|
+
return (this.passedScopeKeysByFingerprint.get(normalizedFingerprint)?.size ?? 0) > 0;
|
|
73
|
+
}
|
|
74
|
+
// implements REQ-opencode-worktree-hard-enforcement-v1
|
|
75
|
+
async runCheckpoint(context, fingerprint) {
|
|
76
|
+
const normalizedFingerprint = normalizeFingerprint(fingerprint);
|
|
77
|
+
const metadata = this.baseMetadata(context, normalizedFingerprint);
|
|
78
|
+
if (!context.workContext.isAuthoritative) {
|
|
79
|
+
return {
|
|
80
|
+
kind: "skip",
|
|
81
|
+
metadata: { ...metadata, reason: "non_authoritative" },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (context.maintenanceDegraded === true) {
|
|
85
|
+
return {
|
|
86
|
+
kind: "hard_block",
|
|
87
|
+
metadata: {
|
|
88
|
+
...metadata,
|
|
89
|
+
reason: "maintenance_degraded",
|
|
90
|
+
restoreInstructions: "Restore Kibi maintenance for this authoritative root before continuing: ensure sync is enabled and configured KB targets resolve, then retry the hard checkpoint.",
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const request = this.requested.get(metadata.scopeKey);
|
|
95
|
+
if (!request?.guidanceRendered) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "hard_block",
|
|
98
|
+
metadata: {
|
|
99
|
+
...metadata,
|
|
100
|
+
guidanceRendered: false,
|
|
101
|
+
reason: "checkpoint_not_requested",
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const completions = [];
|
|
106
|
+
const scheduler = createSyncScheduler({
|
|
107
|
+
worktree: context.workContext.worktreeRoot,
|
|
108
|
+
config: context.config ?? this.config ?? DEFAULTS,
|
|
109
|
+
...(this.runSync ? { runSync: this.runSync } : {}),
|
|
110
|
+
...(this.runCheck ? { runCheck: this.runCheck } : {}),
|
|
111
|
+
onRunComplete: (meta) => {
|
|
112
|
+
completions.push(meta);
|
|
113
|
+
this.onRunComplete?.(meta);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
scheduler.scheduleSync(CHECKPOINT_REASON, context.filePath, normalizedCheckRules(context));
|
|
118
|
+
const outcome = await this.withTimeout(scheduler.flush());
|
|
119
|
+
if (outcome === "timeout") {
|
|
120
|
+
scheduler.dispose();
|
|
121
|
+
return {
|
|
122
|
+
kind: "hard_block",
|
|
123
|
+
metadata: {
|
|
124
|
+
...request.metadata,
|
|
125
|
+
reason: "timeout",
|
|
126
|
+
timeoutMs: this.timeoutMs,
|
|
127
|
+
checkRules: normalizedCheckRules(context),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const sync = completions[completions.length - 1];
|
|
132
|
+
if (!sync) {
|
|
133
|
+
return {
|
|
134
|
+
kind: "hard_block",
|
|
135
|
+
metadata: {
|
|
136
|
+
...request.metadata,
|
|
137
|
+
reason: "sync_not_run",
|
|
138
|
+
checkRules: normalizedCheckRules(context),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const failureReason = checkpointFailureReason(sync, normalizedCheckRules(context));
|
|
143
|
+
if (failureReason) {
|
|
144
|
+
return {
|
|
145
|
+
kind: "hard_block",
|
|
146
|
+
metadata: {
|
|
147
|
+
...request.metadata,
|
|
148
|
+
sync,
|
|
149
|
+
reason: failureReason,
|
|
150
|
+
checkRules: normalizedCheckRules(context),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const passedMetadata = {
|
|
155
|
+
...request.metadata,
|
|
156
|
+
sync,
|
|
157
|
+
checkRules: normalizedCheckRules(context),
|
|
158
|
+
};
|
|
159
|
+
this.recordPassed(passedMetadata);
|
|
160
|
+
return { kind: "passed", metadata: passedMetadata };
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
scheduler.dispose();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
baseMetadata(context, fingerprint) {
|
|
167
|
+
const workContext = context.workContext;
|
|
168
|
+
return {
|
|
169
|
+
fingerprint,
|
|
170
|
+
scopeKey: this.scopeKey(context, fingerprint),
|
|
171
|
+
worktree: workContext.worktreeRoot,
|
|
172
|
+
branch: workContext.branch,
|
|
173
|
+
...(workContext.sessionId !== undefined
|
|
174
|
+
? { sessionId: workContext.sessionId }
|
|
175
|
+
: {}),
|
|
176
|
+
agentIdentity: workContext.agentIdentity,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
scopeKey(context, fingerprint) {
|
|
180
|
+
return buildEnforcementScopeKey({
|
|
181
|
+
sessionId: context.workContext.sessionId,
|
|
182
|
+
agentIdentity: context.workContext.agentIdentity,
|
|
183
|
+
worktreeRoot: context.workContext.worktreeRoot,
|
|
184
|
+
branch: context.workContext.branch,
|
|
185
|
+
dirtyRelevantFingerprint: fingerprint,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
renderHardGuidance(context) {
|
|
189
|
+
if (typeof context.hardGuidanceText === "string") {
|
|
190
|
+
const trimmed = context.hardGuidanceText.trim();
|
|
191
|
+
return trimmed.length > 0 ? context.hardGuidanceText : null;
|
|
192
|
+
}
|
|
193
|
+
const policy = computeEnforcementPolicy({
|
|
194
|
+
resolvedContext: context.workContext,
|
|
195
|
+
effectiveMode: "hard",
|
|
196
|
+
lifecycleEvents: context.lifecycleEvents ?? [defaultLifecycleEvent(context)],
|
|
197
|
+
pathKinds: context.pathKinds ?? ["code"],
|
|
198
|
+
linkedEntityResults: context.linkedEntityResults ?? [
|
|
199
|
+
{ ids: [], source: "none" },
|
|
200
|
+
],
|
|
201
|
+
e2eSignals: context.e2eSignals ?? [NO_E2E_SIGNAL],
|
|
202
|
+
checkpointEvidence: false,
|
|
203
|
+
posture: context.workContext.posture,
|
|
204
|
+
});
|
|
205
|
+
return policy.kind === "hard_block" ? policy.text : null;
|
|
206
|
+
}
|
|
207
|
+
async withTimeout(promise) {
|
|
208
|
+
let timeoutHandle;
|
|
209
|
+
const timeout = new Promise((resolve) => {
|
|
210
|
+
timeoutHandle = this.setTimeoutFn(() => resolve("timeout"), this.timeoutMs);
|
|
211
|
+
});
|
|
212
|
+
const result = await Promise.race([
|
|
213
|
+
promise.then(() => "done"),
|
|
214
|
+
timeout,
|
|
215
|
+
]);
|
|
216
|
+
if (timeoutHandle !== undefined) {
|
|
217
|
+
this.clearTimeoutFn(timeoutHandle);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
recordPassed(metadata) {
|
|
222
|
+
this.passed.set(metadata.scopeKey, metadata);
|
|
223
|
+
const existing = this.passedScopeKeysByFingerprint.get(metadata.fingerprint);
|
|
224
|
+
if (existing) {
|
|
225
|
+
existing.add(metadata.scopeKey);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.passedScopeKeysByFingerprint.set(metadata.fingerprint, new Set([metadata.scopeKey]));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function normalizeFingerprint(fingerprint) {
|
|
232
|
+
const trimmed = fingerprint.trim();
|
|
233
|
+
return trimmed.length > 0 ? trimmed : "clean";
|
|
234
|
+
}
|
|
235
|
+
function normalizedCheckRules(context) {
|
|
236
|
+
return context.checkRules && context.checkRules.length > 0
|
|
237
|
+
? [...context.checkRules]
|
|
238
|
+
: undefined;
|
|
239
|
+
}
|
|
240
|
+
function defaultLifecycleEvent(context) {
|
|
241
|
+
return {
|
|
242
|
+
normalizedPath: context.filePath ?? context.workContext.repoRelativePath ?? "<unknown>",
|
|
243
|
+
lifecycle: "edited",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function checkpointFailureReason(sync, checkRules) {
|
|
247
|
+
if (sync.exitCode !== 0) {
|
|
248
|
+
return "sync_failed";
|
|
249
|
+
}
|
|
250
|
+
if (checkRules && checkRules.length > 0 && sync.checkExitCode !== 0) {
|
|
251
|
+
return "check_failed";
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PathKind } from "./path-kind.js";
|
|
2
|
+
import type { FileLifecycle } from "./file-operation-state.js";
|
|
3
|
+
import type { RiskClass } from "./risk-classifier.js";
|
|
4
|
+
/**
|
|
5
|
+
* Classification for whether a file change requires KB freshness evidence.
|
|
6
|
+
*
|
|
7
|
+
* - "requires-kb-evidence": The change is meaningful for KB freshness — the KB
|
|
8
|
+
* snapshot should be considered stale and needs a re-sync.
|
|
9
|
+
* - "advisory": The change is low-risk (lockfiles, docs-only) — advisory only.
|
|
10
|
+
* - "ignored": The change is in a build artifact or third-party path — no KB
|
|
11
|
+
* impact.
|
|
12
|
+
*/
|
|
13
|
+
export type MeaningfulChangeClass = "requires-kb-evidence" | "advisory" | "ignored";
|
|
14
|
+
/**
|
|
15
|
+
* Classify whether a file change requires KB freshness evidence.
|
|
16
|
+
*
|
|
17
|
+
* Rules (first match wins):
|
|
18
|
+
* 1. Ignored paths (build artifacts, vendor dirs) → `ignored`
|
|
19
|
+
* 2. Advisory paths (lockfiles) → `advisory`
|
|
20
|
+
* 3. Meaningful pathKinds (code, requirements, KB docs, etc.) → `requires-kb-evidence`
|
|
21
|
+
* 4. Risk-based upgrade (behavior/traceability/req_policy candidates) → `requires-kb-evidence`
|
|
22
|
+
* 5. `safe_docs_only` risk + `unknown` pathKind → `advisory`
|
|
23
|
+
* 6. Default → `requires-kb-evidence` (safety default)
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyMeaningfulChange(params: {
|
|
26
|
+
normalizedPath: string;
|
|
27
|
+
pathKind: PathKind;
|
|
28
|
+
lifecycle: FileLifecycle;
|
|
29
|
+
riskClass?: RiskClass;
|
|
30
|
+
}): MeaningfulChangeClass;
|