@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,119 @@
1
+ import { SUGGESTIONS } from "./Reflector.js";
2
+ /**
3
+ * v0.4.0 (K4-003/K4-004 / plan §5.1) — QualityGate.
4
+ *
5
+ * Pure, NO-LLM predicates deciding whether a lesson is worth injecting into
6
+ * the model's prompt. Principle 11: never inject noise.
7
+ *
8
+ * - `evaluate` — classifies a lesson (rescued errorType, non-generic
9
+ * suggestion, isActionable, strength).
10
+ * - `rescueErrorType` — a dispatched code overrides the coarse
11
+ * `errorType` (e.g. `'unknown'` → `'TS2304'`), fixing the defect where
12
+ * lessons said "fails with unknown" even when the Reflector DID identify
13
+ * the error code.
14
+ * - `canInject` — per-session + per-fingerprint admission rules (K4-004).
15
+ */
16
+ /** The v0.1.x fallback suggestions. A lesson whose suggestion is one of these
17
+ * (and has no dispatched code) carries no actionable information. */
18
+ export const GENERIC_SUGGESTIONS = new Set(Object.values(SUGGESTIONS));
19
+ export function isGenericSuggestion(suggestion) {
20
+ return GENERIC_SUGGESTIONS.has(suggestion);
21
+ }
22
+ export const QualityGate = {
23
+ /**
24
+ * A dispatched code always overrides the coarse errorType for display:
25
+ * `'unknown'` + `TS2304` → `'TS2304'`.
26
+ */
27
+ rescueErrorType(dispatch, errorType) {
28
+ if (dispatch?.code)
29
+ return dispatch.code;
30
+ return errorType;
31
+ },
32
+ /** Classify a lesson per §5.1 rule 1 (rescue) and the generic ban. */
33
+ evaluate(lesson, dispatch, errorType) {
34
+ const rescued = this.rescueErrorType(dispatch, errorType);
35
+ const generic = isGenericSuggestion(lesson.suggestion);
36
+ // §5.1 rule 2: the generic ban applies only when there is no
37
+ // dispatched code — a matched code always makes the lesson
38
+ // actionable (the lesson then carries a "Likely cause" hint).
39
+ const isActionable = !generic || dispatch?.code != null;
40
+ const strength = dispatch?.code != null || rescued !== "unknown" ? "strong" : "weak";
41
+ return {
42
+ errorType: rescued,
43
+ suggestion: lesson.suggestion,
44
+ isActionable,
45
+ strength,
46
+ };
47
+ },
48
+ /**
49
+ * v0.4.0 (K4-004 / plan §5.1 rules 2-4) — admission gate evaluated at
50
+ * injection time. A lesson is injectable only when:
51
+ *
52
+ * 1. its memory id was not already injected this session (seen-set),
53
+ * 2. its memory is not `stale`,
54
+ * 3. its fingerprint has not recurred since injection (`recurrenceCount
55
+ * === 0` — a lesson that failed to prevent the error leaves the
56
+ * prompt),
57
+ * 4. it is actionable AND strong — weak lessons with generic suggestions
58
+ * are never injected (unless `qualityGateEnabled = false` for debug,
59
+ * K4-023).
60
+ *
61
+ * v0.5.0 (K5-006 / plan §5.2, D5-04) — the boolean form is a thin
62
+ * wrapper over `canInjectVerdict`; the reason is never discarded by
63
+ * callers that use the verdict form.
64
+ */
65
+ canInject(memory, ctx, qualityGateEnabled = true) {
66
+ return this.canInjectVerdict(memory, ctx, qualityGateEnabled).allowed;
67
+ },
68
+ /**
69
+ * v0.5.0 (K5-006 / plan §5.2, D5-04) — the verdict form of `canInject`.
70
+ * Branch order preserves the v0.4.0 sequence with `ignored` inserted
71
+ * second:
72
+ *
73
+ * 0. `confidence < ctx.confidenceFloor` → `low_confidence` (v0.6.0,
74
+ * K6-022 — runs FIRST: it is the cheapest check, and a memory below
75
+ * the floor should not consume a seen-set slot it will never use)
76
+ * 1. seen this session → `seen_this_session`
77
+ * 2. `memory.ignored === true` → `ignored`
78
+ * 3. status !== 'active' → `not_active`
79
+ * 4. recurrence in session → `recurrence`
80
+ * 5. weak / not actionable → `weak` (skipped entirely when
81
+ * `qualityGateEnabled === false` — the debug flag bypasses the
82
+ * quality check only, never the seen/ignored/status/recurrence bans)
83
+ *
84
+ * `ignored` is a human verdict (D5-07) and is enforced even in debug
85
+ * mode: the flag excludes a memory from retrieval entirely, so the gate
86
+ * is only ever asked about it defensively.
87
+ *
88
+ * The floor comparison is `>=`-admissive: a memory exactly AT the floor
89
+ * passes the branch (asserted in K6-022). A floor of '0' admits
90
+ * everything the other five branches allow.
91
+ */
92
+ canInjectVerdict(memory, ctx, qualityGateEnabled = true) {
93
+ if (memory.confidence !== undefined &&
94
+ ctx.confidenceFloor !== undefined &&
95
+ memory.confidence < ctx.confidenceFloor) {
96
+ return { allowed: false, reason: "low_confidence" };
97
+ }
98
+ if (ctx.seenThisSession.has(memory.id)) {
99
+ return { allowed: false, reason: "seen_this_session" };
100
+ }
101
+ if (memory.ignored === true) {
102
+ return { allowed: false, reason: "ignored" };
103
+ }
104
+ if (memory.status !== undefined && memory.status !== "active") {
105
+ return { allowed: false, reason: "not_active" };
106
+ }
107
+ if (ctx.recurrenceCount > 0) {
108
+ return { allowed: false, reason: "recurrence" };
109
+ }
110
+ if (!memory.isActionable || memory.strength === "weak") {
111
+ if (qualityGateEnabled) {
112
+ return { allowed: false, reason: "weak" };
113
+ }
114
+ // K4-023 debug mode: weak lessons are admitted but must be
115
+ // flagged by the caller (ContextInjector adds the marker).
116
+ }
117
+ return { allowed: true, reason: "ok" };
118
+ },
119
+ };
@@ -0,0 +1,91 @@
1
+ import type { MemoryService } from "./MemoryService.js";
2
+ import type { Metrics } from "./metrics.js";
3
+ export interface ReflectionInput {
4
+ toolName: string;
5
+ argsSummary: string;
6
+ stderr: string;
7
+ stdout: string;
8
+ exitCode?: number;
9
+ errorType: string;
10
+ sessionId: string;
11
+ /** Optional project scope for v0.2.0 dedup + per-fingerprint throttle. */
12
+ projectId?: string | null;
13
+ /** v0.3.0 fix — callID of the failing tool_call. Used by onLinkError
14
+ * (see ReflectorOptions) to stamp tool_calls.error_fingerprint so the
15
+ * feedback loop can match recurrences by the SAME identity dimension
16
+ * the error memory uses. */
17
+ callID?: string;
18
+ }
19
+ export interface ReflectorOptions {
20
+ throttleMs?: number;
21
+ /** v0.3.0 (K3-018) — optional LLM enrichment callback. Default no-op. */
22
+ enrich?: (lesson: string, stderr: string, stdout: string) => Promise<string | null>;
23
+ /** v0.3.0 fix — link a failing tool_call to the stderr-based fingerprint
24
+ * the matching error memory uses. Default no-op. The caller (index.ts)
25
+ * provides an implementation that UPDATEs tool_calls.error_fingerprint
26
+ * for the given callID, so boost/penalize feedback queries stop missing
27
+ * recurrences by fingerprint mismatch. */
28
+ onLinkError?: (callID: string, fingerprint: string) => void;
29
+ }
30
+ export interface HeuristicLessonInput {
31
+ toolName: string;
32
+ errorType: string;
33
+ /** v0.4.0 (K4-005) — errorType shown in the `fails with` slot after
34
+ * rescue: the dispatched code when one matched, else the coarse
35
+ * errorType. Suggestion lookup still uses `errorType` (the coarse
36
+ * category) so the category suggestion is kept even when a code
37
+ * overrides the display. */
38
+ displayErrorType?: string;
39
+ firstErrorLine: string;
40
+ /** v0.2.0 (K2-018) — optional pre-computed dispatch result. When absent,
41
+ * dispatch is performed on `firstErrorLine` alone. */
42
+ dispatched?: DispatchedLesson | null;
43
+ }
44
+ /** Result of the per-error-code deterministic rule dispatch (K2-018 / D2-09). */
45
+ export interface DispatchedLesson {
46
+ /** Stable short code captured from the source output (e.g. `TS2304`,
47
+ * `EADDRINUSE`, `F401`, the captured `Error: <Name>` class, the failing
48
+ * command string). `null` when no rule matches (fallback path). */
49
+ code: string | null;
50
+ /** Short deterministic hint, e.g. `import or typo`, `review syscall:
51
+ * EADDRINUSE`. `null` when `code` is null (the v0.1.x fallback path). */
52
+ hint: string | null;
53
+ }
54
+ export declare const ERROR_LINE_RE: RegExp;
55
+ export declare const STRONG_ERROR_RE: RegExp;
56
+ /**
57
+ * v0.1.x fallback table — keyed by `errorType`. RETAINED in v0.2.0 as the
58
+ * fallback for memos whose output does not match a deterministic code rule.
59
+ * The v0.2.0 per-error-code rule table below layers ON TOP of this fallback:
60
+ * when a code is matched, the hint is appended to the v0.1.x suggestion as a
61
+ * 'Likely cause:' line; when no code is matched, output is identical to v0.1.x.
62
+ *
63
+ * Exported since v0.4.0 (K4-003) so QualityGate can detect the generic
64
+ * fallback suggestions without duplicating the strings.
65
+ */
66
+ export declare const SUGGESTIONS: Record<string, string>;
67
+ export declare const TS_CODE_RULES: Record<string, string>;
68
+ export declare class Reflector {
69
+ private memoryService;
70
+ private lastReflectionByFp;
71
+ private throttleMs;
72
+ private metrics;
73
+ private enrichFn;
74
+ private onLinkErrorFn;
75
+ constructor(memoryService: MemoryService, options?: ReflectorOptions, metrics?: Metrics | null);
76
+ invoke(input: ReflectionInput): Promise<string | null>;
77
+ generateHeuristicLesson(input: HeuristicLessonInput): string;
78
+ /**
79
+ * v0.2.0 (K2-018 / D2-09) — deterministic per-error-code rule dispatch.
80
+ * Pure regex sweep over `stderr + '\n' + stdout` with NO LLM hop. Returns
81
+ * a short stable `code` + `hint` pair when a known rule matches, or
82
+ * `{ code: null, hint: null }` for the v0.1.x fallback path (which keeps
83
+ * the v0.1.x `SUGGESTIONS[errorType]` suggestion verbatim).
84
+ *
85
+ * Exported for unit testing (K2-019).
86
+ */
87
+ dispatchLesson(stderr: string, stdout: string, errorType: string): DispatchedLesson;
88
+ redactPaths(text: string): string;
89
+ redactSecrets(text: string): string;
90
+ private extractFirstErrorLine;
91
+ }
@@ -0,0 +1,307 @@
1
+ import { fingerprint as computeFingerprint } from "./fingerprint.js";
2
+ import { classify } from "./inferability.js";
3
+ import { redactPaths as redactPathsText } from "./redact.js";
4
+ const DEFAULT_THROTTLE_MS = 60_000;
5
+ const MAX_CONTENT_CHARS = 4096;
6
+ const MAX_ERROR_LINE_CHARS = 500;
7
+ const TRUNC_SUFFIX = "... [truncated]";
8
+ const CONTEXT_PREFIX = "\n\nContext:\n";
9
+ export const ERROR_LINE_RE = /\b(error|failed|fail|cannot find|cannot resolve|TS\d{4,}|exception|traceback|panic|fatal|referenceerror|typeerror|syntaxerror|command failed|non-zero exit)\b/i;
10
+ export const STRONG_ERROR_RE = /\b(cannot find|cannot resolve|TS\d{4,}|error TS\d|command failed|non-zero exit|exit code [1-9]\d*|traceback|referenceerror|typeerror|syntaxerror|fatal error|exception|failed to compile|build failed|compilation failed)\b/i;
11
+ /**
12
+ * v0.1.x fallback table — keyed by `errorType`. RETAINED in v0.2.0 as the
13
+ * fallback for memos whose output does not match a deterministic code rule.
14
+ * The v0.2.0 per-error-code rule table below layers ON TOP of this fallback:
15
+ * when a code is matched, the hint is appended to the v0.1.x suggestion as a
16
+ * 'Likely cause:' line; when no code is matched, output is identical to v0.1.x.
17
+ *
18
+ * Exported since v0.4.0 (K4-003) so QualityGate can detect the generic
19
+ * fallback suggestions without duplicating the strings.
20
+ */
21
+ export const SUGGESTIONS = {
22
+ typecheck: "Verify types and imports before running.",
23
+ lint: "Run linter and fix warnings before committing.",
24
+ test: "Run tests and fix failures before proceeding.",
25
+ runtime: "Check error message and stack trace for root cause.",
26
+ timeout: "Check for infinite loops or long-running operations.",
27
+ unknown: "Review the error output for details.",
28
+ };
29
+ // --- v0.2.0 (K2-018) lesson v2 — per-error-code deterministic dispatch (D2-09).
30
+ // Pure TS, NO LLM hop. Order of dispatch matches the plan §B6.4 priority list:
31
+ // (1) TS\d{4,5} > (2) Python lint > (3) syscall > (4) generic `Error: <Name>` >
32
+ // (5) `Command "<cmd>" failed` > (6) v0.1.x SUGGESTIONS fallback.
33
+ // v0.2.0 (K2-018) — shared per-error-code rule table. Exported so
34
+ // kevin_why.ts reuses the SAME hints instead of duplicating them
35
+ // (bug #6).
36
+ // v0.4.0 (K4-022 / plan §5.4, D4-07) — expanded with the observed §3.3
37
+ // cases: TS2307 (cannot find module), TS2339/TS2305 (missing member),
38
+ // TS6133 (unused).
39
+ export const TS_CODE_RULES = {
40
+ "2304": "import or typo",
41
+ "2322": "type mismatch",
42
+ "2740": "missing or wrong property",
43
+ "2552": "undefined identifier",
44
+ "18047": "possibly null",
45
+ "2307": "install the dependency or add it to package.json before importing",
46
+ "2339": "check the imported surface for the correct member name",
47
+ "2305": "check the imported surface for the correct member name",
48
+ "6133": "remove the declaration or use it",
49
+ };
50
+ const TS_CODE_RE = /\bTS(\d{4,5})\b/i;
51
+ const PY_LINT_RE = /\b(ELIF\d{0,4})\b|\b(F\d{3,4})\b|flake8:\s+(\S+)/;
52
+ const SYSCALL_RE = /\b(EADDRINUSE|ENOENT|EACCES|EPERM)\b/;
53
+ // v0.4.0 (K4-022) — rust unresolved item/module (E0433/E0432).
54
+ const RUST_CODE_RE = /\b(E0433|E0432)\b/;
55
+ // v0.4.0 (K4-022) — shell command-not-found (`cmd: command not found`,
56
+ // `The term 'cmd' is not recognized ...`).
57
+ const COMMAND_NOT_FOUND_RE = /\b([a-zA-Z0-9_.-]+):\s+(?:command not found|The term\s+['"][^'"]+['"]\s+is not recognized)\b/i;
58
+ const GENERIC_ERROR_RE = /\bError:\s+(\w+)/;
59
+ const COMMAND_FAILED_RE = /Command\s+"([^"]+)"\s+failed/;
60
+ const SECRET_PATTERNS = [
61
+ /\b(API_KEY|SECRET|PASSWORD|TOKEN)\b\s*[=:]\s*\S+/gi,
62
+ /\bBearer\s+\S+/gi,
63
+ // BUG-013 — same narrowing as ToolCallObserver: the old
64
+ // `\btoken\s+\S+/gi` mangled harmless phrasing ("token budget",
65
+ // "token count") in stderr/stdout — corrupting stored summaries AND
66
+ // the fingerprints derived from the redacted text. Credential
67
+ // contexts only: named token variables WITH an assignment, and
68
+ // `token=<value>` / `token: <value>` assignments.
69
+ /\b(access_?token|auth_?token|api_?token)\b\s*[=:]\s*\S+/gi,
70
+ /\btoken\s*[=:]\s*\S+/gi,
71
+ ];
72
+ const PATH_PATTERNS_DEPRECATED = null;
73
+ export class Reflector {
74
+ memoryService;
75
+ lastReflectionByFp = new Map();
76
+ throttleMs;
77
+ metrics;
78
+ enrichFn;
79
+ onLinkErrorFn;
80
+ constructor(memoryService, options, metrics) {
81
+ this.memoryService = memoryService;
82
+ this.throttleMs = options?.throttleMs ?? DEFAULT_THROTTLE_MS;
83
+ this.metrics = metrics ?? null;
84
+ this.enrichFn = options?.enrich ?? (async () => null);
85
+ this.onLinkErrorFn = options?.onLinkError ?? (() => { });
86
+ }
87
+ async invoke(input) {
88
+ const now = Date.now();
89
+ const redactedStderr = this.redactSecrets(this.redactPaths(input.stderr));
90
+ const redactedStdout = this.redactSecrets(this.redactPaths(input.stdout));
91
+ const sourceOutput = redactedStderr.length > 0 ? redactedStderr : redactedStdout;
92
+ const firstErrorLine = this.extractFirstErrorLine(sourceOutput);
93
+ // v0.4.0 (K4-005) — dispatch once and reuse for the rescued
94
+ // errorType AND metadata.dispatch.
95
+ const dispatched = this.dispatchLesson(redactedStderr, redactedStdout, input.errorType);
96
+ const lesson = this.generateHeuristicLesson({
97
+ toolName: input.toolName,
98
+ errorType: input.errorType,
99
+ displayErrorType: dispatched.code ?? input.errorType,
100
+ firstErrorLine,
101
+ dispatched,
102
+ });
103
+ // v0.3.0 fix (bug #5) — the per-fingerprint throttle check runs
104
+ // BEFORE the optional LLM enrichment, so throttled repeats never
105
+ // waste an LLM call. The fingerprint is computed from the source
106
+ // output when present, otherwise from the PRE-enrichment lesson,
107
+ // keeping the identity stable and identical to the saved memory's.
108
+ const projectId = input.projectId ?? null;
109
+ const fpContent = sourceOutput.length > 0 ? sourceOutput : lesson;
110
+ const fp = computeFingerprint(fpContent, projectId ?? undefined);
111
+ // v0.3.0 fix — stamp the failing tool_call with the stderr-based
112
+ // fingerprint so the feedback loop's recurrence queries can match
113
+ // it (closes the fingerprint-mismatch bug). Runs BEFORE the
114
+ // throttle check: throttled repeats (K4-025 recurrences) still
115
+ // stamp — the throttle only skips the expensive enrichment/save,
116
+ // but the recurrence signal (settle's COUNT, QualityGate,
117
+ // penalizeRecurringReflectors) must see every failing call.
118
+ if (input.callID && this.onLinkErrorFn) {
119
+ try {
120
+ this.onLinkErrorFn(input.callID, fp);
121
+ }
122
+ catch {
123
+ // linking failure is non-blocking
124
+ }
125
+ }
126
+ const last = this.lastReflectionByFp.get(fp) ?? 0;
127
+ if (now - last < this.throttleMs) {
128
+ this.metrics?.incr("reflections_throttled", 1);
129
+ return null;
130
+ }
131
+ this.lastReflectionByFp.set(fp, now);
132
+ const configurableMemoryService = this.memoryService;
133
+ const lessonMode = configurableMemoryService.getSetting?.("error_lesson_mode", "all") ??
134
+ "all";
135
+ const verdict = classify({
136
+ type: "error",
137
+ content: lesson,
138
+ metadata: { dispatch: { code: dispatched.code, hint: dispatched.hint } },
139
+ });
140
+ if (lessonMode === "triage_only" && verdict === "inferable") {
141
+ this.metrics?.incr("error_lessons_suppressed", 1);
142
+ return null;
143
+ }
144
+ // v0.3.0 (K3-018): optional LLM enrichment opt-in. Runs only when
145
+ // the throttle check passed (bug #5).
146
+ let enrichedLesson = lesson;
147
+ try {
148
+ const enrichment = await this.enrichFn(lesson, redactedStderr, redactedStdout);
149
+ if (enrichment) {
150
+ enrichedLesson = `${lesson}\n${enrichment}`;
151
+ }
152
+ }
153
+ catch {
154
+ // enrichment failures are non-blocking
155
+ }
156
+ const metadata = {};
157
+ if (input.callID) {
158
+ metadata.origin_call_id = input.callID;
159
+ }
160
+ // v0.4.0 (K4-005) — persist the dispatch result so injection and
161
+ // promotion can reuse the rescued errorType and hint without
162
+ // re-dispatching. Stored even when no code matched ({code:null,hint:null})
163
+ // so callers can distinguish "checked and unmatched" from "absent".
164
+ metadata.dispatch = { code: dispatched.code, hint: dispatched.hint };
165
+ let finalContent;
166
+ if (sourceOutput.length > 0) {
167
+ const fullLen = enrichedLesson.length + CONTEXT_PREFIX.length + sourceOutput.length;
168
+ if (fullLen <= MAX_CONTENT_CHARS) {
169
+ finalContent = `${enrichedLesson}${CONTEXT_PREFIX}${sourceOutput}`;
170
+ }
171
+ else {
172
+ const budget = MAX_CONTENT_CHARS -
173
+ enrichedLesson.length -
174
+ CONTEXT_PREFIX.length -
175
+ TRUNC_SUFFIX.length;
176
+ const truncated = sourceOutput.slice(0, Math.max(0, budget));
177
+ finalContent = `${enrichedLesson}${CONTEXT_PREFIX}${truncated}${TRUNC_SUFFIX}`;
178
+ metadata.truncated = true;
179
+ }
180
+ }
181
+ else {
182
+ finalContent = enrichedLesson;
183
+ }
184
+ // K2-007: the per-fingerprint throttle check ran above (before
185
+ // enrichment); `fp` is reused here so dedup/throttle and the saved
186
+ // memory agree on identity.
187
+ const id = this.memoryService.save({
188
+ type: "error",
189
+ content: finalContent,
190
+ scope: "project",
191
+ sourceTool: input.toolName,
192
+ sourceSession: input.sessionId,
193
+ metadata,
194
+ origin: "reflector",
195
+ projectId: projectId ?? undefined,
196
+ fingerprint: fp,
197
+ });
198
+ return id;
199
+ }
200
+ generateHeuristicLesson(input) {
201
+ const dispatched = input.dispatched ??
202
+ this.dispatchLesson(input.firstErrorLine, "", input.errorType);
203
+ const suggestion = SUGGESTIONS[input.errorType] ?? SUGGESTIONS.unknown;
204
+ const shownErrorType = input.displayErrorType ?? input.errorType;
205
+ const line = input.firstErrorLine.length > MAX_ERROR_LINE_CHARS
206
+ ? `${input.firstErrorLine.slice(0, MAX_ERROR_LINE_CHARS)}...`
207
+ : input.firstErrorLine;
208
+ let lesson = `When ${input.toolName} fails with ${shownErrorType}: ${line}\nSuggestion: ${suggestion}`;
209
+ if (dispatched.code && dispatched.hint) {
210
+ lesson += `\nLikely cause: ${dispatched.hint} (code ${dispatched.code})`;
211
+ }
212
+ return lesson;
213
+ }
214
+ /**
215
+ * v0.2.0 (K2-018 / D2-09) — deterministic per-error-code rule dispatch.
216
+ * Pure regex sweep over `stderr + '\n' + stdout` with NO LLM hop. Returns
217
+ * a short stable `code` + `hint` pair when a known rule matches, or
218
+ * `{ code: null, hint: null }` for the v0.1.x fallback path (which keeps
219
+ * the v0.1.x `SUGGESTIONS[errorType]` suggestion verbatim).
220
+ *
221
+ * Exported for unit testing (K2-019).
222
+ */
223
+ dispatchLesson(stderr, stdout, errorType) {
224
+ const combined = `${stderr}\n${stdout}`;
225
+ const tsMatch = combined.match(TS_CODE_RE);
226
+ if (tsMatch) {
227
+ const num = tsMatch[1];
228
+ const hint = TS_CODE_RULES[num] ?? `review TS${num}`;
229
+ return { code: `TS${num}`, hint };
230
+ }
231
+ const pyMatch = combined.match(PY_LINT_RE);
232
+ if (pyMatch) {
233
+ const rule = pyMatch[1] || pyMatch[2] || pyMatch[3] || "unknown";
234
+ return { code: rule, hint: `review python lint: ${rule}` };
235
+ }
236
+ const rustMatch = combined.match(RUST_CODE_RE);
237
+ if (rustMatch) {
238
+ const code = rustMatch[1];
239
+ return {
240
+ code,
241
+ hint: "add the dependency to Cargo.toml or use a full path (crate::...)",
242
+ };
243
+ }
244
+ const sysMatch = combined.match(SYSCALL_RE);
245
+ if (sysMatch) {
246
+ const code = sysMatch[1];
247
+ const hint = code === "EADDRINUSE"
248
+ ? "free the port (netstat -ano | findstr :PORT) or change the port"
249
+ : `review syscall: ${code}`;
250
+ return { code, hint };
251
+ }
252
+ const cmdNotFoundMatch = combined.match(COMMAND_NOT_FOUND_RE);
253
+ if (cmdNotFoundMatch) {
254
+ const cmd = cmdNotFoundMatch[1];
255
+ return {
256
+ code: cmd,
257
+ hint: `install the tool (e.g. npm i -g ${cmd}) or call it by its full path`,
258
+ };
259
+ }
260
+ const errMatch = combined.match(GENERIC_ERROR_RE);
261
+ if (errMatch) {
262
+ const name = errMatch[1];
263
+ return { code: name, hint: `review error class: ${name}` };
264
+ }
265
+ const cmdMatch = combined.match(COMMAND_FAILED_RE);
266
+ if (cmdMatch) {
267
+ const cmd = cmdMatch[1];
268
+ return { code: cmd, hint: `review failing command: ${cmd}` };
269
+ }
270
+ return { code: null, hint: null };
271
+ }
272
+ redactPaths(text) {
273
+ return redactPathsText(text);
274
+ }
275
+ redactSecrets(text) {
276
+ let out = text;
277
+ for (const pat of SECRET_PATTERNS) {
278
+ out = out.replace(pat, (match) => {
279
+ // BUG-013 — preserve the original separator so
280
+ // `auth_token = abc123` keeps its spacing: only the
281
+ // value is replaced.
282
+ const m = match.match(/^(.+?)(\s*[=:]\s*)(\S+)$/);
283
+ if (m) {
284
+ return `${m[1]}${m[2]}<redacted>`;
285
+ }
286
+ const parts = match.split(/\s+/);
287
+ return `${parts[0]} <redacted>`;
288
+ });
289
+ }
290
+ return out;
291
+ }
292
+ extractFirstErrorLine(text) {
293
+ const lines = text.split(/\r?\n/);
294
+ for (const line of lines) {
295
+ const trimmed = line.trim();
296
+ if (trimmed.length > 0 && ERROR_LINE_RE.test(trimmed)) {
297
+ return trimmed;
298
+ }
299
+ }
300
+ for (const line of lines) {
301
+ const trimmed = line.trim();
302
+ if (trimmed.length > 0)
303
+ return trimmed;
304
+ }
305
+ return "";
306
+ }
307
+ }
@@ -0,0 +1,124 @@
1
+ import type { ArtifactWriter } from "./ArtifactWriter.js";
2
+ import type { HostSurface } from "./host.js";
3
+ /**
4
+ * The four sources `resolve()` tries, in order (plan §5.2, D9-13).
5
+ */
6
+ export type IdentitySource = "declared" | "remote" | "host" | "path";
7
+ /**
8
+ * The outcome of resolving a repository identity.
9
+ */
10
+ export interface ResolvedIdentity {
11
+ /** The repository-scoped id every Team scope is keyed on. */
12
+ repoId: string;
13
+ /** Which source produced `repoId`. */
14
+ source: IdentitySource;
15
+ /**
16
+ * A short, non-secret description of how the id was derived,
17
+ * surfaced by `kevin_project show`. Never contains a credential
18
+ * or an absolute path.
19
+ */
20
+ evidence: string;
21
+ /** The v0.7.0 project scope, always returned regardless of source. */
22
+ projectId: string;
23
+ }
24
+ /**
25
+ * Read the first `url = …` value inside `[remote "<name>"]` of a
26
+ * `.git/config`-style INI text.
27
+ *
28
+ * Line-oriented reader, not an INI library: tracks the current
29
+ * `[section "sub"]` header, tolerates tabs, spaces around `=`,
30
+ * CRLF, and comment lines. Returns `null` on anything unrecognised
31
+ * and never throws — on any input, including binary.
32
+ */
33
+ export declare function parseGitConfigRemote(text: string, name?: string): string | null;
34
+ /**
35
+ * Fold the URL shapes git accepts into one canonical string.
36
+ *
37
+ * Steps, in order (K8-005):
38
+ * 1. strip a trailing `.git`;
39
+ * 2. strip a `scheme://` prefix;
40
+ * 3. strip everything up to and including the last `@` in the
41
+ * authority (userinfo and embedded credentials — a token must
42
+ * never reach a hash that lands in a committed file, D8-04);
43
+ * 4. strip a numeric port (`:8443`, `:443`, …) so the same repo
44
+ * served over different transports folds to one id;
45
+ * 5. rewrite the scp-style `host:path` separator to `host/path`;
46
+ * 6. strip a trailing `/`;
47
+ * 7. lowercase the whole result.
48
+ *
49
+ * Returns `null` when the result contains no `/` (e.g. a local
50
+ * path remote) — the caller falls through to the path source.
51
+ */
52
+ export declare function normalizeRemote(url: string): string | null;
53
+ /**
54
+ * Derive a repository id from a normalized remote (K8-006).
55
+ *
56
+ * The domain prefix means a repo id can never collide with a memory
57
+ * fingerprint computed over the same string, and it leaves room to
58
+ * version the derivation later.
59
+ */
60
+ export declare function computeRepoId(normalized: string): string;
61
+ /**
62
+ * Resolve the repository identity for `cwd`, trying the four
63
+ * sources in order (plan §5.2, D9-13):
64
+ *
65
+ * 1. `declared` — `.kevin/project.json` → `id` (validated as
66
+ * exactly 16 lowercase hex characters; anything else is ignored
67
+ * and falls through, because a hand-edited garbage id in a
68
+ * committed file would otherwise scope a whole team's corpus
69
+ * onto a typo);
70
+ * 2. `remote` — `.git/config` → `[remote "origin"] url`,
71
+ * normalized and hashed;
72
+ * 3. `host` — the value the host resolved for this directory,
73
+ * `host.project.worktree` first, `host.project.directory` as
74
+ * fallback; both empty or absent falls through to `path`.
75
+ * This is strictly better than cwd — the host's own
76
+ * ToolContext documents "prefer this over cwd" —
77
+ * but it sits below the explicit sources, because monorepos and
78
+ * D8-03's confirmed re-keying depend on `.kevin/project.json`
79
+ * winning;
80
+ * 4. `path` — `fingerprint(cwd)`, the v0.7.0 behaviour, preserved
81
+ * exactly when `host` is absent.
82
+ *
83
+ * Never throws: a directory that is not a git repository, an
84
+ * unreadable `.git/config`, a malformed `project.json`, and a host
85
+ * with no usable project fields all fall through to source 4.
86
+ * `projectId` is always returned alongside `repoId`, regardless of
87
+ * which source won.
88
+ */
89
+ export declare function resolve(cwd: string, host?: HostSurface): ResolvedIdentity;
90
+ /**
91
+ * The outcome of `initProjectFile` (K8-008).
92
+ */
93
+ export interface InitProjectResult {
94
+ /** Whether the file was written. */
95
+ ok: boolean;
96
+ /** The absolute path the file was written to, or refused. */
97
+ path: string;
98
+ /** Human-readable refusal reason when `ok` is false. */
99
+ reason?: string;
100
+ /** The pinned id when the file was written. */
101
+ id?: string;
102
+ /** The ISO-8601 Z timestamp written into the file. */
103
+ createdAt?: string;
104
+ }
105
+ /**
106
+ * Write `.kevin/project.json` pinning the repository identity (K8-008
107
+ * / plan §5.8). The id comes from the current `resolve(cwd)` — so `init`
108
+ * in a repository with a remote *pins* the remote-derived id, which is
109
+ * the point: it survives the organisation renaming the repo (a remote
110
+ * URL change would otherwise silently re-scope the corpus).
111
+ *
112
+ * The file is `{"id": "<16 hex>", "created_at": "<ISO-8601 Z>",
113
+ * "generator": "opencode-kevin/0.8.0"}` with sorted keys and a
114
+ * terminating newline (deterministic bytes, the codec discipline of
115
+ * plan §7.3). `init` refuses when the file already exists: overwriting
116
+ * it re-scopes a team's corpus, and that is `rekey`'s job (K8-009),
117
+ * with a confirmation.
118
+ *
119
+ * NOTE (K8-019): the write goes through `ArtifactWriter` in whole-file
120
+ * mode — `.kevin/project.json` is a Kevin-owned file, and D8-08 leaves
121
+ * no scenario in which a second raw write path is acceptable (asserted
122
+ * by tests/unit/single_write_path.test.ts).
123
+ */
124
+ export declare function initProjectFile(cwd: string, writer: ArtifactWriter): InitProjectResult;