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,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reddit Thread Finder for ThumbGate Engagement
4
+ *
5
+ * Add to package.json:
6
+ * "content:reddit": "node scripts/content-engine/reddit-thread-finder.js"
7
+ * "content:reddit:dry": "node scripts/content-engine/reddit-thread-finder.js --dry-run"
8
+ * "content:reddit:limit": "node scripts/content-engine/reddit-thread-finder.js --limit 20"
9
+ */
10
+
11
+ const https = require('https');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const SUBREDDITS = [
16
+ 'ChatGPTCoding', 'ClaudeAI', 'cursor', 'devops',
17
+ 'SoftwareEngineering', 'ExperiencedDevs', 'MachineLearning', 'LocalLLaMA'
18
+ ];
19
+
20
+ const KEYWORDS = [
21
+ 'agent broke', 'agent deleted', 'force push', 'prevent AI from',
22
+ 'guardrails', 'agent governance', 'coding agent mistakes'
23
+ ];
24
+
25
+ const USER_AGENT = 'script:thumbgate-content:v1.0';
26
+ const DELAY = 2000; // ms between requests
27
+ const DEFAULT_LIMIT = 10;
28
+
29
+ let dryRun = false;
30
+ let outputLimit = DEFAULT_LIMIT;
31
+
32
+ process.argv.slice(2).forEach(arg => {
33
+ if (arg === '--dry-run') dryRun = true;
34
+ if (arg.startsWith('--limit')) outputLimit = parseInt(arg.split('=')[1] || arg.split(' ')[1], 10);
35
+ });
36
+
37
+ function fetchReddit(url) {
38
+ return new Promise((resolve, reject) => {
39
+ const req = https.get(url, { headers: { 'User-Agent': USER_AGENT } }, res => {
40
+ let data = '';
41
+ res.on('data', chunk => data += chunk);
42
+ res.on('end', () => {
43
+ try {
44
+ resolve(JSON.parse(data));
45
+ } catch (e) {
46
+ reject(e);
47
+ }
48
+ });
49
+ });
50
+ req.on('error', reject);
51
+ });
52
+ }
53
+
54
+ async function searchSubreddit(sub, keyword) {
55
+ const url = `https://www.reddit.com/r/${sub}/search.json?q=${encodeURIComponent(keyword)}&sort=new&t=week&limit=5`;
56
+ try {
57
+ const data = await fetchReddit(url);
58
+ return (data.data?.children || []).map(post => ({
59
+ id: post.data.id,
60
+ title: post.data.title,
61
+ url: `https://reddit.com${post.data.permalink}`,
62
+ subreddit: post.data.subreddit,
63
+ score: post.data.score,
64
+ numComments: post.data.num_comments,
65
+ created: post.data.created_utc,
66
+ selftext: post.data.selftext
67
+ }));
68
+ } catch (err) {
69
+ console.error(`Error searching ${sub} for "${keyword}": ${err.message}`);
70
+ return [];
71
+ }
72
+ }
73
+
74
+ function scoreThread(thread) {
75
+ const now = Math.floor(Date.now() / 1000);
76
+ const ageHours = (now - thread.created) / 3600;
77
+ const recencyScore = Math.max(0, 1 - ageHours / 168); // 0-1 over a week
78
+ const upvoteScore = Math.log(Math.max(1, thread.score)) / Math.log(100);
79
+ const commentScore = Math.log(Math.max(1, thread.numComments)) / Math.log(100);
80
+
81
+ return (recencyScore * 0.5) + (upvoteScore * 0.3) + (commentScore * 0.2);
82
+ }
83
+
84
+ function generateReply(thread) {
85
+ const context = thread.selftext.substring(0, 200);
86
+ return `
87
+ **ThumbGate can help prevent this.** Our pre-action gates catch agent mistakes before they happen:
88
+ - Stop force pushes on protected branches
89
+ - Prevent deletions of critical files
90
+ - Verify AI actions before execution
91
+ - Capture lessons from failures to block similar mistakes
92
+
93
+ Learn more: https://thumbgate-production.up.railway.app/dashboard
94
+ `;
95
+ }
96
+
97
+ async function main() {
98
+ console.log(`[${new Date().toISOString()}] Starting Reddit thread finder...`);
99
+
100
+ const threads = {};
101
+ let requestCount = 0;
102
+
103
+ for (const sub of SUBREDDITS) {
104
+ for (const keyword of KEYWORDS) {
105
+ if (requestCount > 0) await new Promise(r => setTimeout(r, DELAY));
106
+
107
+ console.log(` Searching r/${sub} for "${keyword}"...`);
108
+ const results = await searchSubreddit(sub, keyword);
109
+
110
+ results.forEach(thread => {
111
+ if (!threads[thread.id]) {
112
+ threads[thread.id] = thread;
113
+ }
114
+ });
115
+ requestCount++;
116
+ }
117
+ }
118
+
119
+ const sorted = Object.values(threads)
120
+ .sort((a, b) => scoreThread(b) - scoreThread(a))
121
+ .slice(0, outputLimit);
122
+
123
+ const date = new Date().toISOString().split('T')[0];
124
+ const outputDir = path.join(__dirname, 'output');
125
+
126
+ if (!fs.existsSync(outputDir)) {
127
+ fs.mkdirSync(outputDir, { recursive: true });
128
+ }
129
+
130
+ let markdown = `# Reddit Threads - ${date}\n\nFound ${sorted.length} high-relevance threads.\n\n`;
131
+
132
+ sorted.forEach((thread, idx) => {
133
+ const score = scoreThread(thread);
134
+ markdown += `## ${idx + 1}. ${thread.title}\n`;
135
+ markdown += `**r/${thread.subreddit}** | [Link](${thread.url}) | Score: ${thread.score} | Comments: ${thread.numComments}\n`;
136
+ markdown += `**Relevance Score:** ${score.toFixed(2)}\n\n`;
137
+
138
+ if (!dryRun) {
139
+ markdown += `**Suggested Reply:**\n${generateReply(thread)}\n\n`;
140
+ }
141
+ markdown += '---\n\n';
142
+ });
143
+
144
+ const outputFile = path.join(outputDir, `reddit-threads-${date}.md`);
145
+ fs.writeFileSync(outputFile, markdown);
146
+
147
+ console.log(`\n✅ Generated ${sorted.length} threads to ${outputFile}`);
148
+ if (dryRun) console.log(' (--dry-run: no reply suggestions included)');
149
+ }
150
+
151
+ main().catch(err => {
152
+ console.error('❌ Fatal error:', err.message);
153
+ process.exit(1);
154
+ });
@@ -20,6 +20,7 @@ const fs = require('fs');
20
20
  const path = require('path');
21
21
  const crypto = require('crypto');
22
22
  const { constructContextPack } = require('./contextfs');
23
+ const { ensureDir } = require('./fs-utils');
23
24
 
24
25
  // ---------------------------------------------------------------------------
25
26
  // Default paths
@@ -75,11 +76,6 @@ const TOOL_CONSOLIDATION = {
75
76
  // Utility: ensure directory exists
76
77
  // ---------------------------------------------------------------------------
77
78
 
78
- function ensureDir(dirPath) {
79
- if (!fs.existsSync(dirPath)) {
80
- fs.mkdirSync(dirPath, { recursive: true });
81
- }
82
- }
83
79
 
84
80
  // ---------------------------------------------------------------------------
85
81
  // Knowledge Bundle Builder
@@ -649,10 +645,29 @@ function compactContext(entries, anchors, opts) {
649
645
  return true;
650
646
  });
651
647
 
648
+ // Stage 6: Global token budget — drop entries (oldest first) until total chars fit
649
+ let finalStage = 5;
650
+ const totalMaxChars = typeof options.totalMaxChars === 'number' ? options.totalMaxChars : null;
651
+ if (totalMaxChars !== null) {
652
+ let budget = totalMaxChars;
653
+ const budgeted = [];
654
+ // Iterate newest-first so most recent entries are preserved
655
+ for (let i = working.length - 1; i >= 0; i--) {
656
+ const entrySize = JSON.stringify(working[i]).length;
657
+ if (budget - entrySize < 0) break;
658
+ budget -= entrySize;
659
+ budgeted.unshift(working[i]);
660
+ }
661
+ if (budgeted.length < working.length) {
662
+ working = budgeted;
663
+ finalStage = 6;
664
+ }
665
+ }
666
+
652
667
  const removedCount = initial - working.length;
653
668
  return {
654
669
  entries: [...anchorEntries, ...working],
655
- stage: 5,
670
+ stage: finalStage,
656
671
  removedCount,
657
672
  compacted: removedCount > 0,
658
673
  };
@@ -12,6 +12,7 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const crypto = require('crypto');
14
14
  const { resolveFeedbackDir } = require('./feedback-paths');
15
+ const { ensureDir, readJsonl } = require('./fs-utils');
15
16
  const {
16
17
  retrieveHierarchicalDocuments,
17
18
  shouldUseHierarchicalRetrieval,
@@ -99,11 +100,6 @@ const PACK_TEMPLATES = {
99
100
  },
100
101
  };
101
102
 
102
- function ensureDir(dirPath) {
103
- if (!fs.existsSync(dirPath)) {
104
- fs.mkdirSync(dirPath, { recursive: true });
105
- }
106
- }
107
103
 
108
104
  function ensureContextFs() {
109
105
  Object.values(NAMESPACES).forEach((subPath) => {
@@ -140,22 +136,6 @@ function appendJsonl(filePath, payload) {
140
136
  fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`);
141
137
  }
142
138
 
143
- function readJsonl(filePath) {
144
- if (!fs.existsSync(filePath)) return [];
145
- const raw = fs.readFileSync(filePath, 'utf-8').trim();
146
- if (!raw) return [];
147
- return raw
148
- .split('\n')
149
- .map((line) => {
150
- try {
151
- return JSON.parse(line);
152
- } catch {
153
- return null;
154
- }
155
- })
156
- .filter(Boolean);
157
- }
158
-
159
139
  function listJsonFiles(dirPath) {
160
140
  if (!fs.existsSync(dirPath)) return [];
161
141
  const files = fs.readdirSync(dirPath, { withFileTypes: true });
@@ -21,6 +21,7 @@ const { getSettingsStatus } = require('./settings-hierarchy');
21
21
  const { summarizeWorkflowRuns } = require('./workflow-runs');
22
22
  const { searchLessons } = require('./lesson-search');
23
23
  const { getInterventionPolicySummary } = require('./intervention-policy');
24
+ const { computeDecisionMetrics } = require('./decision-journal');
24
25
 
25
26
  const PROJECT_ROOT = path.join(__dirname, '..');
26
27
  const DEFAULT_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
@@ -787,6 +788,7 @@ function generateDashboard(feedbackDir, options = {}) {
787
788
  const readiness = generateAgentReadinessReport({ projectRoot: PROJECT_ROOT });
788
789
  const harness = computeHarnessOverview(feedbackDir, entries);
789
790
  const interventionPolicy = getInterventionPolicySummary(feedbackDir);
791
+ const decisions = computeDecisionMetrics(feedbackDir);
790
792
  const settingsStatus = getSettingsStatus({ projectRoot: PROJECT_ROOT });
791
793
  settingsStatus.routingPreview = {
792
794
  dashboardTool: routeProfile({
@@ -820,6 +822,13 @@ function generateDashboard(feedbackDir, options = {}) {
820
822
  lessonEffectiveness: { rate: totalNeg > 0 ? Math.round((autoGates / totalNeg) * 10000) / 100 : 0, totalNegative: totalNeg, autoGatesCreated: autoGates },
821
823
  errorTrend: { direction: lastWeekNeg > 0 ? (negRecent.length < lastWeekNeg ? 'improving' : negRecent.length > lastWeekNeg ? 'worsening' : 'stable') : (negRecent.length > 0 ? 'new-errors' : 'clean'), thisWeek: negRecent.length, lastWeek: lastWeekNeg },
822
824
  weeklyActivity: { positive: posRecent.length, negative: negRecent.length, total: recentEntries.length },
825
+ decisionLoop: {
826
+ fastPathRate: decisions.fastPathRate,
827
+ overrideRate: decisions.overrideRate,
828
+ rollbackRate: decisions.rollbackRate,
829
+ medianLatencyMs: decisions.medianLatencyMs,
830
+ resolvedCount: decisions.resolvedCount,
831
+ },
823
832
  };
824
833
 
825
834
  const team = generateOrgDashboard({
@@ -857,6 +866,7 @@ function generateDashboard(feedbackDir, options = {}) {
857
866
  instrumentation,
858
867
  readiness,
859
868
  interventionPolicy,
869
+ decisions,
860
870
  settingsStatus,
861
871
  team,
862
872
  templateLibrary,
@@ -886,6 +896,7 @@ function printDashboard(data) {
886
896
  instrumentation,
887
897
  readiness,
888
898
  interventionPolicy,
899
+ decisions,
889
900
  settingsStatus,
890
901
  team,
891
902
  templateLibrary,
@@ -945,6 +956,14 @@ function printDashboard(data) {
945
956
  console.log(` Top Deny Signal : ${interventionPolicy.topTokens.deny[0].token}`);
946
957
  }
947
958
 
959
+ console.log('');
960
+ console.log('🧭 Decision Loop');
961
+ console.log(` Evaluations : ${decisions.evaluationCount}`);
962
+ console.log(` Fast Path : ${Math.round((decisions.fastPathRate || 0) * 100)}%`);
963
+ console.log(` Override Rate : ${Math.round((decisions.overrideRate || 0) * 100)}%`);
964
+ console.log(` Rollback Rate : ${Math.round((decisions.rollbackRate || 0) * 100)}%`);
965
+ console.log(` Median Latency : ${Math.round((decisions.medianLatencyMs || 0) / 1000)}s`);
966
+
948
967
  console.log('');
949
968
  console.log('🎯 North Star');
950
969
  console.log(` Weekly Proof Runs: ${analytics.northStar.weeklyActiveProofBackedWorkflowRuns}`);
@@ -1120,6 +1139,7 @@ module.exports = {
1120
1139
  generateDashboard,
1121
1140
  printDashboard,
1122
1141
  computeApprovalStats,
1142
+ computeDecisionMetrics,
1123
1143
  computeGateStats,
1124
1144
  computePreventionImpact,
1125
1145
  computeSessionTrend,
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { resolveFeedbackDir } = require('./feedback-paths');
8
+ const { sanitizeToolInput } = require('./audit-trail');
9
+ const { ensureDir } = require('./fs-utils');
10
+
11
+ const DECISION_LOG_FILENAME = 'decision-journal.jsonl';
12
+ const DEFAULT_DAY_COUNT = 14;
13
+ const RESOLVED_OUTCOMES = new Set(['accepted', 'completed', 'overridden', 'rolled_back', 'blocked', 'aborted']);
14
+ const DECISION_OUTCOMES = new Set([...RESOLVED_OUTCOMES, 'warned']);
15
+
16
+ function getDecisionLogPath(feedbackDir) {
17
+ return path.join(resolveFeedbackDir({ feedbackDir }), DECISION_LOG_FILENAME);
18
+ }
19
+
20
+
21
+ function buildActionId(prefix = 'decision') {
22
+ return `${prefix}_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
23
+ }
24
+
25
+ function readDecisionLog(logPath) {
26
+ const targetPath = logPath || getDecisionLogPath();
27
+ if (!fs.existsSync(targetPath)) return [];
28
+ const raw = fs.readFileSync(targetPath, 'utf8').trim();
29
+ if (!raw) return [];
30
+ return raw
31
+ .split('\n')
32
+ .map((line) => {
33
+ try {
34
+ return JSON.parse(line);
35
+ } catch {
36
+ return null;
37
+ }
38
+ })
39
+ .filter(Boolean);
40
+ }
41
+
42
+ function appendDecisionRecord(record, feedbackDir) {
43
+ const logPath = getDecisionLogPath(feedbackDir);
44
+ ensureDir(path.dirname(logPath));
45
+ fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`, 'utf8');
46
+ return record;
47
+ }
48
+
49
+ function toLocalDayKey(value) {
50
+ const ts = value instanceof Date ? value : new Date(value);
51
+ if (Number.isNaN(ts.getTime())) return null;
52
+ const year = ts.getFullYear();
53
+ const month = String(ts.getMonth() + 1).padStart(2, '0');
54
+ const day = String(ts.getDate()).padStart(2, '0');
55
+ return `${year}-${month}-${day}`;
56
+ }
57
+
58
+ function normalizeOutcome(value) {
59
+ const normalized = String(value || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
60
+ if (DECISION_OUTCOMES.has(normalized)) return normalized;
61
+ return 'completed';
62
+ }
63
+
64
+ function inferActualDecision(outcome, fallback) {
65
+ if (fallback) return String(fallback);
66
+ if (outcome === 'blocked') return 'deny';
67
+ if (outcome === 'warned') return 'warn';
68
+ return 'allow';
69
+ }
70
+
71
+ function median(values) {
72
+ const sorted = values
73
+ .map((value) => Number(value))
74
+ .filter((value) => Number.isFinite(value))
75
+ .sort((left, right) => left - right);
76
+ if (sorted.length === 0) return 0;
77
+ const middle = Math.floor(sorted.length / 2);
78
+ if (sorted.length % 2 === 1) return sorted[middle];
79
+ return Math.round((sorted[middle - 1] + sorted[middle]) / 2);
80
+ }
81
+
82
+ function summarizeBlastRadius(report = {}) {
83
+ const blastRadius = report.blastRadius || {};
84
+ return {
85
+ severity: blastRadius.severity || 'low',
86
+ fileCount: Number(blastRadius.fileCount || 0),
87
+ surfaceCount: Number(blastRadius.surfaceCount || 0),
88
+ releaseSensitiveCount: Array.isArray(blastRadius.releaseSensitiveFiles) ? blastRadius.releaseSensitiveFiles.length : 0,
89
+ protectedWithoutApprovalCount: Array.isArray(blastRadius.unapprovedProtectedFiles) ? blastRadius.unapprovedProtectedFiles.length : 0,
90
+ summary: blastRadius.summary || '',
91
+ };
92
+ }
93
+
94
+ function normalizeRecommendation(report = {}) {
95
+ const control = report.decisionControl || {};
96
+ return {
97
+ decision: report.decision || 'allow',
98
+ riskScore: Number(report.riskScore || 0),
99
+ riskBand: report.band || 'low',
100
+ executionMode: control.executionMode || (report.decision === 'deny' ? 'blocked' : report.decision === 'warn' ? 'checkpoint_required' : 'auto_execute'),
101
+ decisionOwner: control.decisionOwner || (report.decision === 'allow' ? 'agent' : 'shared'),
102
+ reversibility: control.reversibility || 'reviewable',
103
+ requiresHumanApproval: control.requiresHumanApproval === true,
104
+ summary: report.summary || '',
105
+ recommendedAction: control.recommendedAction || (report.decision === 'deny' ? 'halt' : report.decision === 'warn' ? 'review' : 'proceed'),
106
+ };
107
+ }
108
+
109
+ function recordDecisionEvaluation(report, params = {}, options = {}) {
110
+ const actionId = params.actionId || buildActionId();
111
+ const changedFiles = Array.isArray(params.changedFiles)
112
+ ? params.changedFiles.slice()
113
+ : Array.isArray(report && report.blastRadius && report.blastRadius.affectedFiles)
114
+ ? report.blastRadius.affectedFiles.slice()
115
+ : [];
116
+ const record = {
117
+ recordType: 'evaluation',
118
+ actionId,
119
+ timestamp: params.timestamp || new Date().toISOString(),
120
+ source: params.source || 'workflow-sentinel',
121
+ toolName: params.toolName || report.toolName || 'unknown',
122
+ toolInput: sanitizeToolInput(params.toolInput || {}),
123
+ changedFiles,
124
+ recommendation: normalizeRecommendation(report),
125
+ blastRadius: summarizeBlastRadius(report),
126
+ learnedPolicy: report.learnedPolicy && report.learnedPolicy.enabled
127
+ ? {
128
+ label: report.learnedPolicy.prediction && report.learnedPolicy.prediction.label || null,
129
+ confidence: Number((report.learnedPolicy.prediction && report.learnedPolicy.prediction.confidence) || 0),
130
+ }
131
+ : null,
132
+ topRemediations: Array.isArray(report.remediations)
133
+ ? report.remediations.slice(0, 3).map((entry) => ({ id: entry.id, title: entry.title }))
134
+ : [],
135
+ evidence: Array.isArray(report.evidence) ? report.evidence.slice(0, 4) : [],
136
+ };
137
+ return appendDecisionRecord(record, options.feedbackDir);
138
+ }
139
+
140
+ function recordDecisionOutcome(params = {}, options = {}) {
141
+ const actionId = params.actionId || buildActionId('decision_outcome');
142
+ const entries = readDecisionLog(getDecisionLogPath(options.feedbackDir));
143
+ const evaluation = [...entries]
144
+ .reverse()
145
+ .find((entry) => entry && entry.recordType === 'evaluation' && entry.actionId === actionId) || null;
146
+ const outcome = normalizeOutcome(params.outcome);
147
+ const timestamp = params.timestamp || new Date().toISOString();
148
+ const latencyMs = Number.isFinite(params.latencyMs)
149
+ ? Number(params.latencyMs)
150
+ : evaluation && evaluation.timestamp
151
+ ? Math.max(0, new Date(timestamp).getTime() - new Date(evaluation.timestamp).getTime())
152
+ : null;
153
+ const record = {
154
+ recordType: 'outcome',
155
+ actionId,
156
+ timestamp,
157
+ source: params.source || 'api',
158
+ actor: params.actor || 'human',
159
+ outcome,
160
+ actualDecision: inferActualDecision(outcome, params.actualDecision),
161
+ notes: params.notes || '',
162
+ metadata: params.metadata && typeof params.metadata === 'object' ? params.metadata : {},
163
+ latencyMs: Number.isFinite(latencyMs) ? latencyMs : null,
164
+ recommendation: evaluation ? evaluation.recommendation : (params.recommendation || null),
165
+ toolName: evaluation ? evaluation.toolName : (params.toolName || 'unknown'),
166
+ changedFiles: evaluation ? evaluation.changedFiles : (Array.isArray(params.changedFiles) ? params.changedFiles.slice() : []),
167
+ };
168
+ return appendDecisionRecord(record, options.feedbackDir);
169
+ }
170
+
171
+ function collapseDecisionTimeline(records) {
172
+ const actions = new Map();
173
+ for (const record of records) {
174
+ if (!record || !record.actionId) continue;
175
+ if (!actions.has(record.actionId)) {
176
+ actions.set(record.actionId, { actionId: record.actionId, evaluation: null, outcomes: [] });
177
+ }
178
+ const bucket = actions.get(record.actionId);
179
+ if (record.recordType === 'evaluation') {
180
+ bucket.evaluation = record;
181
+ } else if (record.recordType === 'outcome') {
182
+ bucket.outcomes.push(record);
183
+ }
184
+ }
185
+ for (const bucket of actions.values()) {
186
+ bucket.outcomes.sort((left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime());
187
+ }
188
+ return [...actions.values()].sort((left, right) => {
189
+ const leftTs = left.evaluation ? new Date(left.evaluation.timestamp).getTime() : 0;
190
+ const rightTs = right.evaluation ? new Date(right.evaluation.timestamp).getTime() : 0;
191
+ return leftTs - rightTs;
192
+ });
193
+ }
194
+
195
+ function initializeDaySeries(dayCount) {
196
+ const today = new Date();
197
+ today.setHours(0, 0, 0, 0);
198
+ const days = [];
199
+ for (let offset = dayCount - 1; offset >= 0; offset -= 1) {
200
+ const day = new Date(today);
201
+ day.setDate(today.getDate() - offset);
202
+ days.push({
203
+ dayKey: toLocalDayKey(day),
204
+ evaluations: 0,
205
+ fastPath: 0,
206
+ checkpoint: 0,
207
+ blockedRecommendations: 0,
208
+ overrides: 0,
209
+ rollbacks: 0,
210
+ completions: 0,
211
+ blockedOutcomes: 0,
212
+ latencies: [],
213
+ });
214
+ }
215
+ return days;
216
+ }
217
+
218
+ function safeRate(numerator, denominator) {
219
+ if (!denominator) return 0;
220
+ return Number((numerator / denominator).toFixed(4));
221
+ }
222
+
223
+ function computeDecisionMetrics(feedbackDir, options = {}) {
224
+ const dayCount = Number.isInteger(options.dayCount) ? options.dayCount : DEFAULT_DAY_COUNT;
225
+ const records = readDecisionLog(getDecisionLogPath(feedbackDir));
226
+ const actions = collapseDecisionTimeline(records).filter((entry) => entry.evaluation);
227
+ const series = initializeDaySeries(dayCount);
228
+ const dayMap = new Map(series.map((day) => [day.dayKey, day]));
229
+ const outcomeCounts = {
230
+ accepted: 0,
231
+ completed: 0,
232
+ overridden: 0,
233
+ rolled_back: 0,
234
+ blocked: 0,
235
+ aborted: 0,
236
+ warned: 0,
237
+ };
238
+
239
+ let fastPathCount = 0;
240
+ let checkpointCount = 0;
241
+ let blockedRecommendationCount = 0;
242
+ let overrideCount = 0;
243
+ let rollbackCount = 0;
244
+ let resolvedCount = 0;
245
+ const latencyValues = [];
246
+
247
+ for (const action of actions) {
248
+ const evaluation = action.evaluation;
249
+ const recommendation = evaluation.recommendation || {};
250
+ const evalDay = dayMap.get(toLocalDayKey(evaluation.timestamp));
251
+ if (evalDay) {
252
+ evalDay.evaluations += 1;
253
+ if (recommendation.executionMode === 'auto_execute') evalDay.fastPath += 1;
254
+ if (recommendation.executionMode === 'checkpoint_required') evalDay.checkpoint += 1;
255
+ if (recommendation.executionMode === 'blocked') evalDay.blockedRecommendations += 1;
256
+ }
257
+
258
+ if (recommendation.executionMode === 'auto_execute') fastPathCount += 1;
259
+ if (recommendation.executionMode === 'checkpoint_required') checkpointCount += 1;
260
+ if (recommendation.executionMode === 'blocked') blockedRecommendationCount += 1;
261
+
262
+ const hasOverride = action.outcomes.some((outcome) => outcome.outcome === 'overridden');
263
+ const hasRollback = action.outcomes.some((outcome) => outcome.outcome === 'rolled_back');
264
+ if (hasOverride) overrideCount += 1;
265
+ if (hasRollback) rollbackCount += 1;
266
+
267
+ const latestOutcome = action.outcomes.length > 0 ? action.outcomes[action.outcomes.length - 1] : null;
268
+ if (latestOutcome) {
269
+ outcomeCounts[latestOutcome.outcome] = (outcomeCounts[latestOutcome.outcome] || 0) + 1;
270
+ const outcomeDay = dayMap.get(toLocalDayKey(latestOutcome.timestamp));
271
+ if (outcomeDay) {
272
+ if (latestOutcome.outcome === 'overridden') outcomeDay.overrides += 1;
273
+ if (latestOutcome.outcome === 'rolled_back') outcomeDay.rollbacks += 1;
274
+ if (latestOutcome.outcome === 'completed' || latestOutcome.outcome === 'accepted') outcomeDay.completions += 1;
275
+ if (latestOutcome.outcome === 'blocked') outcomeDay.blockedOutcomes += 1;
276
+ }
277
+ if (RESOLVED_OUTCOMES.has(latestOutcome.outcome)) {
278
+ resolvedCount += 1;
279
+ }
280
+ if (Number.isFinite(latestOutcome.latencyMs)) {
281
+ latencyValues.push(latestOutcome.latencyMs);
282
+ if (outcomeDay) outcomeDay.latencies.push(latestOutcome.latencyMs);
283
+ }
284
+ }
285
+ }
286
+
287
+ const days = series.map((day) => ({
288
+ dayKey: day.dayKey,
289
+ evaluations: day.evaluations,
290
+ fastPath: day.fastPath,
291
+ checkpoint: day.checkpoint,
292
+ blockedRecommendations: day.blockedRecommendations,
293
+ overrides: day.overrides,
294
+ rollbacks: day.rollbacks,
295
+ completions: day.completions,
296
+ blockedOutcomes: day.blockedOutcomes,
297
+ medianLatencyMs: median(day.latencies),
298
+ }));
299
+
300
+ return {
301
+ evaluationCount: actions.length,
302
+ resolvedCount,
303
+ fastPathCount,
304
+ checkpointCount,
305
+ blockedRecommendationCount,
306
+ overrideCount,
307
+ rollbackCount,
308
+ outcomeCounts,
309
+ fastPathRate: safeRate(fastPathCount, actions.length),
310
+ checkpointRate: safeRate(checkpointCount, actions.length),
311
+ overrideRate: safeRate(overrideCount, resolvedCount || actions.length),
312
+ rollbackRate: safeRate(rollbackCount, resolvedCount || actions.length),
313
+ followRate: safeRate(Math.max(0, resolvedCount - overrideCount), resolvedCount || actions.length),
314
+ medianLatencyMs: median(latencyValues),
315
+ averageLatencyMs: latencyValues.length > 0
316
+ ? Math.round(latencyValues.reduce((sum, value) => sum + value, 0) / latencyValues.length)
317
+ : 0,
318
+ dayCount,
319
+ days,
320
+ activeDays: days.filter((day) => {
321
+ return day.evaluations > 0 || day.overrides > 0 || day.rollbacks > 0 || day.completions > 0 || day.blockedOutcomes > 0;
322
+ }).length,
323
+ };
324
+ }
325
+
326
+ module.exports = {
327
+ DECISION_LOG_FILENAME,
328
+ RESOLVED_OUTCOMES,
329
+ buildActionId,
330
+ collapseDecisionTimeline,
331
+ computeDecisionMetrics,
332
+ getDecisionLogPath,
333
+ normalizeOutcome,
334
+ readDecisionLog,
335
+ recordDecisionEvaluation,
336
+ recordDecisionOutcome,
337
+ };
338
+
339
+ if (require.main === module) {
340
+ console.log(JSON.stringify(computeDecisionMetrics(process.argv[2]), null, 2));
341
+ }
@@ -4,6 +4,7 @@
4
4
  const crypto = require('crypto');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const { ensureDir } = require('./fs-utils');
7
8
  const {
8
9
  loadSubagentProfiles,
9
10
  getAllowedTools,
@@ -32,11 +33,6 @@ function getVerificationLoopModule() {
32
33
  return require('./verification-loop');
33
34
  }
34
35
 
35
- function ensureDir(dirPath) {
36
- if (!fs.existsSync(dirPath)) {
37
- fs.mkdirSync(dirPath, { recursive: true });
38
- }
39
- }
40
36
 
41
37
  function readJSONL(filePath) {
42
38
  if (!fs.existsSync(filePath)) return [];