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
@@ -242,24 +242,24 @@
242
242
  </div>
243
243
  <div class="metrics-summary-grid" id="metricsSummaryGrid">
244
244
  <div class="metric-tile">
245
- <div class="metric-kicker">7d approval</div>
245
+ <div class="metric-kicker">Fast path rate</div>
246
246
  <div class="metric-number purple">--</div>
247
- <div class="metric-note">Waiting for live feedback</div>
247
+ <div class="metric-note">Waiting for live decisions</div>
248
248
  </div>
249
249
  <div class="metric-tile">
250
- <div class="metric-kicker">This week negatives</div>
250
+ <div class="metric-kicker">Override rate</div>
251
251
  <div class="metric-number red">--</div>
252
- <div class="metric-note">Waiting for live feedback</div>
252
+ <div class="metric-note">Waiting for live decisions</div>
253
253
  </div>
254
254
  <div class="metric-tile">
255
- <div class="metric-kicker">Actions blocked</div>
256
- <div class="metric-number green">--</div>
257
- <div class="metric-note">Recorded gate denies</div>
255
+ <div class="metric-kicker">Rollback rate</div>
256
+ <div class="metric-number cyan">--</div>
257
+ <div class="metric-note">Waiting for live decisions</div>
258
258
  </div>
259
259
  <div class="metric-tile">
260
- <div class="metric-kicker">Repeat pressure</div>
261
- <div class="metric-number cyan">--</div>
262
- <div class="metric-note">Share of negatives that repeat</div>
260
+ <div class="metric-kicker">Median latency</div>
261
+ <div class="metric-number green">--</div>
262
+ <div class="metric-note">Time from recommendation to recorded outcome</div>
263
263
  </div>
264
264
  </div>
265
265
  <div class="metrics-chart-wrap">
@@ -492,7 +492,7 @@ function updateMetricsSelection() {
492
492
  note.textContent = 'Showing timeline events for ' + formatShortDay(activeTimelineDay) + '. Clear the day filter to return to the full timeline.';
493
493
  }
494
494
  } else {
495
- note.textContent = 'The chart combines recorded feedback events with daily gate-audit activity.';
495
+ note.textContent = 'The chart combines recorded feedback events with daily gate-audit activity. Decision-loop metrics above come from recorded evaluations and outcomes.';
496
496
  }
497
497
  }
498
498
 
@@ -627,12 +627,12 @@ function renderImprovementMetrics(stats, data) {
627
627
  if (!summary || !grid || !chart) return;
628
628
 
629
629
  if (isDemo) {
630
- summary.textContent = 'Live improvement trends appear here once local Pro is connected. The chart is based on recorded feedback, gate denials, and repeat-failure pressure.';
630
+ summary.textContent = 'Live improvement trends appear here once local Pro is connected. The decision tiles are based on recorded evaluations plus override and rollback outcomes, while the chart below stays anchored to feedback and gate audits.';
631
631
  grid.innerHTML = [
632
- { label: '7d approval', value: '12.5%', tone: 'purple', note: 'Demo preview only' },
633
- { label: 'This week negatives', value: '4', tone: 'red', note: 'Sample trend only' },
634
- { label: 'Actions blocked', value: '42', tone: 'green', note: 'Sample gate denies' },
635
- { label: 'Repeat pressure', value: '38%', tone: 'cyan', note: 'Sample repeated failures' }
632
+ { label: 'Fast path rate', value: '62%', tone: 'purple', note: 'Sample auto-execute share' },
633
+ { label: 'Override rate', value: '14%', tone: 'red', note: 'Sample operator overrides' },
634
+ { label: 'Rollback rate', value: '4%', tone: 'cyan', note: 'Sample reversed decisions' },
635
+ { label: 'Median latency', value: '6m', tone: 'green', note: 'Sample decision completion speed' }
636
636
  ].map(function(tile) {
637
637
  return '<div class="metric-tile"><div class="metric-kicker">' + escHtml(tile.label) + '</div><div class="metric-number ' + escHtml(tile.tone) + '">' + escHtml(tile.value) + '</div><div class="metric-note">' + escHtml(tile.note) + '</div></div>';
638
638
  }).join('');
@@ -645,23 +645,32 @@ function renderImprovementMetrics(stats, data) {
645
645
  var gateStats = (data && data.gateStats) || {};
646
646
  var gateAudit = (data && data.gateAudit) || {};
647
647
  var liveMetrics = (data && data.liveMetrics) || {};
648
+ var decisionLoop = (data && data.decisions) || {};
648
649
  var harness = (data && data.harness) || {};
649
650
  var approvalRate = Math.round(((stats && (stats.recentRate || stats.approvalRate)) || 0) * 100);
650
651
  var errorTrend = liveMetrics.errorTrend || {};
651
- var repeatPressure = Math.round(((harness.repeatFailureRate || 0) * 100));
652
- var repeatedCount = harness.repeatedFailureCount || 0;
652
+ var fastPathRate = Math.round(((decisionLoop.fastPathRate || 0) * 100));
653
+ var overrideRate = Math.round(((decisionLoop.overrideRate || 0) * 100));
654
+ var rollbackRate = Math.round(((decisionLoop.rollbackRate || 0) * 100));
655
+ var medianLatencyMs = Number(decisionLoop.medianLatencyMs || 0);
656
+ var medianLatencyText = medianLatencyMs >= 3600000
657
+ ? (medianLatencyMs / 3600000).toFixed(1) + 'h'
658
+ : medianLatencyMs >= 60000
659
+ ? Math.round(medianLatencyMs / 60000) + 'm'
660
+ : Math.round(medianLatencyMs / 1000) + 's';
653
661
  var thisWeekNeg = errorTrend.thisWeek || 0;
654
662
  var lastWeekNeg = errorTrend.lastWeek || 0;
655
663
  var blocked = gateStats.blocked || 0;
664
+ var resolvedCount = decisionLoop.resolvedCount || 0;
656
665
 
657
- summary.textContent = '7-day approval is ' + approvalRate + '%. This week logged ' + thisWeekNeg + ' negative signal' + (thisWeekNeg === 1 ? '' : 's')
658
- + ' versus ' + lastWeekNeg + ' last week, while gates recorded ' + blocked + ' deny decision' + (blocked === 1 ? '' : 's') + '.';
666
+ summary.textContent = 'ThumbGate is auto-routing ' + fastPathRate + '% of tracked decisions while holding override rate at ' + overrideRate + '% and rollback rate at ' + rollbackRate + '% across '
667
+ + resolvedCount + ' resolved decision' + (resolvedCount === 1 ? '' : 's') + '. Feedback approval is ' + approvalRate + '%, and gates still recorded ' + blocked + ' deny decision' + (blocked === 1 ? '' : 's') + '.';
659
668
 
660
669
  grid.innerHTML = [
661
- { label: '7d approval', value: approvalRate + '%', tone: 'purple', note: 'Recent approval rate from feedback signals' },
662
- { label: 'This week negatives', value: String(thisWeekNeg), tone: 'red', note: 'Vs ' + lastWeekNeg + ' last week' },
663
- { label: 'Actions blocked', value: String(blocked), tone: 'green', note: 'Recorded gate denies from the audit trail' },
664
- { label: 'Repeat pressure', value: repeatPressure + '%', tone: 'cyan', note: repeatedCount + ' repeated failure pattern' + (repeatedCount === 1 ? '' : 's') }
670
+ { label: 'Fast path rate', value: fastPathRate + '%', tone: 'purple', note: 'Recorded evaluations that stayed auto-executable' },
671
+ { label: 'Override rate', value: overrideRate + '%', tone: 'red', note: 'Resolved decisions later changed by a human' },
672
+ { label: 'Rollback rate', value: rollbackRate + '%', tone: 'cyan', note: 'Resolved decisions later reversed' },
673
+ { label: 'Median latency', value: medianLatencyText, tone: 'green', note: 'Time from recommendation to recorded outcome' }
665
674
  ].map(function(tile) {
666
675
  return '<div class="metric-tile"><div class="metric-kicker">' + escHtml(tile.label) + '</div><div class="metric-number ' + escHtml(tile.tone) + '">' + escHtml(tile.value) + '</div><div class="metric-note">' + escHtml(tile.note) + '</div></div>';
667
676
  }).join('');
@@ -0,0 +1,140 @@
1
+ # ThumbGate — Agent Governance for Engineering Teams
2
+
3
+ ## What ThumbGate Is
4
+
5
+ ThumbGate is agent governance software for engineering teams that run AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode) on shared codebases. It enforces approval policies, captures and propagates lessons from agent mistakes, and physically blocks known-bad tool calls before they execute via PreToolUse hooks wired into the agent runtime. Unlike CLAUDE.md rules or .cursorrules files, which are suggestions the agent can ignore, ThumbGate enforcement operates at the tool-call level and cannot be bypassed by the agent. The business is enterprise-first: the best first paid motion is the Workflow Hardening Sprint for one workflow, while the local CLI stays free as the adoption wedge and Pro remains a solo side lane.
6
+
7
+ ## Problems ThumbGate Prevents
8
+
9
+ - An AI coding agent force-pushes to the main branch, overwriting a teammate's commit
10
+ - An agent deletes a production config file because the prompt said "clean up unused files"
11
+ - An agent bypasses CI by committing with --no-verify after seeing test failures
12
+ - An agent repeats the same database migration mistake across three pull requests because the lesson was never captured
13
+ - One engineer gives a thumbs-down on a bad agent pattern; teammates running the same agent repeat the mistake because lessons are not shared
14
+ - An agent modifies secrets or PII-bearing files because no approval policy was in place
15
+ - A team cannot audit which agent actions were blocked, approved, or overridden, making compliance reporting impossible
16
+
17
+ ## How ThumbGate Works Technically
18
+
19
+ ThumbGate is built on Node.js >=18.18.0 and runs locally on each developer's machine with optional team sync.
20
+
21
+ **CLI-first install, MCP-compatible transport**: `npx thumbgate init` is the default setup path. It installs the local gateway, wires the needed hooks, and configures MCP transport automatically for the agent that is already in use. MCP matters for compatibility, but the product surface is the operator-friendly CLI.
22
+
23
+ **PreToolUse Hooks**: Every agent tool call (Bash, file writes, git operations, API calls) passes through a hook before execution. If the call matches a known-bad pattern stored in the lesson database, the hook blocks it and returns a descriptive error. The agent cannot proceed until the human approves or the policy is updated.
24
+
25
+ **SQLite + FTS5 Lesson Database**: When an agent makes a mistake, the developer gives a thumbs-down with context. ThumbGate stores this as a lesson in a local SQLite database with full-text search. Lessons are retrieved at the start of every agent session via the `recall` MCP tool, so the agent enters each session already aware of known failure patterns.
26
+
27
+ **Thompson Sampling for Adaptive Gates**: Gates use Thompson Sampling (a Bayesian multi-armed bandit algorithm) to tune their own sensitivity. Gates that block too aggressively accumulate negative feedback and are dialed back. Gates that catch real failures are reinforced. This prevents gate fatigue without manual tuning.
28
+
29
+ **Shared Team Enforcement**: In team mode, lessons learned on one seat propagate to all seats via a shared lesson database. A pattern that caused a mistake for one engineer is immediately visible to every agent on every seat. The shared database is the single source of truth for team-wide enforcement rules.
30
+
31
+ **CI Gate Integration**: ThumbGate can run as a CI step. Pull requests that contain agent-generated changes matching known failure signatures are blocked from merging until a human reviews and approves the exception.
32
+
33
+ **Audit Trail**: Every gate decision (blocked, approved, overridden) is logged with a timestamp, the triggering tool call, the matching lesson ID, and the identity of any human who approved an exception. This log is queryable and exportable for compliance reporting.
34
+
35
+ **Three-Tier Approval Routing (OVIS-inspired)**: ThumbGate gates operate on three distinct tiers, inspired by the OVIS decision framework (Owner, Veto, Influence). Each gate carries an `action` field that determines the routing outcome:
36
+
37
+ - **`block`** — Hard stop. The agent cannot proceed. The tool call is denied immediately. Used for force-pushes, secret commits, destructive SQL, and any irreversible action. The agent receives an error message explaining why the action was blocked.
38
+ - **`approve`** — Pause and escalate. The agent is halted and the caller receives `{ decision: "approve", requiresApproval: true }`. A human must explicitly confirm before the action can proceed. Used for production deploys, schema migrations, and permission changes where human oversight is mandatory.
39
+ - **`log`** — Record and continue. The action is allowed to proceed but is written to the audit trail. The agent receives `{ decision: "log", logged: true }` and continues without interruption. Used for style violations, large file writes, and non-critical warnings where visibility matters but blocking would create friction.
40
+
41
+ This model maps directly to the OVIS framework: `block` exercises Veto authority, `approve` requires Owner sign-off, and `log` satisfies Influence-layer audit requirements without halting execution.
42
+
43
+ ## Who ThumbGate Is For
44
+
45
+ Engineering teams of 2 to 200+ developers who are actively using AI coding agents on shared repositories and need:
46
+
47
+ - Consistent enforcement of coding policies across all agents and all seats
48
+ - A shared memory of agent mistakes so errors are not repeated by different team members
49
+ - Approval gates for high-risk actions (pushing to protected branches, modifying production configs, running database migrations)
50
+ - An audit trail for compliance, incident review, or just understanding what the agent did
51
+ - Gradual rollout: start with observation mode, add enforcement rules incrementally
52
+
53
+ ThumbGate is not a model training pipeline. It does not retrain the underlying LLM. It shapes agent behavior through context injection and hard enforcement hooks.
54
+
55
+ ## Academic Validation
56
+
57
+ ThumbGate implements the **Memento-Skills architecture** described in "Memento-Skills: Let Agents Design Agents" (arXiv 2603.18743, March 2026). This architecture—Read → Execute → Reflect → Write—allows agents to improve themselves through external skill memory that rewrites from failure feedback, eliminating the need for model retraining. Published results demonstrate 26.2% and 116.2% relative accuracy improvements on General AI Assistants benchmarks and Humanity's Last Exam. ThumbGate applies this same pattern to production AI coding agents via PreToolUse hooks, Thompson Sampling, SQLite+FTS5 lesson databases, and LanceDB vectors—treating each agent mistake as a skill refinement opportunity rather than a training event.
58
+
59
+ ## Continual Learning Architecture
60
+
61
+ ThumbGate implements continual learning across all three layers identified by LangChain's framework for building learning agents (Harrison Chase, April 2026):
62
+
63
+ - **Model layer**: ThumbGate Pro exports DPO pairs so teams can fine-tune local models (Llama 3, Mistral) to natively avoid known failures without retraining the upstream foundation model.
64
+ - **Harness layer**: Prevention rules auto-generated from feedback are injected into the agent's system prompt at session start via the `recall` MCP tool. The agent enters every session pre-loaded with lessons — no code changes required.
65
+ - **Context layer**: The SQLite+FTS5 lesson database and LanceDB vector search provide retrieval-augmented context at tool-call time. When an agent attempts an action, PreToolUse hooks query the lesson DB and block or approve based on prior feedback. This is the layer that runs continuously and requires zero human intervention after the initial feedback signal.
66
+
67
+ This three-layer architecture means ThumbGate improves agent behavior at every level: context injection for immediate effect, harness-level rules for session-wide enforcement, and model-level export for permanent behavioral change.
68
+
69
+ ## Enterprise Safety Framework Alignment
70
+
71
+ ThumbGate's architecture maps directly to the enterprise safety framework pattern recommended by Google Cloud for agentic AI workflows (April 2026). Google Cloud's reference architecture routes every agent action through a Safety Framework gate before execution, with unsafe actions producing a canned response and safe actions proceeding to the Agentic Workflow, followed by a Quality Framework check on the output.
72
+
73
+ ThumbGate implements this same pattern for AI coding agents:
74
+
75
+ - **Safety Framework gate** → PreToolUse hooks evaluate every tool call against the lesson database before execution. Known-bad patterns are blocked immediately.
76
+ - **Safe → Agentic Workflow** → Tool calls that pass gate evaluation proceed normally. The agent operates without interruption.
77
+ - **Unsafe → Canned response** → Blocked tool calls return a descriptive error to the agent explaining why the action was denied and what policy it violated.
78
+ - **Quality Framework** → Post-action audit trail, feedback sessions, and auto-generated prevention rules create a continuous quality improvement loop.
79
+
80
+ This alignment means ThumbGate is not an experimental tool — it implements the same safety architecture that Google Cloud recommends for enterprise agentic deployments, applied specifically to AI coding agents operating on shared codebases.
81
+
82
+ ## Pricing
83
+
84
+ - **Free**: Local CLI enforcement for individual developers. Includes 3 daily feedback captures, 5 lesson searches per day, unlimited recall, and PreToolUse hook blocking.
85
+ - **Workflow Hardening Sprint / Team**: Team pricing anchors at $99/seat/mo with a 3-seat minimum after qualification. The first paid step is an intake-led sprint around one workflow, one repeated blocker, and one proof review.
86
+ - **Pro**: $19/mo or $149/yr. Adds a personal local dashboard, DPO export for fine-tuning, and advanced data exports for solo operators who want a self-serve side lane.
87
+
88
+ ## How to Install
89
+
90
+ ```bash
91
+ npx thumbgate init
92
+ ```
93
+
94
+ ThumbGate auto-detects your AI coding agent (Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode) and configures PreToolUse hooks. No API key required. Everything runs locally. For team deployments, run with the `--team` flag to connect to the shared lesson database.
95
+
96
+ ```bash
97
+ npx thumbgate init --agent claude-code
98
+ npx thumbgate dashboard
99
+ ```
100
+
101
+ ## Comparison vs Alternatives
102
+
103
+ | Approach | Blocks actions before execution | Learns from feedback | Shared team enforcement | Audit trail |
104
+ |---|---|---|---|---|
105
+ | **ThumbGate** | Yes — PreToolUse hooks | Yes — auto-generates rules | Yes — shared lesson DB | Yes — full log |
106
+ | CLAUDE.md / .cursorrules | No — suggestions only | No — hand-written | No — per-developer files | No |
107
+ | ESLint / linters | Partial — static analysis | No — hand-written rules | Partial — shared config | No |
108
+ | Manual code review | Partial — after PR, not before | No — reviewer memory | Partial — PR comments | Partial — PR history |
109
+ | Post-hoc git revert | No — damage already done | No | No | Partial — git log |
110
+
111
+ **CLAUDE.md rules** are plain-text instructions read by the agent at session start. The agent can ignore them, forget them, or misinterpret them mid-session. CLAUDE.md is useful for project context but provides no enforcement guarantee.
112
+
113
+ **ESLint and linters** check code at build time, not at tool-call time. They cannot block an agent from deleting a file, force-pushing, or making a destructive API call — they only catch code-style issues after the code has been written.
114
+
115
+ **Manual code review** is asynchronous and expensive. It catches mistakes after the PR is created, not before the agent makes the mistake. It scales poorly as AI agents generate changes at 10x the rate of human developers.
116
+
117
+ **ThumbGate** is the only tool that (1) blocks actions before execution, (2) learns automatically from feedback without manual rule-writing, (3) shares lessons across an entire team, and (4) produces a full audit trail.
118
+
119
+ ## Additional Resources
120
+
121
+ - GitHub: https://github.com/IgorGanapolsky/ThumbGate
122
+ - npm: https://www.npmjs.com/package/thumbgate
123
+ - Documentation: https://thumbgate-production.up.railway.app/guide
124
+ - Comparison: https://thumbgate-production.up.railway.app/compare
125
+ - Team sprint intake: https://thumbgate-production.up.railway.app/#workflow-sprint-intake
126
+
127
+ ## How ThumbGate Promotes AI Authenticity
128
+
129
+ In an era of AI slop — where agents generate repetitive, generic outputs that erode brand trust — ThumbGate is the enforcement layer that keeps AI behavior grounded in human judgment.
130
+
131
+ The core thesis: AI slop happens when agents act without human judgment as a hard gate. ThumbGate solves this at the tool-call level, not through prompt suggestions the agent can ignore.
132
+
133
+ - **Human insight leads**: The thumbs-up/down mechanism is a literal human gate between AI intent and execution. The agent cannot proceed on blocked patterns.
134
+ - **Standards, not patterns**: Every thumbs-down distills into a prevention rule specific to your team's context — not a generic best practice.
135
+ - **Authenticity over volume**: ThumbGate reinforces what "good" looks like for your specific codebase, standards, and risk tolerance. Agents learn your standards, not average AI behavior.
136
+ - **Discovery advantage**: As platforms and search engines increasingly reward authentic, human-verified outputs over high-volume AI generation, ThumbGate provides the enforcement infrastructure that proves human oversight was applied.
137
+
138
+ **Positioning for authenticity-conscious audiences**: ThumbGate is the authenticity enforcement layer for AI coding agents. Where AI slop is the symptom, unconstrained agent autonomy is the cause. ThumbGate addresses the cause.
139
+
140
+ *Last updated: 2026-04-09*
package/public/pro.html CHANGED
@@ -792,10 +792,10 @@ __GA_BOOTSTRAP__
792
792
  <section class="hero">
793
793
  <div class="container hero-grid">
794
794
  <div class="panel hero-copy">
795
- <div class="eyebrow">Paid lane for individual operators</div>
796
- <h1>Stop paying review time for the same AI mistake twice.</h1>
797
- <p>ThumbGate Pro is for the operator who already likes the free local install, but now needs a personal local dashboard, DPO export, review-ready evidence, and founder support on the workflows that keep hurting: deploys, migrations, force-pushes, and CI.</p>
798
- <p>Free remains the honest default if all you need is local recall, Pre-Action Gates, and MCP wiring. Pro is the paid lane when you need faster debugging, exportable proof, and a tighter Reliability Gateway around one operator's real workflow.</p>
795
+ <div class="eyebrow">Agent governance for engineering teams</div>
796
+ <h1>One correction protects every agent on your team.</h1>
797
+ <p>ThumbGate prevents unsafe AI agent actions before they hit shared repos, CI pipelines, and production. When one developer flags a bad pattern, every agent on the team is permanently blocked from repeating it.</p>
798
+ <p>Open-source core for individuals. Team plan for shared enforcement, CI gates, approval policies, and audit trails across your engineering org.</p>
799
799
  <div class="hero-proof">
800
800
  <div class="proof-pill">Personal local dashboard</div>
801
801
  <div class="proof-pill">DPO export from real corrections</div>
@@ -947,30 +947,32 @@ __GA_BOOTSTRAP__
947
947
  <div class="container pricing-shell">
948
948
  <div class="pricing-card">
949
949
  <div class="section-label" style="text-align:left;margin-bottom:8px;">Pricing</div>
950
- <h3>ThumbGate Pro for one operator</h3>
951
- <div class="price">$19<span>/mo</span></div>
952
- <div class="annual">or $149/yr when you already know this workflow needs to stay hardened</div>
953
- <p class="pricing-note">Pro keeps the local-first posture and adds the paid operator surface: personal local dashboard, DPO export, auto-connect after activation, Model Hardening Advisor, and founder support.</p>
950
+ <h3>ThumbGate Team Agent Governance</h3>
951
+ <div class="price">$99<span>/seat/mo</span></div>
952
+ <div class="annual">Billed monthly or annually · Starts with a 30-min pilot call</div>
953
+ <p class="pricing-note">Shared enforcement memory, CI gates, approval policies, sandbox routing, and a full audit trail of every blocked agent action across your team.</p>
954
954
  <ul>
955
- <li>Visual gate debugger to inspect every blocked action and the gate that fired.</li>
956
- <li>DPO training data export from real thumbs-down corrections.</li>
957
- <li>Auto-connect running agents to your personal local dashboard after activation.</li>
958
- <li>Founder support on risky workflows: migrations, deploys, force-pushes, and CI.</li>
955
+ <li><strong>Shared enforcement</strong> one developer's correction blocks that pattern for every agent on the team.</li>
956
+ <li><strong>CI gate integration</strong> block unsafe merges, enforce test requirements, prevent PRs with unresolved threads.</li>
957
+ <li><strong>Approval policies</strong> — require human sign-off for high-risk actions (production deploys, schema migrations, destructive SQL).</li>
958
+ <li><strong>Audit trail</strong> every blocked action logged with timestamp, agent, context, and the rule that fired.</li>
959
+ <li><strong>Sandbox routing</strong> — route risky agent runs into isolated execution environments.</li>
960
+ <li><strong>Org dashboard</strong> — active agents, gate hit rates, risk scores, and proof-backed team metrics.</li>
959
961
  </ul>
960
962
  <div class="pricing-actions">
961
- <a class="btn-primary btn-pro-checkout" href="/checkout/pro?utm_source=website&utm_medium=pro_page_pricing&utm_campaign=pro_pack&cta_id=pro_page_pricing_monthly&cta_placement=pricing&plan_id=pro&landing_path=%2Fpro">Start 7-Day Free Trial</a>
962
- <a class="btn-secondary btn-pro-checkout" href="/checkout/pro?utm_source=website&utm_medium=pro_page_pricing&utm_campaign=pro_pack&cta_id=pro_page_pricing_annual&cta_placement=pricing&plan_id=pro&billing_cycle=annual&landing_path=%2Fpro">Choose annual</a>
963
+ <a class="btn-primary" href="/#workflow-sprint-intake">Book a Team Pilot Call</a>
964
+ <a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page_pricing&utm_campaign=team">Open dashboard demo</a>
963
965
  </div>
964
- <div class="pricing-meta">If the personal local dashboard and export path do not matter yet, stay on Free. This page exists so paid buyers do not have to infer the value from the general homepage.</div>
966
+ <div class="pricing-meta">Team pricing anchors at $99/seat/mo with a 3-seat minimum. Starts with one workflow, one repo, one repeat failure. We measure before/after and expand only when the results are real.</div>
965
967
  </div>
966
968
 
967
969
  <div class="pricing-sidebar">
968
970
  <div class="team-card">
969
971
  <div class="section-label" style="text-align:left;margin-bottom:8px;">When Team is better</div>
970
- <h3>Multiple agents, shared repos, one correction that should protect everyone</h3>
971
- <p>Choose Team when you need a shared hosted lesson DB, org dashboard visibility, hosted review views, and a workflow hardening pilot instead of one operator's personal local dashboard.</p>
972
+ <h3>Solo developer? Start free.</h3>
973
+ <p>The open-source core gives you local enforcement, PreToolUse hooks, and MCP integrations at no cost. Upgrade to Team when your org needs shared enforcement and audit.</p>
972
974
  <div class="hero-actions" style="margin-top:18px;">
973
- <a class="btn-secondary" href="/?utm_source=website&utm_medium=pro_page&utm_campaign=team_rollout#workflow-sprint-intake">Start team pilot intake</a>
975
+ <a class="btn-secondary" href="/guide?utm_source=website&utm_medium=pro_page&utm_campaign=free_install">Install free locally</a>
974
976
  </div>
975
977
  </div>
976
978
  <div class="team-card">
@@ -1010,11 +1012,11 @@ __GA_BOOTSTRAP__
1010
1012
  <section class="final-cta">
1011
1013
  <div class="container">
1012
1014
  <div class="final-shell">
1013
- <h2>Make the paid path obvious before you ask for the card.</h2>
1014
- <p>ThumbGate Pro exists for the operator who wants the local dashboard, DPO export, and proof-ready story. The free install stays honest. The paid lane now has its own page.</p>
1015
+ <h2>Your team's AI agents are one bad action away from breaking production.</h2>
1016
+ <p>ThumbGate prevents force-pushes, secret commits, unsafe publishes, and destructive SQL before they execute. One correction protects every developer on your team permanently.</p>
1015
1017
  <div class="hero-actions" style="justify-content:center;">
1016
- <a class="btn-primary btn-pro-checkout" href="/checkout/pro?utm_source=website&utm_medium=pro_page_final&utm_campaign=pro_pack&cta_id=pro_page_final&cta_placement=final&plan_id=pro&landing_path=%2Fpro">Start 7-Day Free Trial</a>
1017
- <a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page_final&utm_campaign=pro_pack">Open dashboard demo</a>
1018
+ <a class="btn-primary" href="/#workflow-sprint-intake">Book a Team Pilot Call</a>
1019
+ <a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page_final&utm_campaign=team">Open dashboard demo</a>
1018
1020
  </div>
1019
1021
  </div>
1020
1022
  </div>
@@ -3,8 +3,8 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { resolveFeedbackDir } = require('./feedback-paths');
6
+ const { readJsonl } = require('./fs-utils');
6
7
  function getAccessLogPath() { return path.join(resolveFeedbackDir(), 'access-log.jsonl'); }
7
- function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
8
8
  function recordAccessAttempt({ agentId, authorized, reason, source } = {}) { const lp = getAccessLogPath(); const dir = path.dirname(lp); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const e = { id: `access_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, timestamp: new Date().toISOString(), agentId: agentId || 'unknown', authorized: authorized !== false, reason: reason || '', source: source || 'unknown' }; fs.appendFileSync(lp, JSON.stringify(e) + '\n'); return e; }
9
9
  function computeAccessStats({ periodHours = 24 } = {}) { const entries = readJsonl(getAccessLogPath()); const cutoff = Date.now() - periodHours * 60 * 60 * 1000; const recent = entries.filter((e) => new Date(e.timestamp).getTime() > cutoff); const authorized = recent.filter((e) => e.authorized).length; const failed = recent.filter((e) => !e.authorized).length; const total = recent.length; const failRate = total > 0 ? Math.round((failed / total) * 1000) / 10 : 0; const byAgent = {}; for (const e of recent) { if (!byAgent[e.agentId]) byAgent[e.agentId] = { authorized: 0, failed: 0 }; if (e.authorized) byAgent[e.agentId].authorized++; else byAgent[e.agentId].failed++; } return { periodHours, total, authorized, failed, failRate, byAgent }; }
10
10
  function detectAnomalies({ baselineHours = 168, recentHours = 24, spikeMultiplier = 3 } = {}) { const baseline = computeAccessStats({ periodHours: baselineHours }); const recent = computeAccessStats({ periodHours: recentHours }); const baselineRate = baselineHours > 0 ? baseline.failed / baselineHours : 0; const recentRate = recentHours > 0 ? recent.failed / recentHours : 0; const isAnomaly = baselineRate > 0 && recentRate > baselineRate * spikeMultiplier; const isHighFailRate = recent.failRate > 20 && recent.total >= 5; const anomalies = []; if (isAnomaly) anomalies.push({ type: 'failure_spike', severity: recentRate > baselineRate * 5 ? 'critical' : 'warning', message: `Failed access rate ${recentRate.toFixed(1)}/h is ${(recentRate / baselineRate).toFixed(1)}x the baseline ${baselineRate.toFixed(1)}/h`, recentRate, baselineRate }); if (isHighFailRate) anomalies.push({ type: 'high_fail_rate', severity: recent.failRate > 50 ? 'critical' : 'warning', message: `${recent.failRate}% of access attempts failed in the last ${recentHours}h (${recent.failed}/${recent.total})`, failRate: recent.failRate }); for (const [agentId, counts] of Object.entries(recent.byAgent)) { const agentTotal = counts.authorized + counts.failed; if (counts.failed >= 5 && counts.failed / agentTotal > 0.5) anomalies.push({ type: 'agent_abuse', severity: 'warning', message: `Agent ${agentId}: ${counts.failed} failed attempts out of ${agentTotal}`, agentId }); } return { baseline: { periodHours: baselineHours, failedPerHour: Math.round(baselineRate * 100) / 100 }, recent: { periodHours: recentHours, failedPerHour: Math.round(recentRate * 100) / 100, ...recent }, anomalies, hasAnomalies: anomalies.length > 0, checkedAt: new Date().toISOString() }; }
@@ -23,11 +23,6 @@ const { validateApiKey } = require('./billing');
23
23
  // Keep track of the last processed ID to avoid re-consolidating the exact same logs
24
24
  const STATE_FILE = process.env.ADK_STATE_FILE || path.join(PROJECT_ROOT, '.thumbgate', 'adk-state.json');
25
25
 
26
- function ensureDir(dirPath) {
27
- if (!fs.existsSync(dirPath)) {
28
- fs.mkdirSync(dirPath, { recursive: true });
29
- }
30
- }
31
26
 
32
27
  function loadState() {
33
28
  if (fs.existsSync(STATE_FILE)) {
@@ -46,6 +41,7 @@ function saveState(state) {
46
41
  }
47
42
 
48
43
  const { createRuleProposal, createReasoningTrace } = require('./a2ui-engine');
44
+ const { ensureDir } = require('./fs-utils');
49
45
 
50
46
  function buildFakeConsolidation(anchorLogs, newLogs) {
51
47
  const combined = [...anchorLogs, ...newLogs].filter(Boolean);
@@ -14,11 +14,9 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
  const { resolveFeedbackDir } = require('./feedback-paths');
17
+ const { ensureParentDir, readJsonl } = require('./fs-utils');
17
18
 
18
19
  function getFeedbackDir() { return resolveFeedbackDir(); }
19
- function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
20
- function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
21
-
22
20
  const CRED_LOG = 'credential-attestations.jsonl';
23
21
  const ESCALATION_LOG = 'escalation-events.jsonl';
24
22
  const DEP_LOG = 'dependency-attestations.jsonl';
@@ -47,7 +45,7 @@ function attestCredential({ agentId, credentialType, credentialId, toolName, sco
47
45
  sessionId: sessionId || null,
48
46
  };
49
47
  const logPath = getCredLogPath();
50
- ensureDir(logPath);
48
+ ensureParentDir(logPath);
51
49
  fs.appendFileSync(logPath, JSON.stringify(entry) + '\n');
52
50
  return entry;
53
51
  }
@@ -114,7 +112,7 @@ function detectPrivilegeEscalation({ agentId, toolName, mcpProfile } = {}) {
114
112
  message: `Agent "${agentId}" attempted to use "${toolName}" which is outside "${profile}" profile scope`,
115
113
  };
116
114
  const logPath = getEscalationLogPath();
117
- ensureDir(logPath);
115
+ ensureParentDir(logPath);
118
116
  fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
119
117
  return { escalation: true, event };
120
118
  }
@@ -197,7 +195,7 @@ function attestDependency({ packageName, version, agentId, action } = {}) {
197
195
  };
198
196
 
199
197
  const logPath = getDepLogPath();
200
- ensureDir(logPath);
198
+ ensureParentDir(logPath);
201
199
  fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
202
200
 
203
201
  return { allowed, findings, isTrustedScope, event };
@@ -18,6 +18,7 @@ const {
18
18
  } = require('./analytics-window');
19
19
  const { appendWorkflowRun } = require('./workflow-runs');
20
20
  const { buildPredictiveInsights } = require('./predictive-insights');
21
+ const { ensureDir } = require('./fs-utils');
21
22
 
22
23
  const PIPELINE_DIRNAME = 'agentic-data-pipeline';
23
24
  const DEFAULT_JOB_ID = 'agentic-data-pipeline';
@@ -32,9 +33,6 @@ function normalizeText(value) {
32
33
  return text || null;
33
34
  }
34
35
 
35
- function ensureDir(dirPath) {
36
- fs.mkdirSync(dirPath, { recursive: true });
37
- }
38
36
 
39
37
  function readJson(filePath) {
40
38
  try {
@@ -8,6 +8,7 @@ const { captureFeedback, analyzeFeedback, getFeedbackPaths, readJSONL } = requir
8
8
  const { runVerificationLoop } = require('./verification-loop');
9
9
  const { createExperiment } = require('./experiment-tracker');
10
10
  const { recommendEvolutionTarget } = require('./workspace-evolver');
11
+ const { ensureDir } = require('./fs-utils');
11
12
 
12
13
  const JOB_LOG_FILENAME = 'job-log.jsonl';
13
14
  const JOB_CONTROL_FILENAME = 'job-control.json';
@@ -17,11 +18,6 @@ const RESUMABLE_STATUSES = new Set(['paused', 'running', 'resume_requested']);
17
18
  const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
18
19
  const CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
19
20
 
20
- function ensureDir(dirPath) {
21
- if (!fs.existsSync(dirPath)) {
22
- fs.mkdirSync(dirPath, { recursive: true });
23
- }
24
- }
25
21
 
26
22
  function nowIso() {
27
23
  return new Date().toISOString();
@@ -13,6 +13,7 @@
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
15
  const { resolveFeedbackDir } = require('./feedback-paths');
16
+ const { ensureDir } = require('./fs-utils');
16
17
 
17
18
  const AUDIT_LOG_FILENAME = 'audit-trail.jsonl';
18
19
 
@@ -24,11 +25,6 @@ function getAuditLogPath() {
24
25
  return path.join(resolveFeedbackDir(), AUDIT_LOG_FILENAME);
25
26
  }
26
27
 
27
- function ensureDir(dirPath) {
28
- if (!fs.existsSync(dirPath)) {
29
- fs.mkdirSync(dirPath, { recursive: true });
30
- }
31
- }
32
28
 
33
29
  // ---------------------------------------------------------------------------
34
30
  // Core audit record
@@ -6,8 +6,8 @@ const path = require('path');
6
6
  const { resolveFeedbackDir } = require('./feedback-paths');
7
7
 
8
8
  const MAX_AUTO_GATES = 10;
9
- const WARN_THRESHOLD = 3; // 3+ repeated failures surface a warning gate
10
- const BLOCK_THRESHOLD = 5; // 5+ repeated failures hard-block the action
9
+ const WARN_THRESHOLD = 2; // 2+ repeated failures surface a warning gate
10
+ const BLOCK_THRESHOLD = 3; // 3+ repeated failures hard-block the action
11
11
  const WINDOW_DAYS = 30;
12
12
 
13
13
  const NEG_SIGNALS = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
@@ -20,8 +20,10 @@ function getFeedbackLogPath() {
20
20
  const localClaude = path.join(process.cwd(), '.claude', 'memory', 'feedback', 'feedback-log.jsonl');
21
21
  if (fs.existsSync(localFallback)) return localFallback;
22
22
  if (fs.existsSync(localClaude)) return localClaude;
23
+ // Fall back to resolveFeedbackDir() for proper home-dir resolution
24
+ const resolved = path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
25
+ if (fs.existsSync(resolved)) return resolved;
23
26
  return localFallback; // default even if doesn't exist
24
- return path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
25
27
  }
26
28
 
27
29
  function getAutoGatesPath() {
@@ -16,21 +16,13 @@
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const { resolveFeedbackDir } = require('./feedback-paths');
19
+ const { ensureParentDir, readJsonl } = require('./fs-utils');
19
20
 
20
21
  const RUNS_FILE = 'agent-runs.jsonl';
21
22
 
22
23
  function getFeedbackDir() { return resolveFeedbackDir(); }
23
24
  function getRunsPath() { return path.join(getFeedbackDir(), RUNS_FILE); }
24
25
 
25
- function readJsonl(fp) {
26
- if (!fs.existsSync(fp)) return [];
27
- const raw = fs.readFileSync(fp, 'utf-8').trim();
28
- if (!raw) return [];
29
- return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
30
- }
31
-
32
- function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
33
-
34
26
  // ---------------------------------------------------------------------------
35
27
  // 1. Run Tracking
36
28
  // ---------------------------------------------------------------------------
@@ -41,7 +33,7 @@ function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.m
41
33
  */
42
34
  function recordAgentRun({ agentId, runType, source, branch, prNumber, status, gatesChecked, gatesBlocked, filesChanged, ciPassed, duration, metadata } = {}) {
43
35
  const runsPath = getRunsPath();
44
- ensureDir(runsPath);
36
+ ensureParentDir(runsPath);
45
37
  const run = {
46
38
  id: `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
47
39
  timestamp: new Date().toISOString(),