thumbgate 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  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 +133 -23
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +168 -0
  9. package/adapters/claude/.mcp.json +2 -2
  10. package/adapters/codex/config.toml +2 -2
  11. package/adapters/mcp/server-stdio.js +85 -2
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bin/cli.js +215 -19
  14. package/bin/postinstall.js +8 -2
  15. package/config/budget.json +18 -0
  16. package/config/gates/code-edit.json +61 -0
  17. package/config/gates/db-write.json +61 -0
  18. package/config/gates/default.json +154 -3
  19. package/config/gates/deploy.json +61 -0
  20. package/config/github-about.json +2 -1
  21. package/config/merge-quality-checks.json +23 -0
  22. package/config/model-tiers.json +11 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -13
  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/cursor-marketplace/README.md +2 -2
  34. package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
  35. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
  36. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
  37. package/plugins/opencode-profile/INSTALL.md +1 -1
  38. package/public/blog.html +73 -0
  39. package/public/compare/mem0.html +189 -0
  40. package/public/compare/speclock.html +180 -0
  41. package/public/compare.html +12 -4
  42. package/public/guide.html +5 -5
  43. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  44. package/public/guides/codex-cli-guardrails.html +158 -0
  45. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  46. package/public/guides/pre-action-gates.html +162 -0
  47. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  48. package/public/index.html +169 -70
  49. package/public/learn/ai-agent-persistent-memory.html +1 -0
  50. package/public/lessons.html +334 -17
  51. package/public/llm-context.md +140 -0
  52. package/public/pro.html +24 -22
  53. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  54. package/scripts/access-anomaly-detector.js +1 -1
  55. package/scripts/adk-consolidator.js +1 -5
  56. package/scripts/agent-security-hardening.js +4 -6
  57. package/scripts/agentic-data-pipeline.js +1 -3
  58. package/scripts/async-job-runner.js +1 -5
  59. package/scripts/audit-trail.js +7 -5
  60. package/scripts/background-agent-governance.js +2 -10
  61. package/scripts/billing.js +2 -16
  62. package/scripts/budget-enforcer.js +173 -0
  63. package/scripts/build-codex-plugin.js +152 -0
  64. package/scripts/capture-railway-diagnostics.sh +97 -0
  65. package/scripts/check-congruence.js +133 -15
  66. package/scripts/claude-feedback-sync.js +320 -0
  67. package/scripts/cli-telemetry.js +4 -1
  68. package/scripts/commercial-offer.js +5 -7
  69. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  70. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  71. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  72. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  73. package/scripts/context-engine.js +21 -6
  74. package/scripts/contextfs.js +33 -44
  75. package/scripts/dashboard.js +104 -0
  76. package/scripts/decision-journal.js +341 -0
  77. package/scripts/delegation-runtime.js +1 -5
  78. package/scripts/distribution-surfaces.js +26 -0
  79. package/scripts/document-intake.js +927 -0
  80. package/scripts/ephemeral-agent-store.js +1 -8
  81. package/scripts/evolution-state.js +1 -5
  82. package/scripts/experiment-tracker.js +1 -5
  83. package/scripts/export-databricks-bundle.js +1 -5
  84. package/scripts/export-hf-dataset.js +1 -5
  85. package/scripts/export-training.js +1 -5
  86. package/scripts/feedback-attribution.js +1 -16
  87. package/scripts/feedback-history-distiller.js +1 -16
  88. package/scripts/feedback-loop.js +17 -5
  89. package/scripts/feedback-root-consolidator.js +2 -21
  90. package/scripts/feedback-session.js +49 -0
  91. package/scripts/feedback-to-rules.js +188 -28
  92. package/scripts/filesystem-search.js +1 -9
  93. package/scripts/fs-utils.js +104 -0
  94. package/scripts/gates-engine.js +149 -4
  95. package/scripts/github-about.js +32 -8
  96. package/scripts/gtm-revenue-loop.js +1 -5
  97. package/scripts/harness-selector.js +148 -0
  98. package/scripts/hosted-job-launcher.js +1 -5
  99. package/scripts/hybrid-feedback-context.js +7 -33
  100. package/scripts/intervention-policy.js +753 -0
  101. package/scripts/lesson-db.js +3 -18
  102. package/scripts/lesson-inference.js +194 -16
  103. package/scripts/lesson-retrieval.js +60 -24
  104. package/scripts/llm-client.js +59 -0
  105. package/scripts/local-model-profile.js +18 -2
  106. package/scripts/managed-lesson-agent.js +183 -0
  107. package/scripts/marketing-experiment.js +8 -22
  108. package/scripts/meta-agent-loop.js +624 -0
  109. package/scripts/metered-billing.js +1 -1
  110. package/scripts/model-tier-router.js +10 -1
  111. package/scripts/money-watcher.js +1 -4
  112. package/scripts/obsidian-export.js +1 -5
  113. package/scripts/operational-integrity.js +369 -34
  114. package/scripts/org-dashboard.js +6 -1
  115. package/scripts/per-step-scoring.js +2 -4
  116. package/scripts/pr-manager.js +201 -19
  117. package/scripts/pro-features.js +3 -2
  118. package/scripts/prompt-dlp.js +3 -3
  119. package/scripts/prove-adapters.js +2 -5
  120. package/scripts/prove-attribution.js +1 -5
  121. package/scripts/prove-automation.js +3 -5
  122. package/scripts/prove-cloudflare-sandbox.js +1 -3
  123. package/scripts/prove-data-pipeline.js +1 -3
  124. package/scripts/prove-intelligence.js +1 -3
  125. package/scripts/prove-lancedb.js +1 -5
  126. package/scripts/prove-local-intelligence.js +1 -3
  127. package/scripts/prove-packaged-runtime.js +326 -0
  128. package/scripts/prove-predictive-insights.js +1 -3
  129. package/scripts/prove-runtime.js +13 -0
  130. package/scripts/prove-training-export.js +1 -3
  131. package/scripts/prove-workflow-contract.js +1 -5
  132. package/scripts/rate-limiter.js +6 -4
  133. package/scripts/reddit-dm-outreach.js +14 -4
  134. package/scripts/schedule-manager.js +3 -5
  135. package/scripts/security-scanner.js +448 -0
  136. package/scripts/self-distill-agent.js +579 -0
  137. package/scripts/semantic-dedup.js +115 -0
  138. package/scripts/skill-exporter.js +1 -3
  139. package/scripts/skill-generator.js +1 -5
  140. package/scripts/social-analytics/engagement-audit.js +1 -18
  141. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  142. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  143. package/scripts/social-analytics/publishers/zernio.js +51 -0
  144. package/scripts/social-pipeline.js +1 -3
  145. package/scripts/social-post-hourly.js +47 -4
  146. package/scripts/statusline-links.js +6 -5
  147. package/scripts/statusline-local-stats.js +2 -0
  148. package/scripts/statusline.sh +38 -7
  149. package/scripts/sync-branch-protection.js +340 -0
  150. package/scripts/tessl-export.js +1 -3
  151. package/scripts/thumbgate-search.js +32 -1
  152. package/scripts/tool-kpi-tracker.js +1 -1
  153. package/scripts/tool-registry.js +108 -4
  154. package/scripts/vector-store.js +1 -5
  155. package/scripts/weekly-auto-post.js +1 -1
  156. package/scripts/workflow-sentinel.js +205 -4
  157. package/skills/thumbgate/SKILL.md +2 -2
  158. package/src/api/server.js +273 -4
  159. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  160. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -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
@@ -64,6 +60,12 @@ function recordAuditEvent(params = {}) {
64
60
  };
65
61
 
66
62
  fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
63
+ try {
64
+ const { trainAndPersistInterventionPolicy } = require('./intervention-policy');
65
+ trainAndPersistInterventionPolicy(path.dirname(logPath));
66
+ } catch {
67
+ // Keep audit recording resilient even if the learned policy refresh fails.
68
+ }
67
69
  return record;
68
70
  }
69
71
 
@@ -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(),
@@ -43,6 +43,7 @@ const {
43
43
  resolveAnalyticsWindow,
44
44
  serializeAnalyticsWindow,
45
45
  } = require('./analytics-window');
46
+ const { ensureParentDir } = require('./fs-utils');
46
47
 
47
48
  // ---------------------------------------------------------------------------
48
49
  // Config
@@ -73,15 +74,7 @@ const CONFIG = {
73
74
  get NEWSLETTER_SUBSCRIBERS_PATH() {
74
75
  return process.env._TEST_NEWSLETTER_SUBSCRIBERS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'newsletter-subscribers.jsonl');
75
76
  },
76
- CREDIT_PACKS: {
77
- 'mistake-free-starter': {
78
- id: 'mistake-free-starter',
79
- name: 'Mistake-Free Starter Pack',
80
- amountCents: 4900,
81
- credits: 500,
82
- currency: 'USD',
83
- }
84
- }
77
+ CREDIT_PACKS: {}
85
78
  };
86
79
 
87
80
  function resolveLegacyBillingPath(fileName) {
@@ -148,13 +141,6 @@ function safeCompareHex(expectedHex, actualHex) {
148
141
  // Internal helpers
149
142
  // ---------------------------------------------------------------------------
150
143
 
151
- function ensureParentDir(filePath) {
152
- const dir = path.dirname(filePath);
153
- if (!fs.existsSync(dir)) {
154
- fs.mkdirSync(dir, { recursive: true });
155
- }
156
- }
157
-
158
144
  function sanitizeMetadata(metadata) {
159
145
  if (!metadata || typeof metadata !== 'object') return {};
160
146
  try {
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Budget Enforcer — action count, token, and time limits for agent sessions.
6
+ *
7
+ * Competitive parity with LaneKeep's budget system. Tracks:
8
+ * - max_actions: total tool calls allowed per session
9
+ * - max_time_minutes: wall-clock session duration cap
10
+ * - action_count: running count of tool calls in current session
11
+ *
12
+ * Budget state persists to ~/.thumbgate/budget-state.json.
13
+ * Config lives in config/budget.json or can be set via env vars.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const BUDGET_STATE_PATH = process.env.THUMBGATE_BUDGET_STATE_PATH || path.join(
20
+ process.env.HOME || '/tmp',
21
+ '.thumbgate',
22
+ 'budget-state.json'
23
+ );
24
+
25
+ const DEFAULT_BUDGET_CONFIG_PATH = process.env.THUMBGATE_BUDGET_CONFIG_PATH || path.join(
26
+ __dirname, '..', 'config', 'budget.json'
27
+ );
28
+
29
+ const DEFAULT_BUDGET = {
30
+ max_actions: 2000,
31
+ max_time_minutes: 600, // 10 hours
32
+ profiles: {
33
+ strict: { max_actions: 500, max_time_minutes: 150 },
34
+ guided: { max_actions: 2000, max_time_minutes: 600 },
35
+ autonomous: { max_actions: 5000, max_time_minutes: 1200 },
36
+ },
37
+ };
38
+
39
+ function loadBudgetConfig() {
40
+ // 1. Environment overrides
41
+ const envProfile = process.env.THUMBGATE_BUDGET_PROFILE;
42
+ const envMaxActions = process.env.THUMBGATE_MAX_ACTIONS;
43
+ const envMaxTime = process.env.THUMBGATE_MAX_TIME_MINUTES;
44
+
45
+ // 2. Config file
46
+ let fileConfig = {};
47
+ try {
48
+ if (fs.existsSync(DEFAULT_BUDGET_CONFIG_PATH)) {
49
+ fileConfig = JSON.parse(fs.readFileSync(DEFAULT_BUDGET_CONFIG_PATH, 'utf8'));
50
+ }
51
+ } catch { /* use defaults */ }
52
+
53
+ const merged = { ...DEFAULT_BUDGET, ...fileConfig };
54
+
55
+ // Apply profile if set
56
+ if (envProfile && merged.profiles && merged.profiles[envProfile]) {
57
+ Object.assign(merged, merged.profiles[envProfile]);
58
+ }
59
+
60
+ // Env overrides take final precedence
61
+ if (envMaxActions) {
62
+ const parsedMaxActions = parseInt(envMaxActions, 10);
63
+ if (Number.isFinite(parsedMaxActions) && parsedMaxActions > 0) {
64
+ merged.max_actions = parsedMaxActions;
65
+ }
66
+ }
67
+ if (envMaxTime) {
68
+ const parsedMaxTime = parseInt(envMaxTime, 10);
69
+ if (Number.isFinite(parsedMaxTime) && parsedMaxTime > 0) {
70
+ merged.max_time_minutes = parsedMaxTime;
71
+ }
72
+ }
73
+
74
+ return merged;
75
+ }
76
+
77
+ function loadBudgetState() {
78
+ try {
79
+ if (fs.existsSync(BUDGET_STATE_PATH)) {
80
+ return JSON.parse(fs.readFileSync(BUDGET_STATE_PATH, 'utf8'));
81
+ }
82
+ } catch { /* corrupted state — reset */ }
83
+ return { action_count: 0, session_start: new Date().toISOString() };
84
+ }
85
+
86
+ function saveBudgetState(state) {
87
+ const dir = path.dirname(BUDGET_STATE_PATH);
88
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
89
+ fs.writeFileSync(BUDGET_STATE_PATH, JSON.stringify(state, null, 2));
90
+ }
91
+
92
+ function resetBudget() {
93
+ const state = { action_count: 0, session_start: new Date().toISOString() };
94
+ saveBudgetState(state);
95
+ return state;
96
+ }
97
+
98
+ /**
99
+ * Evaluate budget limits. Called before every gate evaluation.
100
+ * Returns null if within budget, or a deny result if budget exceeded.
101
+ */
102
+ function evaluateBudget(toolName, toolInput) {
103
+ const config = loadBudgetConfig();
104
+ const state = loadBudgetState();
105
+
106
+ // Increment action count
107
+ state.action_count = (state.action_count || 0) + 1;
108
+ saveBudgetState(state);
109
+
110
+ // Check action limit
111
+ if (config.max_actions && state.action_count > config.max_actions) {
112
+ return {
113
+ decision: 'deny',
114
+ gate: 'budget-action-limit',
115
+ message: `Budget exceeded: ${state.action_count}/${config.max_actions} actions used. Session budget is exhausted.`,
116
+ severity: 'critical',
117
+ reasoning: `Tool call #${state.action_count} exceeds the configured max_actions limit of ${config.max_actions}.`,
118
+ };
119
+ }
120
+
121
+ // Check time limit
122
+ if (config.max_time_minutes && state.session_start) {
123
+ const elapsedMs = Date.now() - new Date(state.session_start).getTime();
124
+ const elapsedMinutes = elapsedMs / (60 * 1000);
125
+ if (elapsedMinutes > config.max_time_minutes) {
126
+ return {
127
+ decision: 'deny',
128
+ gate: 'budget-time-limit',
129
+ message: `Budget exceeded: session has run ${Math.round(elapsedMinutes)}min, limit is ${config.max_time_minutes}min.`,
130
+ severity: 'critical',
131
+ reasoning: `Session duration (${Math.round(elapsedMinutes)}min) exceeds max_time_minutes (${config.max_time_minutes}).`,
132
+ };
133
+ }
134
+ }
135
+
136
+ return null; // Within budget
137
+ }
138
+
139
+ /**
140
+ * Get current budget status for dashboard/reporting.
141
+ */
142
+ function getBudgetStatus() {
143
+ const config = loadBudgetConfig();
144
+ const state = loadBudgetState();
145
+ const elapsedMs = state.session_start
146
+ ? Date.now() - new Date(state.session_start).getTime()
147
+ : 0;
148
+ const elapsedMinutes = Math.round(elapsedMs / (60 * 1000));
149
+
150
+ return {
151
+ action_count: state.action_count || 0,
152
+ max_actions: config.max_actions,
153
+ actions_remaining: Math.max(0, (config.max_actions || Infinity) - (state.action_count || 0)),
154
+ actions_pct: config.max_actions ? Math.round(((state.action_count || 0) / config.max_actions) * 100) : 0,
155
+ elapsed_minutes: elapsedMinutes,
156
+ max_time_minutes: config.max_time_minutes,
157
+ time_remaining_minutes: Math.max(0, (config.max_time_minutes || Infinity) - elapsedMinutes),
158
+ time_pct: config.max_time_minutes ? Math.round((elapsedMinutes / config.max_time_minutes) * 100) : 0,
159
+ session_start: state.session_start,
160
+ profile: process.env.THUMBGATE_BUDGET_PROFILE || 'guided',
161
+ };
162
+ }
163
+
164
+ module.exports = {
165
+ evaluateBudget,
166
+ getBudgetStatus,
167
+ loadBudgetConfig,
168
+ loadBudgetState,
169
+ saveBudgetState,
170
+ resetBudget,
171
+ BUDGET_STATE_PATH,
172
+ DEFAULT_BUDGET_CONFIG_PATH,
173
+ };