thumbgate 1.3.0 → 1.4.1

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 (156) hide show
  1. package/.claude-plugin/README.md +25 -0
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +242 -126
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/INSTALL.md +59 -4
  9. package/adapters/chatgpt/openapi.yaml +168 -0
  10. package/adapters/claude/.mcp.json +2 -2
  11. package/adapters/codex/config.toml +2 -2
  12. package/adapters/mcp/server-stdio.js +84 -1
  13. package/adapters/opencode/opencode.json +1 -1
  14. package/bin/cli.js +204 -13
  15. package/bin/postinstall.js +8 -2
  16. package/config/budget.json +18 -0
  17. package/config/gates/code-edit.json +61 -0
  18. package/config/gates/db-write.json +61 -0
  19. package/config/gates/default.json +154 -3
  20. package/config/gates/deploy.json +61 -0
  21. package/config/github-about.json +2 -1
  22. package/config/merge-quality-checks.json +23 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -11
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/opencode-profile/INSTALL.md +1 -1
  34. package/public/blog.html +73 -0
  35. package/public/compare/mem0.html +189 -0
  36. package/public/compare/speclock.html +180 -0
  37. package/public/compare.html +10 -2
  38. package/public/guide.html +2 -2
  39. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/codex-cli-guardrails.html +158 -0
  41. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  42. package/public/guides/pre-action-gates.html +162 -0
  43. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  44. package/public/index.html +172 -65
  45. package/public/lessons.html +33 -24
  46. package/public/llm-context.md +140 -0
  47. package/public/pro.html +24 -22
  48. package/scripts/access-anomaly-detector.js +1 -1
  49. package/scripts/adk-consolidator.js +1 -5
  50. package/scripts/agent-security-hardening.js +4 -6
  51. package/scripts/agentic-data-pipeline.js +1 -3
  52. package/scripts/async-job-runner.js +1 -5
  53. package/scripts/audit-trail.js +1 -5
  54. package/scripts/auto-promote-gates.js +5 -3
  55. package/scripts/background-agent-governance.js +2 -10
  56. package/scripts/billing-setup.js +109 -0
  57. package/scripts/billing.js +2 -16
  58. package/scripts/budget-enforcer.js +173 -0
  59. package/scripts/build-claude-mcpb.js +71 -5
  60. package/scripts/build-codex-plugin.js +152 -0
  61. package/scripts/check-congruence.js +132 -14
  62. package/scripts/commercial-offer.js +5 -7
  63. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  64. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  65. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  66. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  67. package/scripts/context-engine.js +21 -6
  68. package/scripts/contextfs.js +1 -21
  69. package/scripts/dashboard.js +20 -0
  70. package/scripts/decision-journal.js +341 -0
  71. package/scripts/delegation-runtime.js +1 -5
  72. package/scripts/distribution-surfaces.js +54 -0
  73. package/scripts/document-intake.js +927 -0
  74. package/scripts/ephemeral-agent-store.js +1 -8
  75. package/scripts/evolution-state.js +1 -5
  76. package/scripts/experiment-tracker.js +1 -5
  77. package/scripts/export-databricks-bundle.js +1 -5
  78. package/scripts/export-hf-dataset.js +1 -5
  79. package/scripts/export-training.js +1 -5
  80. package/scripts/feedback-attribution.js +1 -16
  81. package/scripts/feedback-history-distiller.js +1 -16
  82. package/scripts/feedback-loop.js +1 -5
  83. package/scripts/feedback-root-consolidator.js +2 -21
  84. package/scripts/feedback-session.js +49 -0
  85. package/scripts/feedback-to-rules.js +215 -36
  86. package/scripts/filesystem-search.js +1 -9
  87. package/scripts/fs-utils.js +104 -0
  88. package/scripts/gates-engine.js +200 -11
  89. package/scripts/github-about.js +32 -8
  90. package/scripts/gtm-revenue-loop.js +1 -5
  91. package/scripts/harness-selector.js +148 -0
  92. package/scripts/hosted-config.js +2 -0
  93. package/scripts/hosted-job-launcher.js +1 -5
  94. package/scripts/hybrid-feedback-context.js +33 -49
  95. package/scripts/intervention-policy.js +58 -1
  96. package/scripts/lesson-db.js +3 -18
  97. package/scripts/lesson-inference.js +194 -16
  98. package/scripts/lesson-retrieval.js +60 -24
  99. package/scripts/llm-client.js +59 -0
  100. package/scripts/managed-lesson-agent.js +183 -0
  101. package/scripts/marketing-experiment.js +8 -22
  102. package/scripts/meta-agent-loop.js +624 -0
  103. package/scripts/metered-billing.js +1 -1
  104. package/scripts/money-watcher.js +1 -4
  105. package/scripts/obsidian-export.js +1 -5
  106. package/scripts/operational-integrity.js +15 -3
  107. package/scripts/operational-summary.js +41 -5
  108. package/scripts/org-dashboard.js +6 -1
  109. package/scripts/per-step-scoring.js +2 -4
  110. package/scripts/pr-manager.js +201 -19
  111. package/scripts/pro-features.js +3 -2
  112. package/scripts/prompt-dlp.js +3 -3
  113. package/scripts/prove-adapters.js +1 -5
  114. package/scripts/prove-attribution.js +1 -5
  115. package/scripts/prove-automation.js +1 -3
  116. package/scripts/prove-cloudflare-sandbox.js +1 -3
  117. package/scripts/prove-data-pipeline.js +1 -3
  118. package/scripts/prove-intelligence.js +1 -3
  119. package/scripts/prove-lancedb.js +1 -5
  120. package/scripts/prove-local-intelligence.js +1 -3
  121. package/scripts/prove-packaged-runtime.js +75 -9
  122. package/scripts/prove-predictive-insights.js +1 -3
  123. package/scripts/prove-training-export.js +1 -3
  124. package/scripts/prove-workflow-contract.js +1 -5
  125. package/scripts/ralph-loop.js +376 -0
  126. package/scripts/ralph-mode-ci.js +331 -0
  127. package/scripts/rate-limiter.js +3 -1
  128. package/scripts/reddit-dm-outreach.js +14 -4
  129. package/scripts/rotate-stripe-webhook-secret.js +314 -0
  130. package/scripts/schedule-manager.js +3 -5
  131. package/scripts/security-scanner.js +448 -0
  132. package/scripts/self-distill-agent.js +579 -0
  133. package/scripts/semantic-dedup.js +115 -0
  134. package/scripts/skill-exporter.js +1 -3
  135. package/scripts/skill-generator.js +1 -5
  136. package/scripts/social-analytics/engagement-audit.js +1 -18
  137. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  138. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  139. package/scripts/social-analytics/publishers/zernio.js +51 -0
  140. package/scripts/social-pipeline.js +1 -3
  141. package/scripts/social-post-hourly.js +47 -4
  142. package/scripts/statusline-links.js +6 -5
  143. package/scripts/statusline.sh +29 -153
  144. package/scripts/sync-branch-protection.js +340 -0
  145. package/scripts/tessl-export.js +1 -3
  146. package/scripts/thumbgate-search.js +32 -1
  147. package/scripts/tool-kpi-tracker.js +1 -1
  148. package/scripts/tool-registry.js +106 -2
  149. package/scripts/vector-store.js +1 -5
  150. package/scripts/weekly-auto-post.js +1 -1
  151. package/scripts/workflow-sentinel.js +91 -0
  152. package/skills/thumbgate/SKILL.md +1 -1
  153. package/src/api/server.js +296 -7
  154. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  155. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  156. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Harness Selector — Context-Aware Gate Harness Loading
5
+ *
6
+ * Auto Agent concept: instead of one monolithic gate config, select a
7
+ * specialized harness based on the workflow type detected from the tool call.
8
+ *
9
+ * Detection priority (first match wins):
10
+ * 1. THUMBGATE_HARNESS env var — explicit override
11
+ * 2. Tool-name heuristic (Edit/Write/MultiEdit → code-edit)
12
+ * 3. Command-text heuristic (deploy keywords → deploy, SQL keywords → db-write)
13
+ * 4. null → load only default.json + auto-promoted gates
14
+ *
15
+ * Each harness is ADDITIVE — default.json gates always load first.
16
+ */
17
+
18
+ const path = require('path');
19
+
20
+ const HARNESS_DIR = path.join(__dirname, '..', 'config', 'gates');
21
+
22
+ const HARNESSES = Object.freeze({
23
+ deploy: path.join(HARNESS_DIR, 'deploy.json'),
24
+ 'code-edit': path.join(HARNESS_DIR, 'code-edit.json'),
25
+ 'db-write': path.join(HARNESS_DIR, 'db-write.json'),
26
+ });
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Detection patterns
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const DEPLOY_PATTERNS = [
33
+ /\brailway\s+(deploy|up|run)\b/i,
34
+ /\bdocker\s+(push|build)\b/i,
35
+ /\bnpm\s+publish\b/i,
36
+ /\byarn\s+publish\b/i,
37
+ /\bpnpm\s+publish\b/i,
38
+ /\bgit\s+push\b/i,
39
+ /\bgh\s+pr\s+(create|merge)\b/i,
40
+ /\bchangeset\s+(publish|version)\b/i,
41
+ ];
42
+
43
+ const DB_WRITE_PATTERNS = [
44
+ /\b(DROP|TRUNCATE|DELETE|ALTER|INSERT|UPDATE)\s+(TABLE|FROM|INTO|COLUMN)\b/i,
45
+ /\b(sqlite3|better-sqlite3|knex|sequelize)\b.*\.(run|exec|query)\b/i,
46
+ /\brm\s+.*\.sqlite\b/i,
47
+ /\blancedb\b.*(?:create|delete|drop|truncate)/i,
48
+ /\.db\.exec\(|\.db\.prepare\(/i,
49
+ ];
50
+
51
+ const CODE_EDIT_TOOL_NAMES = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Public API
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /**
58
+ * Given a tool name and input, return the path to the best matching
59
+ * specialized harness config, or null if none applies.
60
+ *
61
+ * @param {string} toolName - e.g. "Bash", "Edit", "Write"
62
+ * @param {object|string} toolInput - raw tool input object or string
63
+ * @returns {string|null} absolute path to harness JSON, or null
64
+ */
65
+ function selectHarness(toolName, toolInput) {
66
+ // 1. Explicit override
67
+ if (process.env.THUMBGATE_HARNESS) {
68
+ const override = process.env.THUMBGATE_HARNESS;
69
+ if (HARNESSES[override]) return HARNESSES[override];
70
+ // Allow absolute path override
71
+ if (path.isAbsolute(override)) return override;
72
+ }
73
+
74
+ // 2. Edit/Write tools always get code-edit harness
75
+ if (CODE_EDIT_TOOL_NAMES.has(toolName)) {
76
+ return HARNESSES['code-edit'];
77
+ }
78
+
79
+ // 3. Inspect command text for Bash tool
80
+ const commandText = extractCommandText(toolInput);
81
+ if (commandText) {
82
+ if (DB_WRITE_PATTERNS.some((p) => p.test(commandText))) {
83
+ return HARNESSES['db-write'];
84
+ }
85
+ if (DEPLOY_PATTERNS.some((p) => p.test(commandText))) {
86
+ return HARNESSES['deploy'];
87
+ }
88
+ }
89
+
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Return the harness name (e.g. "deploy") for a given tool call, or null.
95
+ */
96
+ function selectHarnessName(toolName, toolInput) {
97
+ const harnessPath = selectHarness(toolName, toolInput);
98
+ if (!harnessPath) return null;
99
+ return Object.entries(HARNESSES).find(([, p]) => p === harnessPath)?.[0] ?? null;
100
+ }
101
+
102
+ /**
103
+ * Return the full list of available harness names.
104
+ */
105
+ function listHarnesses() {
106
+ return Object.keys(HARNESSES);
107
+ }
108
+
109
+ /**
110
+ * Return the path for a harness by name.
111
+ */
112
+ function getHarnessPath(name) {
113
+ return HARNESSES[name] ?? null;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Internal helpers
118
+ // ---------------------------------------------------------------------------
119
+
120
+ function extractCommandText(toolInput) {
121
+ if (!toolInput) return '';
122
+ if (typeof toolInput === 'string') return toolInput;
123
+ if (typeof toolInput === 'object') {
124
+ // Claude Code Bash tool: { command: "..." }
125
+ if (typeof toolInput.command === 'string') return toolInput.command;
126
+ // file_path for Edit/Write tools
127
+ if (typeof toolInput.file_path === 'string') return toolInput.file_path;
128
+ // Generic text fields
129
+ for (const key of ['input', 'text', 'content', 'query']) {
130
+ if (typeof toolInput[key] === 'string') return toolInput[key];
131
+ }
132
+ // Fall back to serialised form
133
+ try { return JSON.stringify(toolInput); } catch { return ''; }
134
+ }
135
+ return '';
136
+ }
137
+
138
+ module.exports = {
139
+ selectHarness,
140
+ selectHarnessName,
141
+ listHarnesses,
142
+ getHarnessPath,
143
+ extractCommandText,
144
+ HARNESSES,
145
+ DEPLOY_PATTERNS,
146
+ DB_WRITE_PATTERNS,
147
+ CODE_EDIT_TOOL_NAMES,
148
+ };
@@ -123,6 +123,7 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
123
123
  const proPriceDollars = normalizePriceDollars(env.THUMBGATE_PRO_PRICE_DOLLARS) || DEFAULT_PRO_PRICE_DOLLARS;
124
124
  const proPriceLabel = env.THUMBGATE_PRO_PRICE_LABEL || DEFAULT_PRO_PRICE_LABEL;
125
125
  const gaMeasurementId = normalizeTrackingId(env.THUMBGATE_GA_MEASUREMENT_ID, GA_MEASUREMENT_ID_PATTERN);
126
+ const posthogApiKey = env.POSTHOG_API_KEY || '';
126
127
  const googleSiteVerification = normalizeTrackingId(env.THUMBGATE_GOOGLE_SITE_VERIFICATION);
127
128
 
128
129
  return {
@@ -137,6 +138,7 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
137
138
  proPriceLabel,
138
139
  gaMeasurementId,
139
140
  googleSiteVerification,
141
+ posthogApiKey,
140
142
  };
141
143
  }
142
144
 
@@ -6,6 +6,7 @@ const { spawn } = require('child_process');
6
6
 
7
7
  const runner = require('./async-job-runner');
8
8
  const { buildHarnessJob } = require('./natural-language-harness');
9
+ const { ensureDir } = require('./fs-utils');
9
10
 
10
11
  const RUNNER_SCRIPT_PATH = path.join(__dirname, 'async-job-runner.js');
11
12
  const MANAGED_DPO_EXPORT_SCRIPT_PATH = path.join(__dirname, 'managed-dpo-export.js');
@@ -17,11 +18,6 @@ function nowIso() {
17
18
  return new Date().toISOString();
18
19
  }
19
20
 
20
- function ensureDir(dirPath) {
21
- if (!fs.existsSync(dirPath)) {
22
- fs.mkdirSync(dirPath, { recursive: true });
23
- }
24
- }
25
21
 
26
22
  function shellQuote(value) {
27
23
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
@@ -17,6 +17,7 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { resolveFeedbackDir } = require('./feedback-paths');
20
+ const { readJsonl } = require('./fs-utils');
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Paths
@@ -62,39 +63,12 @@ const POS = new Set([
62
63
  'success', 'pass', 'passed', 'great', 'excellent', 'perfect', 'works',
63
64
  ]);
64
65
 
66
+ const HYBRID_JSONL_READ_LIMIT = 400;
67
+
65
68
  // ---------------------------------------------------------------------------
66
69
  // Low-level helpers
67
70
  // ---------------------------------------------------------------------------
68
71
 
69
- /**
70
- * Read last maxLines of a JSONL file in reverse, then re-reverse so oldest-first.
71
- */
72
- function readJsonl(filePath, maxLines) {
73
- const limit = maxLines !== undefined ? maxLines : 400;
74
- if (!fs.existsSync(filePath)) return [];
75
- let raw;
76
- try {
77
- raw = fs.readFileSync(filePath, 'utf8').trimEnd();
78
- } catch (_) {
79
- return [];
80
- }
81
- if (!raw) return [];
82
- const lines = raw.split('\n');
83
- const slice = lines.slice(-limit);
84
- const parsed = [];
85
- for (let i = slice.length - 1; i >= 0; i--) {
86
- const line = slice[i].trim();
87
- if (!line) continue;
88
- try {
89
- parsed.push(JSON.parse(line));
90
- } catch (_) {
91
- // skip malformed
92
- }
93
- }
94
- parsed.reverse(); // back to chronological order
95
- return parsed;
96
- }
97
-
98
72
  /**
99
73
  * Normalize text: strip /Users/ paths, port numbers, lowercase.
100
74
  */
@@ -208,10 +182,10 @@ function buildHybridState(opts) {
208
182
  const pendingSyncPath = o.pendingSyncPath || process.env.THUMBGATE_PENDING_SYNC || paths.pendingSync;
209
183
  const attributedFeedbackPath = o.attributedFeedbackPath || process.env.THUMBGATE_ATTRIBUTED_FEEDBACK || paths.attributedFeedback;
210
184
 
211
- const feedbackEntries = readJsonl(feedbackLogPath);
212
- const inboxEntries = readJsonl(inboxPath);
213
- const pendingSyncEntries = readJsonl(pendingSyncPath);
214
- const attributedEntries = readJsonl(attributedFeedbackPath);
185
+ const feedbackEntries = readJsonl(feedbackLogPath, HYBRID_JSONL_READ_LIMIT);
186
+ const inboxEntries = readJsonl(inboxPath, HYBRID_JSONL_READ_LIMIT);
187
+ const pendingSyncEntries = readJsonl(pendingSyncPath, HYBRID_JSONL_READ_LIMIT);
188
+ const attributedEntries = readJsonl(attributedFeedbackPath, HYBRID_JSONL_READ_LIMIT);
215
189
 
216
190
  // Deduplicate by id across all sources
217
191
  const seen = new Set();
@@ -534,25 +508,21 @@ function evaluateCompiledGuards(artifact, toolName, toolInput) {
534
508
  const normTool = (toolName || '').toLowerCase();
535
509
 
536
510
  for (const guard of artifact.guards) {
537
- // Check if tool context is relevant
538
511
  const guardText = normalize(guard.text || '');
539
512
  const toolMentioned = guardText.includes(normTool) || normTool === 'unknown';
540
513
 
541
- if (hasTwoKeywordHits(normInput, guard.words || [])) {
542
- return {
543
- mode: guard.mode || 'warn',
544
- reason: `Matched guard pattern (count: ${guard.count}): "${(guard.text || '').slice(0, 80)}"`,
545
- source: 'compiled',
546
- guardHash: guard.hash,
547
- attributed: guard.attributed,
548
- };
549
- }
514
+ const keywordMatch = hasTwoKeywordHits(normInput, guard.words || []);
550
515
 
551
- // Also check tool-level match when input is empty or short
552
- if (normInput.length < 10 && toolMentioned && guard.count >= (artifact.blockThreshold || 3)) {
516
+ // Match if: keyword hits in input, OR tool mentioned + high count.
517
+ // Previously tool-name matching only worked for short inputs this was
518
+ // a false-negative gap that let tool-specific patterns slip through.
519
+ if (keywordMatch || (toolMentioned && guard.count >= (artifact.blockThreshold || 3))) {
520
+ const reason = keywordMatch
521
+ ? `Matched guard pattern (count: ${guard.count}): "${(guard.text || '').slice(0, 80)}"`
522
+ : `Tool "${toolName}" has recurring negative patterns (count: ${guard.count})`;
553
523
  return {
554
524
  mode: guard.mode || 'warn',
555
- reason: `Tool "${toolName}" has recurring negative patterns (count: ${guard.count})`,
525
+ reason,
556
526
  source: 'compiled',
557
527
  guardHash: guard.hash,
558
528
  attributed: guard.attributed,
@@ -622,6 +592,12 @@ function evaluatePretoolFromState(state, toolName, toolInput) {
622
592
  * @param {string} [opts.attributedFeedbackPath]
623
593
  * @returns {{ mode: 'block'|'warn'|'allow', reason: string, source: string }}
624
594
  */
595
+ /**
596
+ * Max age (ms) before compiled guards are considered stale and live state
597
+ * is also consulted. Default: 1 hour.
598
+ */
599
+ const GUARD_STALENESS_MS = 60 * 60 * 1000;
600
+
625
601
  function evaluatePretool(toolName, toolInput, opts) {
626
602
  const o = opts || {};
627
603
 
@@ -631,11 +607,18 @@ function evaluatePretool(toolName, toolInput, opts) {
631
607
  if (artifact) {
632
608
  const result = evaluateCompiledGuards(artifact, toolName, toolInput);
633
609
  if (result.mode !== 'allow') return result;
634
- // Even if compiled says allow, we're done (trust compiled)
635
- return result;
610
+
611
+ // Check staleness: if compiled artifact is fresh enough, trust it
612
+ const compiledAt = artifact.compiledAt ? Date.parse(artifact.compiledAt) : 0;
613
+ const age = Date.now() - compiledAt;
614
+ if (age < GUARD_STALENESS_MS) {
615
+ return result; // Fresh compiled artifact says allow — trust it
616
+ }
617
+ // Stale artifact said allow — fall through to live evaluation
618
+ // in case new feedback was captured since compilation
636
619
  }
637
620
 
638
- // Slow path: build live state
621
+ // Slow path: build live state (also used when compiled guards are stale)
639
622
  const state = buildHybridState({
640
623
  feedbackLogPath: o.feedbackLogPath,
641
624
  attributedFeedbackPath: o.attributedFeedbackPath,
@@ -709,6 +692,7 @@ module.exports = {
709
692
  readJsonl,
710
693
  getHybridPaths,
711
694
  PATHS,
695
+ GUARD_STALENESS_MS,
712
696
  };
713
697
 
714
698
  if (require.main === module) {
@@ -4,6 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { resolveFeedbackDir } = require('./feedback-paths');
7
+ const { getDecisionLogPath, readDecisionLog, collapseDecisionTimeline } = require('./decision-journal');
7
8
 
8
9
  const LABELS = ['allow', 'recall', 'verify', 'warn', 'deny'];
9
10
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -323,14 +324,64 @@ function buildDiagnosticExample(entry) {
323
324
  };
324
325
  }
325
326
 
327
+ function deriveLabelFromDecisionOutcome(outcome) {
328
+ const status = normalizeText(outcome && outcome.outcome);
329
+ const actualDecision = normalizeText(outcome && outcome.actualDecision);
330
+ if (status === 'blocked' || status === 'rolled_back' || actualDecision === 'deny') return 'deny';
331
+ if (status === 'warned' || status === 'overridden' || actualDecision === 'warn') return 'warn';
332
+ if (status === 'accepted' || status === 'completed') return 'allow';
333
+ if (status === 'aborted') return 'warn';
334
+ return null;
335
+ }
336
+
337
+ function buildDecisionExample(action) {
338
+ const evaluation = action && action.evaluation ? action.evaluation : null;
339
+ const latestOutcome = action && Array.isArray(action.outcomes) && action.outcomes.length > 0
340
+ ? action.outcomes[action.outcomes.length - 1]
341
+ : null;
342
+ const label = deriveLabelFromDecisionOutcome(latestOutcome);
343
+ if (!evaluation || !latestOutcome || !label) return null;
344
+
345
+ const recommendation = evaluation.recommendation || {};
346
+ const blastRadius = evaluation.blastRadius || {};
347
+ const toolInput = evaluation.toolInput && typeof evaluation.toolInput === 'object' ? evaluation.toolInput : {};
348
+ const changedFiles = Array.isArray(evaluation.changedFiles) ? evaluation.changedFiles : [];
349
+ const tokens = buildFeatureTokens([
350
+ 'kind:decision',
351
+ `tool:${evaluation.toolName || latestOutcome.toolName || 'unknown'}`,
352
+ `decision:${recommendation.decision || 'allow'}`,
353
+ `execution:${recommendation.executionMode || 'auto_execute'}`,
354
+ `owner:${recommendation.decisionOwner || 'agent'}`,
355
+ `reversibility:${recommendation.reversibility || 'reviewable'}`,
356
+ recommendation.riskBand ? `risk:${recommendation.riskBand}` : null,
357
+ blastRadius.severity ? `blast:${blastRadius.severity}` : null,
358
+ latestOutcome.outcome ? `outcome:${latestOutcome.outcome}` : null,
359
+ latestOutcome.actor ? `actor:${latestOutcome.actor}` : null,
360
+ ...extractCommandTokens(toolInput.command || ''),
361
+ ...changedFiles.flatMap((filePath) => extractFileTokens(filePath)),
362
+ ...tokenizeText([recommendation.summary, latestOutcome.notes].filter(Boolean).join(' '), 10).map((token) => `decisiontok:${token}`),
363
+ ]);
364
+
365
+ if (!tokens.length) return null;
366
+ return {
367
+ id: latestOutcome.actionId || evaluation.actionId || null,
368
+ source: 'decision',
369
+ label,
370
+ timestamp: latestOutcome.timestamp || evaluation.timestamp || new Date().toISOString(),
371
+ tokens,
372
+ };
373
+ }
374
+
326
375
  function buildExamplesFromFeedbackDir(feedbackDir) {
327
376
  const resolvedDir = resolveFeedbackDir({ feedbackDir });
328
377
  const feedbackEntries = readJSONL(path.join(resolvedDir, 'feedback-log.jsonl'));
329
378
  const auditEntries = readJSONL(path.join(resolvedDir, 'audit-trail.jsonl'));
330
379
  const diagnosticEntries = readJSONL(path.join(resolvedDir, 'diagnostic-log.jsonl'));
380
+ const decisionEntries = readDecisionLog(getDecisionLogPath(resolvedDir));
381
+ const decisions = collapseDecisionTimeline(decisionEntries);
331
382
 
332
383
  const examples = [];
333
- const sourceCounts = { feedback: 0, audit: 0, diagnostic: 0 };
384
+ const sourceCounts = { feedback: 0, audit: 0, diagnostic: 0, decision: 0 };
334
385
 
335
386
  for (const entry of feedbackEntries) {
336
387
  const example = buildFeedbackExample(entry);
@@ -350,6 +401,12 @@ function buildExamplesFromFeedbackDir(feedbackDir) {
350
401
  sourceCounts.diagnostic += 1;
351
402
  examples.push(example);
352
403
  }
404
+ for (const action of decisions) {
405
+ const example = buildDecisionExample(action);
406
+ if (!example) continue;
407
+ sourceCounts.decision += 1;
408
+ examples.push(example);
409
+ }
353
410
 
354
411
  examples.sort((left, right) => {
355
412
  return Date.parse(left.timestamp || 0) - Date.parse(right.timestamp || 0);
@@ -14,6 +14,7 @@
14
14
 
15
15
  const path = require('node:path');
16
16
  const fs = require('node:fs');
17
+ const { readJsonl } = require('./fs-utils');
17
18
 
18
19
  const PROJECT_ROOT = path.join(__dirname, '..');
19
20
  const DEFAULT_DB_PATH = path.join(PROJECT_ROOT, '.claude', 'memory', 'lessons.sqlite');
@@ -495,8 +496,8 @@ function backfillFromJsonl(db, feedbackDir) {
495
496
  const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
496
497
  const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
497
498
 
498
- const feedbackEntries = readJsonlSafe(feedbackLogPath);
499
- const memoryEntries = readJsonlSafe(memoryLogPath);
499
+ const feedbackEntries = readJsonl(feedbackLogPath);
500
+ const memoryEntries = readJsonl(memoryLogPath);
500
501
 
501
502
  // Index memories by sourceFeedbackId for joining
502
503
  const memoryByFeedbackId = new Map();
@@ -581,22 +582,6 @@ function safeParseTags(tagsStr) {
581
582
  }
582
583
  }
583
584
 
584
- function readJsonlSafe(filePath) {
585
- if (!fs.existsSync(filePath)) return [];
586
- const raw = fs.readFileSync(filePath, 'utf-8').trim();
587
- if (!raw) return [];
588
- return raw
589
- .split('\n')
590
- .map((line) => {
591
- try {
592
- return JSON.parse(line);
593
- } catch {
594
- return null;
595
- }
596
- })
597
- .filter(Boolean);
598
- }
599
-
600
585
  module.exports = {
601
586
  initDB,
602
587
  upsertLesson,