@jmtrin/opencode-kevin 1.1.0 → 1.2.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 +30 -1
- package/dist/plugin/ChatBridge.d.ts +41 -0
- package/dist/plugin/ChatBridge.js +103 -0
- package/dist/plugin/DashboardHtml.d.ts +5 -0
- package/dist/plugin/DashboardHtml.js +180 -0
- package/dist/plugin/Retrospective.js +3 -0
- package/dist/plugin/TuiActions.d.ts +43 -0
- package/dist/plugin/TuiActions.js +181 -0
- package/dist/plugin/TuiSnapshots.d.ts +24 -0
- package/dist/plugin/TuiSnapshots.js +158 -0
- package/dist/plugin/capabilities.d.ts +2 -0
- package/dist/plugin/capabilities.js +3 -0
- package/dist/plugin/contract.js +3 -0
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +292 -8
- package/dist/plugin/kevin_audit.d.ts +17 -1
- package/dist/plugin/kevin_audit.js +69 -1
- package/dist/plugin/metrics.d.ts +1 -1
- package/dist/plugin/metrics.js +4 -0
- package/dist/plugin/tui-types.d.ts +59 -0
- package/dist/plugin/tui-types.js +4 -0
- package/dist/plugin/tui.d.ts +18 -0
- package/dist/plugin/tui.js +198 -0
- package/package.json +7 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// v1.2.0 (K12-003 / plan §4.2, D12-05) — snapshot flush (pure serialization + atomic write).
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const CAP_BYTES = 512 * 1024;
|
|
5
|
+
const SNAP_FILES = [
|
|
6
|
+
"proposals.json",
|
|
7
|
+
"conflicts.json",
|
|
8
|
+
"health.json",
|
|
9
|
+
"meta.json",
|
|
10
|
+
];
|
|
11
|
+
function byteLen(s) {
|
|
12
|
+
return Buffer.byteLength(s, "utf8");
|
|
13
|
+
}
|
|
14
|
+
function atomicWrite(target, content) {
|
|
15
|
+
const tmp = `${target}.tmp`;
|
|
16
|
+
writeFileSync(tmp, content, "utf8");
|
|
17
|
+
renameSync(tmp, target);
|
|
18
|
+
}
|
|
19
|
+
function truncateProposals(proposals, cap) {
|
|
20
|
+
// Estimate JSON overhead without diffs to compute available budget.
|
|
21
|
+
// We truncate diff fields proportionally to fit cap.
|
|
22
|
+
const serialized = JSON.stringify(proposals);
|
|
23
|
+
if (byteLen(serialized) <= cap)
|
|
24
|
+
return proposals;
|
|
25
|
+
// Compute total diff length
|
|
26
|
+
const totalDiff = proposals.reduce((acc, p) => acc + byteLen(p.diff), 0);
|
|
27
|
+
if (totalDiff === 0)
|
|
28
|
+
return proposals;
|
|
29
|
+
const overhead = byteLen(serialized) - totalDiff;
|
|
30
|
+
const budget = Math.max(0, cap - overhead - 1024); // leave margin
|
|
31
|
+
// Distribute budget proportionally
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = 0; i < proposals.length; i++) {
|
|
34
|
+
const p = proposals[i];
|
|
35
|
+
const diffBytes = byteLen(p.diff);
|
|
36
|
+
const share = Math.floor((diffBytes / totalDiff) * budget);
|
|
37
|
+
// Ensure at least 100 bytes per entry if possible, else share
|
|
38
|
+
const sliceBytes = Math.min(diffBytes, Math.max(share, 100));
|
|
39
|
+
// Slice diff by bytes approximated via string length; diff is ascii mostly
|
|
40
|
+
// so byte length ~ char length. Use char slice.
|
|
41
|
+
const approxChars = Math.floor((sliceBytes / Math.max(diffBytes, 1)) * p.diff.length);
|
|
42
|
+
const truncatedDiff = p.diff.slice(0, Math.max(0, approxChars));
|
|
43
|
+
const needsTrunc = truncatedDiff.length < p.diff.length;
|
|
44
|
+
out.push({
|
|
45
|
+
...p,
|
|
46
|
+
diff: needsTrunc ? `${truncatedDiff}\n…[truncated]` : p.diff,
|
|
47
|
+
truncated: needsTrunc ? true : p.truncated,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// If still over cap after proportional truncation, iteratively trim more
|
|
51
|
+
let result = out;
|
|
52
|
+
let ser = JSON.stringify(result);
|
|
53
|
+
let iter = 0;
|
|
54
|
+
while (byteLen(ser) > cap && iter < 5) {
|
|
55
|
+
result = result.map((p) => {
|
|
56
|
+
if (!p.diff || p.diff.length < 200)
|
|
57
|
+
return p;
|
|
58
|
+
const half = Math.floor(p.diff.length / 2);
|
|
59
|
+
return {
|
|
60
|
+
...p,
|
|
61
|
+
diff: `${p.diff.slice(0, half)}\n…[truncated]`,
|
|
62
|
+
truncated: true,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
ser = JSON.stringify(result);
|
|
66
|
+
iter++;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
export function flushSnapshots(input) {
|
|
71
|
+
const { root, proposals, conflicts, health, metrics, version } = input;
|
|
72
|
+
const dir = join(root, "tui");
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
const generatedAt = new Date().toISOString();
|
|
75
|
+
const written = [];
|
|
76
|
+
const skipped = [];
|
|
77
|
+
// Proposals — cap with truncation
|
|
78
|
+
let propToWrite = proposals;
|
|
79
|
+
let propJson = JSON.stringify(propToWrite, null, 2);
|
|
80
|
+
if (byteLen(propJson) > CAP_BYTES) {
|
|
81
|
+
propToWrite = truncateProposals(proposals, CAP_BYTES - 1024);
|
|
82
|
+
propJson = JSON.stringify(propToWrite, null, 2);
|
|
83
|
+
if (byteLen(propJson) > CAP_BYTES) {
|
|
84
|
+
// Still over after truncation: truncate further by dropping diffs entirely
|
|
85
|
+
const minimal = propToWrite.map((p) => ({
|
|
86
|
+
...p,
|
|
87
|
+
diff: `${p.diff.slice(0, 500)}\n…[truncated]`,
|
|
88
|
+
truncated: true,
|
|
89
|
+
}));
|
|
90
|
+
propJson = JSON.stringify(minimal, null, 2);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
atomicWrite(join(dir, "proposals.json"), propJson);
|
|
94
|
+
written.push("proposals.json");
|
|
95
|
+
// Conflicts
|
|
96
|
+
let conflictsJson = JSON.stringify(conflicts, null, 2);
|
|
97
|
+
if (byteLen(conflictsJson) > CAP_BYTES) {
|
|
98
|
+
// Truncate summaries if needed
|
|
99
|
+
const truncated = conflicts.map((c) => ({
|
|
100
|
+
...c,
|
|
101
|
+
a_summary: c.a_summary.slice(0, 500),
|
|
102
|
+
b_summary: c.b_summary.slice(0, 500),
|
|
103
|
+
}));
|
|
104
|
+
conflictsJson = JSON.stringify(truncated, null, 2);
|
|
105
|
+
}
|
|
106
|
+
atomicWrite(join(dir, "conflicts.json"), conflictsJson);
|
|
107
|
+
written.push("conflicts.json");
|
|
108
|
+
// Health
|
|
109
|
+
let healthJson = JSON.stringify(health, null, 2);
|
|
110
|
+
if (byteLen(healthJson) > CAP_BYTES) {
|
|
111
|
+
// Health should never exceed cap, but truncate counters if it does
|
|
112
|
+
const truncatedHealth = {
|
|
113
|
+
...health,
|
|
114
|
+
counters: {},
|
|
115
|
+
};
|
|
116
|
+
healthJson = JSON.stringify(truncatedHealth, null, 2);
|
|
117
|
+
}
|
|
118
|
+
atomicWrite(join(dir, "health.json"), healthJson);
|
|
119
|
+
written.push("health.json");
|
|
120
|
+
// Meta
|
|
121
|
+
const meta = {
|
|
122
|
+
generatedAt,
|
|
123
|
+
version: version ?? "1.2.0",
|
|
124
|
+
files: SNAP_FILES.slice(0, 3),
|
|
125
|
+
};
|
|
126
|
+
atomicWrite(join(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
127
|
+
written.push("meta.json");
|
|
128
|
+
if (metrics) {
|
|
129
|
+
try {
|
|
130
|
+
metrics.incr("tui_snapshots_flushed");
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// best-effort
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { written, skipped };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Tolerant JSON reader used by tests and (duplicated) by the TUI.
|
|
140
|
+
* Returns {data} on success, {error} on missing/corrupt.
|
|
141
|
+
*/
|
|
142
|
+
export function readJsonSafe(path) {
|
|
143
|
+
try {
|
|
144
|
+
const raw = readFileSync(path, "utf8");
|
|
145
|
+
try {
|
|
146
|
+
return { data: JSON.parse(raw) };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return { error: "corrupt" };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const code = err?.code;
|
|
154
|
+
if (code === "ENOENT")
|
|
155
|
+
return { error: "missing" };
|
|
156
|
+
return { error: "corrupt" };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -11,5 +11,7 @@ export interface Capabilities {
|
|
|
11
11
|
readonly skills: boolean;
|
|
12
12
|
readonly references: boolean;
|
|
13
13
|
readonly apiVersion: string | null;
|
|
14
|
+
/** v1.2.0 (K12-012 / plan D12-03) — additive probe for permission.ask. */
|
|
15
|
+
readonly permissionAsk?: boolean;
|
|
14
16
|
}
|
|
15
17
|
export declare function probe(input: unknown): Capabilities;
|
|
@@ -11,6 +11,7 @@ const ALL_FALSE = {
|
|
|
11
11
|
skills: false,
|
|
12
12
|
references: false,
|
|
13
13
|
apiVersion: null,
|
|
14
|
+
permissionAsk: false,
|
|
14
15
|
};
|
|
15
16
|
function hasCallable(input, domainKey, memberKey) {
|
|
16
17
|
const domain = input[domainKey];
|
|
@@ -27,10 +28,12 @@ export function probe(input) {
|
|
|
27
28
|
const apiVersion = typeof record.apiVersion === "string"
|
|
28
29
|
? record.apiVersion
|
|
29
30
|
: null;
|
|
31
|
+
const permissionAsk = hasCallable(record, "permission", "ask");
|
|
30
32
|
return {
|
|
31
33
|
skills: hasCallable(record, "skill", "source"),
|
|
32
34
|
references: hasCallable(record, "reference", "add"),
|
|
33
35
|
apiVersion,
|
|
36
|
+
permissionAsk,
|
|
34
37
|
};
|
|
35
38
|
}
|
|
36
39
|
catch {
|
package/dist/plugin/contract.js
CHANGED
|
@@ -53,6 +53,9 @@ export const CONTRACT_METRIC_ADDITIONS = [
|
|
|
53
53
|
{ name: "bench_regression_failures", since: "1.1.0" },
|
|
54
54
|
{ name: "forget_requests_total", since: "1.1.0" },
|
|
55
55
|
{ name: "forget_tombstones_published", since: "1.1.0" },
|
|
56
|
+
// v1.2.0 (K12-001 / plan §4, D12-??) — surface metrics (no migration this release)
|
|
57
|
+
{ name: "tui_snapshots_flushed", since: "1.2.0" },
|
|
58
|
+
{ name: "tui_actions_invoked", since: "1.2.0" },
|
|
56
59
|
];
|
|
57
60
|
/**
|
|
58
61
|
* v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -16,10 +16,10 @@ export interface KevinPluginOptions {
|
|
|
16
16
|
* v0.4.0 (K4-021) — settings surfaced by `kevin_config` (plan §8.8).
|
|
17
17
|
* Unknown keys are rejected on `set` unless `strict: false`.
|
|
18
18
|
*/
|
|
19
|
-
export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled"];
|
|
19
|
+
export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled"];
|
|
20
20
|
export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
|
|
21
21
|
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
22
|
-
export declare const KEVIN_VERSION = "1.
|
|
22
|
+
export declare const KEVIN_VERSION = "1.2.0";
|
|
23
23
|
export interface RekeyCounts {
|
|
24
24
|
memories: number;
|
|
25
25
|
shared_entries: number;
|
package/dist/plugin/index.js
CHANGED
|
@@ -6,10 +6,12 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
6
6
|
import { Archiver } from "./Archiver.js";
|
|
7
7
|
import { ArtifactWriter } from "./ArtifactWriter.js";
|
|
8
8
|
import { CausalChain } from "./CausalChain.js";
|
|
9
|
+
import { handleBridgeCommand } from "./ChatBridge.js";
|
|
9
10
|
import { ConflictDetector } from "./ConflictDetector.js";
|
|
10
11
|
import { ContextInjector } from "./ContextInjector.js";
|
|
11
12
|
import { ConventionMiner } from "./ConventionMiner.js";
|
|
12
13
|
import { Curator } from "./Curator.js";
|
|
14
|
+
import { writeDashboard } from "./DashboardHtml.js";
|
|
13
15
|
import { Feedback } from "./Feedback.js";
|
|
14
16
|
import { HookLiveness } from "./HookLiveness.js";
|
|
15
17
|
import { InjectionLedger } from "./InjectionLedger.js";
|
|
@@ -24,8 +26,11 @@ import { Retrospective } from "./Retrospective.js";
|
|
|
24
26
|
import { SharedLayer } from "./SharedLayer.js";
|
|
25
27
|
import { Store } from "./Store.js";
|
|
26
28
|
import { ToolCallObserver } from "./ToolCallObserver.js";
|
|
29
|
+
import { deleteMailbox, processActions, readMailbox, writeResults, } from "./TuiActions.js";
|
|
30
|
+
import { flushSnapshots } from "./TuiSnapshots.js";
|
|
27
31
|
import { probe } from "./capabilities.js";
|
|
28
32
|
import { computeConfidence } from "./confidence.js";
|
|
33
|
+
import { contractDigest, describeContract } from "./contract.js";
|
|
29
34
|
import { probeHost, summarize } from "./host.js";
|
|
30
35
|
import { kevinApprove } from "./kevin_approve.js";
|
|
31
36
|
import { buildAudit } from "./kevin_audit.js";
|
|
@@ -112,6 +117,11 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
112
117
|
"perf_ring_capacity",
|
|
113
118
|
"perf_flush_on_idle",
|
|
114
119
|
"contract_report_enabled",
|
|
120
|
+
// v1.2.0 (K12-001 / plan §4) — the single setting seeded by runtime
|
|
121
|
+
// (no migration this release). Omitting makes `kevin_config set`
|
|
122
|
+
// return { error: "unknown_key" } while `kevin_config list` still
|
|
123
|
+
// shows it.
|
|
124
|
+
"tui_snapshots_enabled",
|
|
115
125
|
];
|
|
116
126
|
// v1.1.0 (K11-007 / D11-08) — no new settings in 1.1.0; thresholds are constants (D11-03)
|
|
117
127
|
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
@@ -122,7 +132,7 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
122
132
|
// behaviour on the next reflection.
|
|
123
133
|
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
124
134
|
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
125
|
-
export const KEVIN_VERSION = "1.
|
|
135
|
+
export const KEVIN_VERSION = "1.2.0";
|
|
126
136
|
function resolveMigrationsDir() {
|
|
127
137
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
128
138
|
return join(here, "..", "migrations");
|
|
@@ -258,6 +268,17 @@ export const KevinPlugin = async (input, options) => {
|
|
|
258
268
|
const store = new Store({ path: dbPath });
|
|
259
269
|
const migrationsDir = opts.migrationsDir ?? resolveMigrationsDir();
|
|
260
270
|
await new Migrate(store, migrationsDir).run();
|
|
271
|
+
// v1.2.0 (K12-001 / D12-??) — no migration this release: ensure the
|
|
272
|
+
// new setting exists with its default so `kevin_config list` shows it
|
|
273
|
+
// on a database that was already at 012.
|
|
274
|
+
try {
|
|
275
|
+
store
|
|
276
|
+
.prepare("INSERT OR IGNORE INTO kevin_settings (key, value) VALUES (?, ?)")
|
|
277
|
+
.run("tui_snapshots_enabled", "1");
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// pre-003 DB without kevin_settings — nothing to seed
|
|
281
|
+
}
|
|
261
282
|
const metrics = new Metrics(store);
|
|
262
283
|
// v0.4.0 (K4-019): the plugin hooks expose no project field, so the
|
|
263
284
|
// project id is derived once from the plugin host's working directory
|
|
@@ -397,6 +418,10 @@ export const KevinPlugin = async (input, options) => {
|
|
|
397
418
|
mkdirSync(join(materializerRoot, "skills"), { recursive: true });
|
|
398
419
|
mkdirSync(join(materializerRoot, "refs"), { recursive: true });
|
|
399
420
|
const capabilities = probe(input);
|
|
421
|
+
// v1.2.0 (K12-012 / D12-03) — permission.ask probe (best-effort, additive).
|
|
422
|
+
// When the host exposes permission.ask, the presence is noted; absence is silent no-op.
|
|
423
|
+
// No new setting — bounded to tui_snapshots_enabled host-support check per spec.
|
|
424
|
+
void capabilities.permissionAsk;
|
|
400
425
|
const materializer = new Materializer(store, { root: materializerRoot });
|
|
401
426
|
// v0.9.0 (K9-016 / plan §5.4, D9-10) — native registration replaces
|
|
402
427
|
// file emission. When attachNative returns a registration for a
|
|
@@ -1675,15 +1700,80 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1675
1700
|
});
|
|
1676
1701
|
},
|
|
1677
1702
|
"chat.message": async (hookInput, output) => {
|
|
1703
|
+
const rawText = output.parts
|
|
1704
|
+
.map((p) => p)
|
|
1705
|
+
.filter((p) => p.type === "text")
|
|
1706
|
+
.map((p) => p.text ?? "")
|
|
1707
|
+
.join(" ");
|
|
1708
|
+
const trimmed = rawText.trim();
|
|
1709
|
+
// v1.2.0 (K12-018 / D12-09) — chat-command bridge BEFORE deriveQuery.
|
|
1710
|
+
// Valid commands are SWALLOWED (parts cleared) and never reach the model.
|
|
1711
|
+
// Must run outside perf.measure so early return actually exits the hook.
|
|
1712
|
+
if (trimmed.length > 0) {
|
|
1713
|
+
try {
|
|
1714
|
+
const bridgeDeps = {
|
|
1715
|
+
getPending: () => {
|
|
1716
|
+
try {
|
|
1717
|
+
const maybe = curator.pending;
|
|
1718
|
+
if (typeof maybe === "function") {
|
|
1719
|
+
const rows = maybe.call(curator);
|
|
1720
|
+
return rows.map((p) => ({
|
|
1721
|
+
id: p.id,
|
|
1722
|
+
proposedText: String(p.proposed_text ?? p.proposedText ?? ""),
|
|
1723
|
+
}));
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
catch { }
|
|
1727
|
+
try {
|
|
1728
|
+
const rows = store
|
|
1729
|
+
.prepare("SELECT id, proposed_text FROM curation_proposals WHERE status = 'pending'")
|
|
1730
|
+
.all();
|
|
1731
|
+
return rows.map((r) => ({
|
|
1732
|
+
id: r.id,
|
|
1733
|
+
proposedText: r.proposed_text,
|
|
1734
|
+
}));
|
|
1735
|
+
}
|
|
1736
|
+
catch {
|
|
1737
|
+
return [];
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
approve: (id) => kevinApprove(store, memoryService, curator, writer, metrics, {
|
|
1741
|
+
proposalId: id,
|
|
1742
|
+
decision: "approve",
|
|
1743
|
+
}),
|
|
1744
|
+
reject: (id, note) => kevinApprove(store, memoryService, curator, writer, metrics, {
|
|
1745
|
+
proposalId: id,
|
|
1746
|
+
decision: "reject",
|
|
1747
|
+
}) && void note,
|
|
1748
|
+
acknowledge: (conflictId) => {
|
|
1749
|
+
try {
|
|
1750
|
+
const cd = conflictDetector;
|
|
1751
|
+
if (typeof cd.acknowledge === "function")
|
|
1752
|
+
cd.acknowledge(conflictId);
|
|
1753
|
+
else if (typeof cd.resolve === "function")
|
|
1754
|
+
cd.resolve(conflictId, "a");
|
|
1755
|
+
}
|
|
1756
|
+
catch { }
|
|
1757
|
+
},
|
|
1758
|
+
metrics,
|
|
1759
|
+
};
|
|
1760
|
+
const br = handleBridgeCommand(trimmed, bridgeDeps);
|
|
1761
|
+
if (br.handled) {
|
|
1762
|
+
try {
|
|
1763
|
+
output.parts = [];
|
|
1764
|
+
}
|
|
1765
|
+
catch { }
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
catch {
|
|
1770
|
+
// best-effort — bridge failure must not break chat flow
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1678
1773
|
perf.measure("chat.message", () => {
|
|
1679
|
-
|
|
1680
|
-
.map((p) => p)
|
|
1681
|
-
.filter((p) => p.type === "text")
|
|
1682
|
-
.map((p) => p.text ?? "")
|
|
1683
|
-
.join(" ");
|
|
1684
|
-
if (text.trim()) {
|
|
1774
|
+
if (trimmed.length > 0) {
|
|
1685
1775
|
const derived = injector.deriveQuery([
|
|
1686
|
-
{ role: "user", content:
|
|
1776
|
+
{ role: "user", content: rawText },
|
|
1687
1777
|
]);
|
|
1688
1778
|
lastUserQuery = derived.length > 0 ? derived : null;
|
|
1689
1779
|
if (hookInput.sessionID && lastUserQuery) {
|
|
@@ -1809,6 +1899,61 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1809
1899
|
// (legacy DBs pre-005 lack the recurrence_count
|
|
1810
1900
|
// column)
|
|
1811
1901
|
.catch(() => { }));
|
|
1902
|
+
// v1.2.0 (K12-007/K12-011 / D12-05) — TUI mailbox: actions→curate ordering.
|
|
1903
|
+
// Process mailbox BEFORE curator.propose so a fresh proposal created this idle
|
|
1904
|
+
// is NOT visible to a stale token (D12-04). Best-effort, never breaks idle.
|
|
1905
|
+
try {
|
|
1906
|
+
const mb = readMailbox(materializerRoot);
|
|
1907
|
+
if (mb.actions.length) {
|
|
1908
|
+
const tuiDeps = {
|
|
1909
|
+
getPending: () => {
|
|
1910
|
+
try {
|
|
1911
|
+
const maybe = curator.pending;
|
|
1912
|
+
if (typeof maybe === "function") {
|
|
1913
|
+
const rows = maybe.call(curator);
|
|
1914
|
+
return rows.map((p) => ({
|
|
1915
|
+
id: p.id,
|
|
1916
|
+
proposedText: String(p.proposed_text ?? p.proposedText ?? ""),
|
|
1917
|
+
}));
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
catch { }
|
|
1921
|
+
try {
|
|
1922
|
+
const rows = store
|
|
1923
|
+
.prepare("SELECT id, proposed_text FROM curation_proposals WHERE status = 'pending'")
|
|
1924
|
+
.all();
|
|
1925
|
+
return rows.map((r) => ({
|
|
1926
|
+
id: r.id,
|
|
1927
|
+
proposedText: r.proposed_text,
|
|
1928
|
+
}));
|
|
1929
|
+
}
|
|
1930
|
+
catch {
|
|
1931
|
+
return [];
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
approve: (id) => kevinApprove(store, memoryService, curator, writer, metrics, {
|
|
1935
|
+
proposalId: id,
|
|
1936
|
+
decision: "approve",
|
|
1937
|
+
}),
|
|
1938
|
+
reject: (id, _note) => kevinApprove(store, memoryService, curator, writer, metrics, {
|
|
1939
|
+
proposalId: id,
|
|
1940
|
+
decision: "reject",
|
|
1941
|
+
}),
|
|
1942
|
+
acknowledge: (conflictId) => {
|
|
1943
|
+
const cd = conflictDetector;
|
|
1944
|
+
if (typeof cd.acknowledge === "function")
|
|
1945
|
+
cd.acknowledge(conflictId);
|
|
1946
|
+
},
|
|
1947
|
+
metrics,
|
|
1948
|
+
};
|
|
1949
|
+
const tuiResults = processActions(mb.actions, tuiDeps);
|
|
1950
|
+
writeResults(materializerRoot, tuiResults);
|
|
1951
|
+
deleteMailbox(materializerRoot);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
catch {
|
|
1955
|
+
// best-effort
|
|
1956
|
+
}
|
|
1812
1957
|
// v0.6.0 (K6-015 / plan §8.14) — session-idle curation
|
|
1813
1958
|
// generation. Dry-run only: `propose()` calls `plan()`
|
|
1814
1959
|
// and never `apply()`, so nothing here can touch disk
|
|
@@ -1850,6 +1995,145 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1850
1995
|
// best-effort, same pattern as ledger.settle — a
|
|
1851
1996
|
// sync failure must not break the idle path
|
|
1852
1997
|
}
|
|
1998
|
+
// v1.2.0 (K12-003/K12-011/K12-017 / D12-05) — snapshot flush gated by tui_snapshots_enabled.
|
|
1999
|
+
// Order: actions→curate→syncSharedLayer→snapshots (D12-05) — snapshots reflect post-action truth.
|
|
2000
|
+
try {
|
|
2001
|
+
if (memoryService.getSetting("tui_snapshots_enabled", "1") === "1") {
|
|
2002
|
+
// Proposals
|
|
2003
|
+
let proposals = [];
|
|
2004
|
+
try {
|
|
2005
|
+
const rawPending = (() => {
|
|
2006
|
+
try {
|
|
2007
|
+
const maybe = curator.pending;
|
|
2008
|
+
if (typeof maybe === "function")
|
|
2009
|
+
return maybe.call(curator);
|
|
2010
|
+
}
|
|
2011
|
+
catch { }
|
|
2012
|
+
return store
|
|
2013
|
+
.prepare("SELECT id, kind, target_path, proposed_text, diff, memory_id, created_at FROM curation_proposals WHERE status = 'pending' ORDER BY created_at")
|
|
2014
|
+
.all();
|
|
2015
|
+
})();
|
|
2016
|
+
const { proposalToken } = await import("./TuiActions.js");
|
|
2017
|
+
proposals = rawPending.map((r) => {
|
|
2018
|
+
const row = r;
|
|
2019
|
+
const id = String(row.id ?? "");
|
|
2020
|
+
const proposed = String(row.proposed_text ?? row.proposedText ?? "");
|
|
2021
|
+
return {
|
|
2022
|
+
id,
|
|
2023
|
+
kind: String(row.kind ?? "agents_md"),
|
|
2024
|
+
target_path: String(row.target_path ?? row.targetPath ?? "AGENTS.md"),
|
|
2025
|
+
diff: String(row.diff ?? ""),
|
|
2026
|
+
memory_ids: String(row.memory_id ?? row.memoryIds ?? "")
|
|
2027
|
+
.split(",")
|
|
2028
|
+
.filter((s) => s.length > 0),
|
|
2029
|
+
created_at: String(row.created_at ??
|
|
2030
|
+
row.createdAt ??
|
|
2031
|
+
new Date().toISOString()),
|
|
2032
|
+
token: proposalToken(id, proposed),
|
|
2033
|
+
};
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
catch { }
|
|
2037
|
+
// Conflicts
|
|
2038
|
+
let conflicts = [];
|
|
2039
|
+
try {
|
|
2040
|
+
const maybe = conflictDetector.openConflicts;
|
|
2041
|
+
if (typeof maybe === "function") {
|
|
2042
|
+
const raw = maybe.call(conflictDetector);
|
|
2043
|
+
conflicts = raw.map((c) => {
|
|
2044
|
+
const row = c;
|
|
2045
|
+
return {
|
|
2046
|
+
id: String(row.id ?? ""),
|
|
2047
|
+
kind: String(row.kind ?? ""),
|
|
2048
|
+
a_summary: String(row.a_summary ?? row.aSummary ?? ""),
|
|
2049
|
+
b_summary: String(row.b_summary ?? row.bSummary ?? ""),
|
|
2050
|
+
opened_at: String(row.opened_at ??
|
|
2051
|
+
row.openedAt ??
|
|
2052
|
+
new Date().toISOString()),
|
|
2053
|
+
};
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
catch {
|
|
2058
|
+
conflicts = [];
|
|
2059
|
+
}
|
|
2060
|
+
// Health
|
|
2061
|
+
let health;
|
|
2062
|
+
try {
|
|
2063
|
+
const doc = buildDoctor(store, host, memoryService);
|
|
2064
|
+
let perfRows = [];
|
|
2065
|
+
try {
|
|
2066
|
+
const stats = perf.stats?.();
|
|
2067
|
+
if (Array.isArray(stats)) {
|
|
2068
|
+
perfRows = stats.map((p) => {
|
|
2069
|
+
const row = p;
|
|
2070
|
+
const budget = row.budget;
|
|
2071
|
+
return {
|
|
2072
|
+
scope: String(row.scope ?? ""),
|
|
2073
|
+
p95: Number(row.p95 ?? 0),
|
|
2074
|
+
budget_p95: Number(budget?.p95Ms ??
|
|
2075
|
+
row.budget_p95 ??
|
|
2076
|
+
row.budgetP95 ??
|
|
2077
|
+
0),
|
|
2078
|
+
within_budget: Boolean(row.withinBudget ?? row.within_budget ?? true),
|
|
2079
|
+
};
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
catch {
|
|
2084
|
+
perfRows = [];
|
|
2085
|
+
}
|
|
2086
|
+
let digest = "unknown";
|
|
2087
|
+
try {
|
|
2088
|
+
digest = contractDigest(describeContract());
|
|
2089
|
+
}
|
|
2090
|
+
catch { }
|
|
2091
|
+
health = {
|
|
2092
|
+
verdict: doc.verdict,
|
|
2093
|
+
reason: doc.reason,
|
|
2094
|
+
hooks: doc.hooks.map((h) => ({
|
|
2095
|
+
hook: h.hook,
|
|
2096
|
+
state: h.state,
|
|
2097
|
+
fire_count: h.fire_count,
|
|
2098
|
+
expected_count: h.expected_count,
|
|
2099
|
+
})),
|
|
2100
|
+
perf: perfRows,
|
|
2101
|
+
contract_digest: digest,
|
|
2102
|
+
counters: metrics.snapshot(),
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
catch {
|
|
2106
|
+
health = {
|
|
2107
|
+
verdict: "unknown",
|
|
2108
|
+
reason: "health unavailable",
|
|
2109
|
+
hooks: [],
|
|
2110
|
+
perf: [],
|
|
2111
|
+
contract_digest: "unknown",
|
|
2112
|
+
counters: {},
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
flushSnapshots({
|
|
2116
|
+
root: materializerRoot,
|
|
2117
|
+
proposals,
|
|
2118
|
+
conflicts,
|
|
2119
|
+
health,
|
|
2120
|
+
metrics,
|
|
2121
|
+
version: KEVIN_VERSION,
|
|
2122
|
+
});
|
|
2123
|
+
try {
|
|
2124
|
+
writeDashboard(materializerRoot, {
|
|
2125
|
+
generatedAt: new Date().toISOString(),
|
|
2126
|
+
proposals,
|
|
2127
|
+
conflicts,
|
|
2128
|
+
health,
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
catch { }
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
catch {
|
|
2135
|
+
// best-effort — snapshot failure must not break idle
|
|
2136
|
+
}
|
|
1853
2137
|
// v1.0.0 (K10-013 / D10-08) — the session recorded work:
|
|
1854
2138
|
// arm the deferred dispose settlement. The ISO timestamp
|
|
1855
2139
|
// lets the next session.start compare it against
|
|
@@ -153,6 +153,22 @@ export interface AuditReport {
|
|
|
153
153
|
clause_count: number;
|
|
154
154
|
deprecated_count: number;
|
|
155
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
* v1.2.0 (K12-014 / D12-??) — TUI surface block. Read-only introspection:
|
|
158
|
+
* enabled_setting is the kevin_settings value, last_flush_age_s is seconds
|
|
159
|
+
* since meta.json generatedAt (null when absent/corrupt), mailbox_depth is
|
|
160
|
+
* queue length (null when file missing), last_results is the parsed
|
|
161
|
+
* results.json array (null when absent). Dashboard age and bridge counters
|
|
162
|
+
* added in K12-019.
|
|
163
|
+
*/
|
|
164
|
+
tui?: {
|
|
165
|
+
enabled_setting: string;
|
|
166
|
+
last_flush_age_s: number | null;
|
|
167
|
+
mailbox_depth: number | null;
|
|
168
|
+
last_results: unknown | null;
|
|
169
|
+
dashboard_last_write_age_s: number | null;
|
|
170
|
+
bridge_interceptions: number;
|
|
171
|
+
};
|
|
156
172
|
partial: boolean;
|
|
157
173
|
}
|
|
158
174
|
/**
|
|
@@ -210,4 +226,4 @@ export interface TruthReport {
|
|
|
210
226
|
is_truncated: false;
|
|
211
227
|
};
|
|
212
228
|
}
|
|
213
|
-
export declare function buildAudit(store: Store, metrics: Metrics, capabilities?: Capabilities, projectId?: string, repoId?: string | null): AuditReport;
|
|
229
|
+
export declare function buildAudit(store: Store, metrics: Metrics, capabilities?: Capabilities, projectId?: string, repoId?: string | null, tuiRoot?: string): AuditReport;
|