@jmtrin/kevin-core 1.3.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.
Files changed (145) hide show
  1. package/dist/Archiver.d.ts +42 -0
  2. package/dist/Archiver.js +83 -0
  3. package/dist/ArtifactWriter.d.ts +93 -0
  4. package/dist/ArtifactWriter.js +291 -0
  5. package/dist/CausalChain.d.ts +22 -0
  6. package/dist/CausalChain.js +197 -0
  7. package/dist/ChatBridge.d.ts +41 -0
  8. package/dist/ChatBridge.js +103 -0
  9. package/dist/ConflictDetector.d.ts +35 -0
  10. package/dist/ConflictDetector.js +260 -0
  11. package/dist/ContextInjector.d.ts +222 -0
  12. package/dist/ContextInjector.js +533 -0
  13. package/dist/ConventionMiner.d.ts +35 -0
  14. package/dist/ConventionMiner.js +242 -0
  15. package/dist/Curator.d.ts +118 -0
  16. package/dist/Curator.js +326 -0
  17. package/dist/DashboardHtml.d.ts +5 -0
  18. package/dist/DashboardHtml.js +180 -0
  19. package/dist/Feedback.d.ts +67 -0
  20. package/dist/Feedback.js +120 -0
  21. package/dist/HookLiveness.d.ts +88 -0
  22. package/dist/HookLiveness.js +322 -0
  23. package/dist/InjectionLedger.d.ts +98 -0
  24. package/dist/InjectionLedger.js +295 -0
  25. package/dist/LessonFixer.d.ts +44 -0
  26. package/dist/LessonFixer.js +45 -0
  27. package/dist/Materializer.d.ts +84 -0
  28. package/dist/Materializer.js +181 -0
  29. package/dist/MemoryService.d.ts +360 -0
  30. package/dist/MemoryService.js +1242 -0
  31. package/dist/Migrate.d.ts +24 -0
  32. package/dist/Migrate.js +285 -0
  33. package/dist/PatternMiner.d.ts +49 -0
  34. package/dist/PatternMiner.js +132 -0
  35. package/dist/QualityGate.d.ts +124 -0
  36. package/dist/QualityGate.js +119 -0
  37. package/dist/Reflector.d.ts +91 -0
  38. package/dist/Reflector.js +307 -0
  39. package/dist/RepoIdentity.d.ts +124 -0
  40. package/dist/RepoIdentity.js +300 -0
  41. package/dist/RepoTruth.d.ts +80 -0
  42. package/dist/RepoTruth.js +599 -0
  43. package/dist/Retrospective.d.ts +18 -0
  44. package/dist/Retrospective.js +216 -0
  45. package/dist/SharedLayer.d.ts +159 -0
  46. package/dist/SharedLayer.js +462 -0
  47. package/dist/Store.d.ts +29 -0
  48. package/dist/Store.js +50 -0
  49. package/dist/ToolCallObserver.d.ts +32 -0
  50. package/dist/ToolCallObserver.js +201 -0
  51. package/dist/TuiActions.d.ts +43 -0
  52. package/dist/TuiActions.js +181 -0
  53. package/dist/TuiSnapshots.d.ts +24 -0
  54. package/dist/TuiSnapshots.js +159 -0
  55. package/dist/capabilities.d.ts +6 -0
  56. package/dist/capabilities.js +1 -0
  57. package/dist/columns.d.ts +11 -0
  58. package/dist/columns.js +54 -0
  59. package/dist/confidence.d.ts +8 -0
  60. package/dist/confidence.js +34 -0
  61. package/dist/contract.d.ts +68 -0
  62. package/dist/contract.js +430 -0
  63. package/dist/diff.d.ts +8 -0
  64. package/dist/diff.js +182 -0
  65. package/dist/env.d.ts +5 -0
  66. package/dist/env.js +12 -0
  67. package/dist/escape.d.ts +39 -0
  68. package/dist/escape.js +60 -0
  69. package/dist/fingerprint.d.ts +27 -0
  70. package/dist/fingerprint.js +73 -0
  71. package/dist/host.d.ts +18 -0
  72. package/dist/host.js +7 -0
  73. package/dist/idle-pipeline.d.ts +9 -0
  74. package/dist/idle-pipeline.js +44 -0
  75. package/dist/index.d.ts +72 -0
  76. package/dist/index.js +109 -0
  77. package/dist/inferability.d.ts +32 -0
  78. package/dist/inferability.js +88 -0
  79. package/dist/kevin_approve.d.ts +34 -0
  80. package/dist/kevin_approve.js +52 -0
  81. package/dist/kevin_audit.d.ts +230 -0
  82. package/dist/kevin_audit.js +631 -0
  83. package/dist/kevin_bench.d.ts +11 -0
  84. package/dist/kevin_bench.js +83 -0
  85. package/dist/kevin_conflicts.d.ts +9 -0
  86. package/dist/kevin_conflicts.js +50 -0
  87. package/dist/kevin_contract.d.ts +8 -0
  88. package/dist/kevin_contract.js +59 -0
  89. package/dist/kevin_doctor.d.ts +63 -0
  90. package/dist/kevin_doctor.js +197 -0
  91. package/dist/kevin_facts.d.ts +42 -0
  92. package/dist/kevin_facts.js +36 -0
  93. package/dist/kevin_forget.d.ts +33 -0
  94. package/dist/kevin_forget.js +260 -0
  95. package/dist/kevin_native.d.ts +29 -0
  96. package/dist/kevin_native.js +79 -0
  97. package/dist/kevin_propose.d.ts +23 -0
  98. package/dist/kevin_propose.js +14 -0
  99. package/dist/kevin_publish.d.ts +38 -0
  100. package/dist/kevin_publish.js +18 -0
  101. package/dist/kevin_why.d.ts +23 -0
  102. package/dist/kevin_why.js +111 -0
  103. package/dist/memory-format.d.ts +20 -0
  104. package/dist/memory-format.js +59 -0
  105. package/dist/metrics.d.ts +97 -0
  106. package/dist/metrics.js +288 -0
  107. package/dist/migrations/001_initial.sql +92 -0
  108. package/dist/migrations/002_indexes.sql +14 -0
  109. package/dist/migrations/003_v02_signal.sql +58 -0
  110. package/dist/migrations/004_v03_knowledge.sql +138 -0
  111. package/dist/migrations/005_v04_signal.sql +57 -0
  112. package/dist/migrations/006_v05_glassbox.sql +118 -0
  113. package/dist/migrations/007_v06_pull.sql +145 -0
  114. package/dist/migrations/008_v07_truth.sql +124 -0
  115. package/dist/migrations/009_v08_team.sql +100 -0
  116. package/dist/migrations/010_v09_native.sql +78 -0
  117. package/dist/migrations/011_v10_proven.sql +69 -0
  118. package/dist/migrations/012_v11_drift.sql +24 -0
  119. package/dist/native.d.ts +23 -0
  120. package/dist/native.js +1 -0
  121. package/dist/okf-export.d.ts +3 -0
  122. package/dist/okf-export.js +134 -0
  123. package/dist/okf-import.d.ts +76 -0
  124. package/dist/okf-import.js +271 -0
  125. package/dist/okf.d.ts +107 -0
  126. package/dist/okf.js +303 -0
  127. package/dist/perf.d.ts +35 -0
  128. package/dist/perf.js +174 -0
  129. package/dist/query-tokenizer.d.ts +13 -0
  130. package/dist/query-tokenizer.js +133 -0
  131. package/dist/redact.d.ts +2 -0
  132. package/dist/redact.js +18 -0
  133. package/dist/replay-types.d.ts +69 -0
  134. package/dist/replay-types.js +156 -0
  135. package/dist/replay.d.ts +44 -0
  136. package/dist/replay.js +215 -0
  137. package/dist/sqlite-adapter.d.ts +12 -0
  138. package/dist/sqlite-adapter.js +78 -0
  139. package/dist/time-ms.d.ts +1 -0
  140. package/dist/time-ms.js +16 -0
  141. package/dist/tui-types.d.ts +59 -0
  142. package/dist/tui-types.js +4 -0
  143. package/dist/uuid.d.ts +1 -0
  144. package/dist/uuid.js +50 -0
  145. package/package.json +28 -0
@@ -0,0 +1,201 @@
1
+ import { hasColumn } from "./columns.js";
2
+ import { fingerprint as computeFingerprint } from "./fingerprint.js";
3
+ import { redactPaths, stripPrivate } from "./redact.js";
4
+ import { uuidv7 } from "./uuid.js";
5
+ const SECRET_PATTERNS = [
6
+ /\b(API_KEY|SECRET|PASSWORD|TOKEN)\b\s*[=:]\s*\S+/gi,
7
+ /\bBearer\s+\S+/gi,
8
+ // BUG-013 — the old `\btoken\s+\S+/gi` mangled harmless phrasing
9
+ // ("token budget", "token count", "max token limit") that LLM tools
10
+ // emit routinely. Narrowed to credential contexts: named token
11
+ // variables WITH an assignment, and `token=<value>` / `token:
12
+ // <value>` assignments.
13
+ /\b(access_?token|auth_?token|api_?token)\b\s*[=:]\s*\S+/gi,
14
+ /\btoken\s*[=:]\s*\S+/gi,
15
+ ];
16
+ export class ToolCallObserver {
17
+ store;
18
+ startTs = new Map();
19
+ metrics;
20
+ constructor(store, metrics) {
21
+ this.store = store;
22
+ this.metrics = metrics ?? null;
23
+ }
24
+ onBefore(input, _output) {
25
+ const key = this.key(input);
26
+ this.startTs.set(key, Date.now());
27
+ }
28
+ onAfter(input, output) {
29
+ const key = this.key(input);
30
+ const start = this.startTs.get(key) ?? Date.now();
31
+ this.startTs.delete(key);
32
+ const durationMs = Math.max(0, Date.now() - start);
33
+ const sessionId = input.sessionId ?? "unknown";
34
+ const argsSummary = this.summarizeArgs(input.args ?? {});
35
+ const success = output.success === true ? 1 : 0;
36
+ const agent = input.agent ?? null;
37
+ const stderr = stripPrivate(output.stderr ?? "");
38
+ const stdout = stripPrivate(output.stdout ?? "");
39
+ const errorType = output.success === false
40
+ ? this.inferErrorType(stderr, stdout, output.exitCode)
41
+ : null;
42
+ const metadata = JSON.stringify(this.redactArgs(input.args ?? {}));
43
+ const projectId = input.projectId ?? null;
44
+ const fp = computeFingerprint(`${input.tool}|${argsSummary}|${success}`, projectId ?? undefined);
45
+ if (this.isDedupEnabled()) {
46
+ const existing = this.store
47
+ .prepare(`SELECT 1 FROM tool_calls
48
+ WHERE fingerprint = ?
49
+ AND (project_id IS ? OR (project_id IS NULL AND ? IS NULL))
50
+ AND strftime('%Y-%m-%d %H:%M', ts) = strftime('%Y-%m-%d %H:%M', 'now')
51
+ LIMIT 1`)
52
+ .get(fp, projectId, projectId);
53
+ if (existing) {
54
+ this.metrics?.incr("tool_calls_deduped", 1);
55
+ return;
56
+ }
57
+ }
58
+ // v1.1.0 (K11-002 / plan §5.2, D11-01/D11-07) — dual-write: legacy `ts`
59
+ // stays (`datetime('now')`), new `ts_ms` holds Date.now() when the
60
+ // column exists. Probe is cached via columns registry (K11-011).
61
+ if (hasColumn(this.store, "tool_calls", "ts_ms")) {
62
+ this.store
63
+ .prepare(`INSERT INTO tool_calls
64
+ (id, session_id, ts, ts_ms, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
65
+ VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
66
+ .run(input.callID ?? uuidv7(), sessionId, Date.now(), input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
67
+ }
68
+ else {
69
+ this.store
70
+ .prepare(`INSERT INTO tool_calls
71
+ (id, session_id, ts, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
72
+ VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
73
+ .run(input.callID ?? uuidv7(), sessionId, input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
74
+ }
75
+ }
76
+ isDedupEnabled() {
77
+ try {
78
+ const row = this.store
79
+ .prepare("SELECT value FROM kevin_settings WHERE key = 'tool_calls_dedup_enabled'")
80
+ .get();
81
+ return row?.value === "1";
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ redactSecrets(text) {
88
+ let out = text;
89
+ for (const pat of SECRET_PATTERNS) {
90
+ out = out.replace(pat, (match) => {
91
+ // BUG-013 — preserve the original separator so
92
+ // `auth_token = abc123` keeps its spacing: only the
93
+ // value is replaced.
94
+ const m = match.match(/^(.+?)(\s*[=:]\s*)(\S+)$/);
95
+ if (m) {
96
+ return `${m[1]}${m[2]}<redacted>`;
97
+ }
98
+ const parts = match.split(/\s+/);
99
+ return `${parts[0]} <redacted>`;
100
+ });
101
+ }
102
+ return out;
103
+ }
104
+ summarizeArgs(args) {
105
+ const interestingKeys = [
106
+ "filePath",
107
+ "path",
108
+ "cwd",
109
+ "command",
110
+ "cmd",
111
+ "file",
112
+ "directory",
113
+ ];
114
+ const parts = [];
115
+ for (const [k, v] of Object.entries(args)) {
116
+ if (interestingKeys.includes(k)) {
117
+ parts.push(`${k}: ${this.redactValue(v)}`);
118
+ }
119
+ else if (this.looksSecret(k)) {
120
+ parts.push(`${k}: <redacted>`);
121
+ }
122
+ else {
123
+ parts.push(`${k}: ${this.redactValue(v, 200)}`);
124
+ }
125
+ }
126
+ let summary = parts.join(", ");
127
+ if (summary.length > 500)
128
+ summary = `${summary.slice(0, 500)}...`;
129
+ return summary;
130
+ }
131
+ inferErrorType(stderr, stdout, exitCode) {
132
+ const combined = `${stderr}\n${stdout}`.toLowerCase();
133
+ if (exitCode === 124 ||
134
+ /timed out|timeout|etimedout|\bkilled\b|sigterm|sigkill/.test(combined)) {
135
+ return "timeout";
136
+ }
137
+ if (/error ts|tsc|\btypescript\b/.test(combined))
138
+ return "typecheck";
139
+ if (/\b(lint|biome|eslint)\b/.test(combined))
140
+ return "lint";
141
+ if (/\b(fail|vitest|jest|test failed)\b/.test(combined))
142
+ return "test";
143
+ if (/error:|typeerror|referenceerror|syntaxerror/.test(combined))
144
+ return "runtime";
145
+ if ((exitCode === undefined || exitCode === -1) &&
146
+ stderr.trim() === "" &&
147
+ stdout.trim() === "") {
148
+ return "timeout";
149
+ }
150
+ return "unknown";
151
+ }
152
+ key(input) {
153
+ if (input.callID)
154
+ return `callID::${input.callID}`;
155
+ const argsKey = JSON.stringify(input.args ?? {});
156
+ return `${input.sessionId ?? "unknown"}::${input.tool}::${argsKey}`;
157
+ }
158
+ looksSecret(key) {
159
+ const k = key.toLowerCase();
160
+ return /(key|secret|password|token|bearer)/.test(k);
161
+ }
162
+ redactArgs(args) {
163
+ const out = {};
164
+ for (const [k, v] of Object.entries(args)) {
165
+ if (this.looksSecret(k)) {
166
+ out[k] = "<redacted>";
167
+ }
168
+ else {
169
+ out[k] = this.redactValue(v);
170
+ }
171
+ }
172
+ return out;
173
+ }
174
+ redactValue(v, truncateAt) {
175
+ if (typeof v === "string") {
176
+ let s = stripPrivate(v);
177
+ s = redactPaths(s);
178
+ s = this.redactSecrets(s);
179
+ if (truncateAt !== undefined && s.length > truncateAt) {
180
+ s = `${s.slice(0, truncateAt)}...`;
181
+ }
182
+ return s;
183
+ }
184
+ if (Array.isArray(v)) {
185
+ return v.map((item) => this.redactValue(item));
186
+ }
187
+ if (v && typeof v === "object") {
188
+ const obj = {};
189
+ for (const [k, val] of Object.entries(v)) {
190
+ if (this.looksSecret(k)) {
191
+ obj[k] = "<redacted>";
192
+ }
193
+ else {
194
+ obj[k] = this.redactValue(val);
195
+ }
196
+ }
197
+ return obj;
198
+ }
199
+ return v;
200
+ }
201
+ }
@@ -0,0 +1,43 @@
1
+ import type { ActionResult, TuiAction } from "./tui-types.js";
2
+ export interface MailboxReadResult {
3
+ readonly actions: readonly TuiAction[];
4
+ readonly warnings: readonly string[];
5
+ }
6
+ /**
7
+ * Read `join(root,"tui","actions.json")` tolerant.
8
+ * - missing file → {actions:[], warnings:[]}
9
+ * - malformed JSON → {actions:[], warnings:["malformed_json"]}
10
+ * - non-object or missing/non-array actions → {actions:[], warnings:["invalid_shape"]}
11
+ * - unknown type values are dropped with warning per entry
12
+ * Never deletes the file here.
13
+ */
14
+ export declare function readMailbox(root: string): MailboxReadResult;
15
+ export declare function proposalToken(proposalId: string, proposedText: string): string;
16
+ export interface PendingProposal {
17
+ readonly id: string;
18
+ readonly proposedText: string;
19
+ }
20
+ export declare function verifyFresh(action: TuiAction, currentPending: readonly PendingProposal[]): {
21
+ ok: true;
22
+ } | {
23
+ ok: false;
24
+ reason: string;
25
+ };
26
+ export type ActionStatus = ActionResult["status"];
27
+ export interface ProcessDeps {
28
+ readonly getPending: () => readonly PendingProposal[];
29
+ readonly approve: (proposalId: string) => unknown;
30
+ readonly reject: (proposalId: string, note?: string) => unknown;
31
+ readonly acknowledge: (conflictId: string) => unknown;
32
+ readonly metrics?: {
33
+ incr: (key: "tui_actions_invoked", by?: number) => void;
34
+ } | null;
35
+ }
36
+ export declare function processActions(actions: readonly TuiAction[], deps: ProcessDeps): ActionResult[];
37
+ export declare function writeResults(root: string, results: readonly ActionResult[]): void;
38
+ export declare function deleteMailbox(root: string): void;
39
+ /**
40
+ * Convenience: read → process → write results → delete queue.
41
+ * Returns results (empty if no actions). Mirrors idle-chain usage.
42
+ */
43
+ export declare function consumeMailbox(root: string, deps: ProcessDeps): ActionResult[];
@@ -0,0 +1,181 @@
1
+ // v1.2.0 (K12-005 / plan §4.3) — mailbox tolerant parser (phase F1).
2
+ // v1.2.0 (K12-006 / plan §4.3, D12-04) — token scheme + stale detection
3
+ import { createHash } from "node:crypto";
4
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
5
+ import { join } from "node:path";
6
+ function isRecord(v) {
7
+ return typeof v === "object" && v !== null && !Array.isArray(v);
8
+ }
9
+ function isValidAction(raw) {
10
+ if (!isRecord(raw))
11
+ return null;
12
+ const t = raw.type;
13
+ if (t === "approve") {
14
+ if (typeof raw.proposalId === "string" && typeof raw.token === "string") {
15
+ return { type: "approve", proposalId: raw.proposalId, token: raw.token };
16
+ }
17
+ return null;
18
+ }
19
+ if (t === "reject") {
20
+ if (typeof raw.proposalId === "string" && typeof raw.token === "string") {
21
+ const note = typeof raw.note === "string" ? raw.note : undefined;
22
+ return note !== undefined
23
+ ? { type: "reject", proposalId: raw.proposalId, token: raw.token, note }
24
+ : { type: "reject", proposalId: raw.proposalId, token: raw.token };
25
+ }
26
+ return null;
27
+ }
28
+ if (t === "acknowledge") {
29
+ if (typeof raw.conflictId === "string") {
30
+ return { type: "acknowledge", conflictId: raw.conflictId };
31
+ }
32
+ return null;
33
+ }
34
+ return null;
35
+ }
36
+ /**
37
+ * Read `join(root,"tui","actions.json")` tolerant.
38
+ * - missing file → {actions:[], warnings:[]}
39
+ * - malformed JSON → {actions:[], warnings:["malformed_json"]}
40
+ * - non-object or missing/non-array actions → {actions:[], warnings:["invalid_shape"]}
41
+ * - unknown type values are dropped with warning per entry
42
+ * Never deletes the file here.
43
+ */
44
+ export function readMailbox(root) {
45
+ const path = join(root, "tui", "actions.json");
46
+ let raw;
47
+ try {
48
+ raw = readFileSync(path, "utf8");
49
+ }
50
+ catch (err) {
51
+ const code = err?.code;
52
+ if (code === "ENOENT")
53
+ return { actions: [], warnings: [] };
54
+ return { actions: [], warnings: ["read_error"] };
55
+ }
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ }
60
+ catch {
61
+ return { actions: [], warnings: ["malformed_json"] };
62
+ }
63
+ if (!isRecord(parsed)) {
64
+ return { actions: [], warnings: ["invalid_shape"] };
65
+ }
66
+ const maybeActions = parsed.actions;
67
+ if (!Array.isArray(maybeActions)) {
68
+ return { actions: [], warnings: ["invalid_shape"] };
69
+ }
70
+ const actions = [];
71
+ const warnings = [];
72
+ for (const entry of maybeActions) {
73
+ if (!isRecord(entry)) {
74
+ warnings.push("dropped_invalid_entry");
75
+ continue;
76
+ }
77
+ const type = entry.type;
78
+ if (type !== "approve" && type !== "reject" && type !== "acknowledge") {
79
+ warnings.push(`dropped_unknown_type:${String(type)}`);
80
+ continue;
81
+ }
82
+ const valid = isValidAction(entry);
83
+ if (!valid) {
84
+ warnings.push(`dropped_invalid_${String(type)}`);
85
+ continue;
86
+ }
87
+ actions.push(valid);
88
+ }
89
+ return { actions, warnings };
90
+ }
91
+ // v1.2.0 (K12-006 / D12-04) — first 16 hex of SHA-256(proposalId + "\0" + proposedText)
92
+ export function proposalToken(proposalId, proposedText) {
93
+ return createHash("sha256")
94
+ .update(`${proposalId}\0${proposedText}`, "utf8")
95
+ .digest("hex")
96
+ .slice(0, 16);
97
+ }
98
+ export function verifyFresh(action, currentPending) {
99
+ if (action.type === "acknowledge")
100
+ return { ok: true };
101
+ const pending = currentPending.find((p) => p.id === action.proposalId);
102
+ if (!pending) {
103
+ return { ok: false, reason: "content_changed_or_absent" };
104
+ }
105
+ const expected = proposalToken(pending.id, pending.proposedText);
106
+ if (expected !== action.token) {
107
+ return { ok: false, reason: "content_changed_or_absent" };
108
+ }
109
+ return { ok: true };
110
+ }
111
+ export function processActions(actions, deps) {
112
+ const results = [];
113
+ const pendingSnapshot = deps.getPending();
114
+ for (const action of actions) {
115
+ // Stale check for approve/reject
116
+ if (action.type === "approve" || action.type === "reject") {
117
+ const fresh = verifyFresh(action, pendingSnapshot);
118
+ if (!fresh.ok) {
119
+ results.push({ action, status: "stale_skipped", detail: fresh.reason });
120
+ try {
121
+ deps.metrics?.incr("tui_actions_invoked", 1);
122
+ }
123
+ catch { }
124
+ continue;
125
+ }
126
+ }
127
+ try {
128
+ if (action.type === "approve") {
129
+ deps.approve(action.proposalId);
130
+ results.push({ action, status: "applied" });
131
+ }
132
+ else if (action.type === "reject") {
133
+ deps.reject(action.proposalId, action.note);
134
+ results.push({ action, status: "rejected" });
135
+ }
136
+ else if (action.type === "acknowledge") {
137
+ deps.acknowledge(action.conflictId);
138
+ results.push({ action, status: "applied" });
139
+ }
140
+ }
141
+ catch (err) {
142
+ const msg = err instanceof Error ? err.message : String(err);
143
+ results.push({ action, status: "error", detail: msg });
144
+ }
145
+ try {
146
+ deps.metrics?.incr("tui_actions_invoked", 1);
147
+ }
148
+ catch { }
149
+ }
150
+ return results;
151
+ }
152
+ export function writeResults(root, results) {
153
+ const dir = join(root, "tui");
154
+ mkdirSync(dir, { recursive: true });
155
+ const payload = JSON.stringify({ generatedAt: new Date().toISOString(), results }, null, 2);
156
+ const target = join(dir, "results.json");
157
+ const tmp = `${target}.tmp`;
158
+ writeFileSync(tmp, payload, "utf8");
159
+ renameSync(tmp, target);
160
+ }
161
+ export function deleteMailbox(root) {
162
+ try {
163
+ unlinkSync(join(root, "tui", "actions.json"));
164
+ }
165
+ catch {
166
+ // missing is fine
167
+ }
168
+ }
169
+ /**
170
+ * Convenience: read → process → write results → delete queue.
171
+ * Returns results (empty if no actions). Mirrors idle-chain usage.
172
+ */
173
+ export function consumeMailbox(root, deps) {
174
+ const { actions } = readMailbox(root);
175
+ if (actions.length === 0)
176
+ return [];
177
+ const results = processActions(actions, deps);
178
+ writeResults(root, results);
179
+ deleteMailbox(root);
180
+ return results;
181
+ }
@@ -0,0 +1,24 @@
1
+ import type { Metrics } from "./metrics.js";
2
+ import type { ConflictView, HealthView, ProposalView } from "./tui-types.js";
3
+ export interface FlushInput {
4
+ readonly root: string;
5
+ readonly proposals: readonly ProposalView[];
6
+ readonly conflicts: readonly ConflictView[];
7
+ readonly health: HealthView;
8
+ readonly metrics?: Metrics | null;
9
+ readonly version?: string;
10
+ }
11
+ export interface FlushResult {
12
+ readonly written: string[];
13
+ readonly skipped: string[];
14
+ }
15
+ export declare function flushSnapshots(input: FlushInput): FlushResult;
16
+ /**
17
+ * Tolerant JSON reader used by tests and (duplicated) by the TUI.
18
+ * Returns {data} on success, {error} on missing/corrupt.
19
+ */
20
+ export declare function readJsonSafe(path: string): {
21
+ data: unknown;
22
+ } | {
23
+ error: "missing" | "corrupt";
24
+ };
@@ -0,0 +1,159 @@
1
+ // v1.3.0 (K13-002/K13-004) — snapshot flush moved to core (pure serialization + atomic write).
2
+ // Was v1.2.0 (K12-003 / plan §4.2, D12-05); version fallback updated for Bedrock (B-002).
3
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ const CAP_BYTES = 512 * 1024;
6
+ const SNAP_FILES = [
7
+ "proposals.json",
8
+ "conflicts.json",
9
+ "health.json",
10
+ "meta.json",
11
+ ];
12
+ function byteLen(s) {
13
+ return Buffer.byteLength(s, "utf8");
14
+ }
15
+ function atomicWrite(target, content) {
16
+ const tmp = `${target}.tmp`;
17
+ writeFileSync(tmp, content, "utf8");
18
+ renameSync(tmp, target);
19
+ }
20
+ function truncateProposals(proposals, cap) {
21
+ // Estimate JSON overhead without diffs to compute available budget.
22
+ // We truncate diff fields proportionally to fit cap.
23
+ const serialized = JSON.stringify(proposals);
24
+ if (byteLen(serialized) <= cap)
25
+ return proposals;
26
+ // Compute total diff length
27
+ const totalDiff = proposals.reduce((acc, p) => acc + byteLen(p.diff), 0);
28
+ if (totalDiff === 0)
29
+ return proposals;
30
+ const overhead = byteLen(serialized) - totalDiff;
31
+ const budget = Math.max(0, cap - overhead - 1024); // leave margin
32
+ // Distribute budget proportionally
33
+ const out = [];
34
+ for (let i = 0; i < proposals.length; i++) {
35
+ const p = proposals[i];
36
+ const diffBytes = byteLen(p.diff);
37
+ const share = Math.floor((diffBytes / totalDiff) * budget);
38
+ // Ensure at least 100 bytes per entry if possible, else share
39
+ const sliceBytes = Math.min(diffBytes, Math.max(share, 100));
40
+ // Slice diff by bytes approximated via string length; diff is ascii mostly
41
+ // so byte length ~ char length. Use char slice.
42
+ const approxChars = Math.floor((sliceBytes / Math.max(diffBytes, 1)) * p.diff.length);
43
+ const truncatedDiff = p.diff.slice(0, Math.max(0, approxChars));
44
+ const needsTrunc = truncatedDiff.length < p.diff.length;
45
+ out.push({
46
+ ...p,
47
+ diff: needsTrunc ? `${truncatedDiff}\n…[truncated]` : p.diff,
48
+ truncated: needsTrunc ? true : p.truncated,
49
+ });
50
+ }
51
+ // If still over cap after proportional truncation, iteratively trim more
52
+ let result = out;
53
+ let ser = JSON.stringify(result);
54
+ let iter = 0;
55
+ while (byteLen(ser) > cap && iter < 5) {
56
+ result = result.map((p) => {
57
+ if (!p.diff || p.diff.length < 200)
58
+ return p;
59
+ const half = Math.floor(p.diff.length / 2);
60
+ return {
61
+ ...p,
62
+ diff: `${p.diff.slice(0, half)}\n…[truncated]`,
63
+ truncated: true,
64
+ };
65
+ });
66
+ ser = JSON.stringify(result);
67
+ iter++;
68
+ }
69
+ return result;
70
+ }
71
+ export function flushSnapshots(input) {
72
+ const { root, proposals, conflicts, health, metrics, version } = input;
73
+ const dir = join(root, "tui");
74
+ mkdirSync(dir, { recursive: true });
75
+ const generatedAt = new Date().toISOString();
76
+ const written = [];
77
+ const skipped = [];
78
+ // Proposals — cap with truncation
79
+ let propToWrite = proposals;
80
+ let propJson = JSON.stringify(propToWrite, null, 2);
81
+ if (byteLen(propJson) > CAP_BYTES) {
82
+ propToWrite = truncateProposals(proposals, CAP_BYTES - 1024);
83
+ propJson = JSON.stringify(propToWrite, null, 2);
84
+ if (byteLen(propJson) > CAP_BYTES) {
85
+ // Still over after truncation: truncate further by dropping diffs entirely
86
+ const minimal = propToWrite.map((p) => ({
87
+ ...p,
88
+ diff: `${p.diff.slice(0, 500)}\n…[truncated]`,
89
+ truncated: true,
90
+ }));
91
+ propJson = JSON.stringify(minimal, null, 2);
92
+ }
93
+ }
94
+ atomicWrite(join(dir, "proposals.json"), propJson);
95
+ written.push("proposals.json");
96
+ // Conflicts
97
+ let conflictsJson = JSON.stringify(conflicts, null, 2);
98
+ if (byteLen(conflictsJson) > CAP_BYTES) {
99
+ // Truncate summaries if needed
100
+ const truncated = conflicts.map((c) => ({
101
+ ...c,
102
+ a_summary: c.a_summary.slice(0, 500),
103
+ b_summary: c.b_summary.slice(0, 500),
104
+ }));
105
+ conflictsJson = JSON.stringify(truncated, null, 2);
106
+ }
107
+ atomicWrite(join(dir, "conflicts.json"), conflictsJson);
108
+ written.push("conflicts.json");
109
+ // Health
110
+ let healthJson = JSON.stringify(health, null, 2);
111
+ if (byteLen(healthJson) > CAP_BYTES) {
112
+ // Health should never exceed cap, but truncate counters if it does
113
+ const truncatedHealth = {
114
+ ...health,
115
+ counters: {},
116
+ };
117
+ healthJson = JSON.stringify(truncatedHealth, null, 2);
118
+ }
119
+ atomicWrite(join(dir, "health.json"), healthJson);
120
+ written.push("health.json");
121
+ // Meta
122
+ const meta = {
123
+ generatedAt,
124
+ version: version ?? "1.3.0",
125
+ files: SNAP_FILES.slice(0, 3),
126
+ };
127
+ atomicWrite(join(dir, "meta.json"), JSON.stringify(meta, null, 2));
128
+ written.push("meta.json");
129
+ if (metrics) {
130
+ try {
131
+ metrics.incr("tui_snapshots_flushed");
132
+ }
133
+ catch {
134
+ // best-effort
135
+ }
136
+ }
137
+ return { written, skipped };
138
+ }
139
+ /**
140
+ * Tolerant JSON reader used by tests and (duplicated) by the TUI.
141
+ * Returns {data} on success, {error} on missing/corrupt.
142
+ */
143
+ export function readJsonSafe(path) {
144
+ try {
145
+ const raw = readFileSync(path, "utf8");
146
+ try {
147
+ return { data: JSON.parse(raw) };
148
+ }
149
+ catch {
150
+ return { error: "corrupt" };
151
+ }
152
+ }
153
+ catch (err) {
154
+ const code = err?.code;
155
+ if (code === "ENOENT")
156
+ return { error: "missing" };
157
+ return { error: "corrupt" };
158
+ }
159
+ }
@@ -0,0 +1,6 @@
1
+ export interface Capabilities {
2
+ readonly skills: boolean;
3
+ readonly references: boolean;
4
+ readonly apiVersion: string | null;
5
+ readonly permissionAsk?: boolean;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { Store } from "./Store.js";
2
+ export declare function hasColumn(store: Store, table: string, column: string): boolean;
3
+ export declare function hasIgnoredColumn(store: Store): boolean;
4
+ export declare function hasCuratedColumn(store: Store): boolean;
5
+ export declare function hasTruthColumns(store: Store): boolean;
6
+ export declare function hasRepoIdColumn(store: Store): boolean;
7
+ export declare function hasLayerColumn(store: Store): boolean;
8
+ export declare function hasRecurrenceColumn(store: Store): boolean;
9
+ export declare function hasArchivedColumn(store: Store): boolean;
10
+ export declare function hasFeedbackColumns(store: Store): boolean;
11
+ export declare function hasFeedbackTable(store: Store): boolean;
@@ -0,0 +1,54 @@
1
+ const cache = new WeakMap();
2
+ export function hasColumn(store, table, column) {
3
+ const byStore = cache.get(store);
4
+ const key = `${table}.${column}`;
5
+ if (byStore?.get(key) === true)
6
+ return true;
7
+ try {
8
+ store.prepare(`SELECT ${column} FROM ${table} LIMIT 0`).get();
9
+ let m = byStore;
10
+ if (!m) {
11
+ m = new Map();
12
+ cache.set(store, m);
13
+ }
14
+ m.set(key, true);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ // v1.1.0 (K11-011 / plan §5.5, D11-06) — named helpers delegating to the registry
22
+ export function hasIgnoredColumn(store) {
23
+ return hasColumn(store, "memories", "ignored");
24
+ }
25
+ export function hasCuratedColumn(store) {
26
+ return hasColumn(store, "memories", "curated");
27
+ }
28
+ export function hasTruthColumns(store) {
29
+ return hasColumn(store, "memories", "truth_penalty");
30
+ }
31
+ export function hasRepoIdColumn(store) {
32
+ return hasColumn(store, "memories", "repo_id");
33
+ }
34
+ export function hasLayerColumn(store) {
35
+ return hasColumn(store, "memories", "layer");
36
+ }
37
+ export function hasRecurrenceColumn(store) {
38
+ return hasColumn(store, "memories", "recurrence_count");
39
+ }
40
+ export function hasArchivedColumn(store) {
41
+ return hasColumn(store, "memories", "archived_at");
42
+ }
43
+ export function hasFeedbackColumns(store) {
44
+ return hasColumn(store, "memories", "feedback_positive");
45
+ }
46
+ export function hasFeedbackTable(store) {
47
+ try {
48
+ store.prepare("SELECT COUNT(*) FROM memory_feedback LIMIT 0").get();
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }