thumbgate 0.9.10 → 0.9.12

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 (115) hide show
  1. package/.claude-plugin/README.md +2 -2
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +1 -1
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +4 -4
  9. package/adapters/mcp/server-stdio.js +61 -1
  10. package/adapters/opencode/opencode.json +4 -2
  11. package/bin/cli.js +156 -8
  12. package/bin/memory.sh +3 -3
  13. package/config/e2e-critical-flows.json +4 -0
  14. package/config/gates/default.json +74 -2
  15. package/config/github-about.json +1 -1
  16. package/config/mcp-allowlists.json +27 -0
  17. package/package.json +22 -5
  18. package/plugins/amp-skill/INSTALL.md +1 -0
  19. package/plugins/amp-skill/SKILL.md +1 -0
  20. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  21. package/plugins/claude-codex-bridge/.mcp.json +4 -2
  22. package/plugins/claude-skill/INSTALL.md +1 -0
  23. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  24. package/plugins/codex-profile/.mcp.json +4 -2
  25. package/plugins/codex-profile/INSTALL.md +1 -1
  26. package/plugins/codex-profile/README.md +1 -1
  27. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  28. package/plugins/cursor-marketplace/README.md +3 -3
  29. package/plugins/cursor-marketplace/mcp.json +3 -1
  30. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  31. package/plugins/gemini-extension/INSTALL.md +3 -3
  32. package/plugins/opencode-profile/INSTALL.md +1 -1
  33. package/public/dashboard.html +15 -8
  34. package/public/index.html +125 -185
  35. package/public/js/buyer-intent.js +252 -0
  36. package/public/pro.html +1085 -0
  37. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  38. package/scripts/adk-consolidator.js +14 -2
  39. package/scripts/agent-readiness.js +3 -1
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/auto-promote-gates.js +2 -0
  42. package/scripts/auto-wire-hooks.js +105 -17
  43. package/scripts/behavioral-extraction.js +2 -6
  44. package/scripts/billing.js +107 -3
  45. package/scripts/budget-guard.js +2 -2
  46. package/scripts/build-metadata.js +14 -0
  47. package/scripts/context-engine.js +1 -0
  48. package/scripts/deploy-policy.js +3 -17
  49. package/scripts/dpo-optimizer.js +3 -6
  50. package/scripts/ensure-repo-bootstrap.js +129 -0
  51. package/scripts/export-dpo-pairs.js +2 -3
  52. package/scripts/export-kto-pairs.js +3 -4
  53. package/scripts/export-training.js +8 -6
  54. package/scripts/feedback-attribution.js +23 -11
  55. package/scripts/feedback-loop.js +40 -2
  56. package/scripts/feedback-to-rules.js +2 -1
  57. package/scripts/filesystem-search.js +3 -2
  58. package/scripts/gates-engine.js +760 -29
  59. package/scripts/generate-pretool-hook.sh +0 -0
  60. package/scripts/gtm-revenue-loop.js +20 -1
  61. package/scripts/hook-auto-capture.sh +8 -3
  62. package/scripts/hook-runtime.js +81 -0
  63. package/scripts/hook-stop-self-score.sh +3 -3
  64. package/scripts/hook-thumbgate-cache-updater.js +99 -38
  65. package/scripts/hosted-config.js +4 -16
  66. package/scripts/hybrid-feedback-context.js +54 -14
  67. package/scripts/install-mcp.js +13 -3
  68. package/scripts/intent-router.js +2 -2
  69. package/scripts/license.js +52 -14
  70. package/scripts/local-model-profile.js +3 -2
  71. package/scripts/mcp-config.js +62 -7
  72. package/scripts/meta-policy.js +4 -8
  73. package/scripts/money-watcher.js +166 -16
  74. package/scripts/obsidian-export.js +1 -0
  75. package/scripts/operational-integrity.js +480 -0
  76. package/scripts/post-everywhere.js +35 -12
  77. package/scripts/pr-manager.js +14 -11
  78. package/scripts/profile-router.js +2 -0
  79. package/scripts/prompt-dlp.js +1 -0
  80. package/scripts/publish-decision.js +10 -0
  81. package/scripts/published-cli.js +61 -0
  82. package/scripts/risk-scorer.js +3 -2
  83. package/scripts/rlhf_session_start.sh +32 -0
  84. package/scripts/skill-quality-tracker.js +3 -5
  85. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  86. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  87. package/scripts/social-analytics/engagement-audit.js +202 -0
  88. package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
  89. package/scripts/social-analytics/install-growth-automation.js +114 -0
  90. package/scripts/social-analytics/load-env.js +46 -0
  91. package/scripts/social-analytics/poll-all.js +23 -23
  92. package/scripts/social-analytics/pollers/plausible.js +2 -4
  93. package/scripts/social-analytics/pollers/zernio.js +3 -0
  94. package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
  95. package/scripts/social-analytics/publish-thumbgate-launch.js +322 -0
  96. package/scripts/social-analytics/publishers/reddit.js +7 -12
  97. package/scripts/social-analytics/publishers/zernio.js +301 -22
  98. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  99. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  100. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  101. package/scripts/social-post-hourly.js +185 -0
  102. package/scripts/social-quality-gate.js +119 -3
  103. package/scripts/social-reply-monitor.js +184 -37
  104. package/scripts/statusline-cache-path.js +27 -0
  105. package/scripts/statusline-local-stats.js +16 -0
  106. package/scripts/statusline-meta.js +22 -0
  107. package/scripts/statusline.sh +40 -33
  108. package/scripts/sync-version.js +24 -3
  109. package/scripts/test-coverage.js +21 -13
  110. package/scripts/tool-registry.js +97 -0
  111. package/scripts/train_from_feedback.py +32 -9
  112. package/scripts/validate-feedback.js +3 -2
  113. package/scripts/vector-store.js +2 -3
  114. package/scripts/verify-obsidian-setup.sh +3 -3
  115. package/src/api/server.js +281 -33
@@ -4,9 +4,13 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const crypto = require('crypto');
7
- const { execSync } = require('child_process');
7
+ const { execSync, execFileSync } = require('child_process');
8
8
 
9
9
  const { isProTier, FREE_TIER_MAX_GATES } = require('./rate-limiter');
10
+ const {
11
+ DEFAULT_BASE_BRANCH,
12
+ evaluateOperationalIntegrity,
13
+ } = require('./operational-integrity');
10
14
 
11
15
  /**
12
16
  * Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
@@ -50,8 +54,25 @@ const CONSTRAINTS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'se
50
54
  const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
51
55
  const SESSION_ACTIONS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'session-actions.json');
52
56
  const CUSTOM_CLAIM_GATES_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'claim-verification.json');
57
+ const GOVERNANCE_STATE_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'governance-state.json');
53
58
  const TTL_MS = 5 * 60 * 1000; // 5 minutes
54
59
  const SESSION_ACTION_TTL_MS = 60 * 60 * 1000; // 1 hour
60
+ const PROTECTED_APPROVAL_TTL_MS = 60 * 60 * 1000; // 1 hour
61
+ const DEFAULT_PROTECTED_FILE_GLOBS = [
62
+ 'AGENTS.md',
63
+ 'CLAUDE.md',
64
+ 'CLAUDE.local.md',
65
+ 'GEMINI.md',
66
+ 'README.md',
67
+ '.gitignore',
68
+ '.husky/**',
69
+ '.claude/**',
70
+ 'skills/**',
71
+ 'SKILL.md',
72
+ 'config/gates/**',
73
+ ];
74
+ const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
75
+ const HIGH_RISK_BASH_PATTERN = /\b(?:git\s+(?:add|commit|push)|gh\s+pr\s+(?:create|merge)|npm\s+publish|yarn\s+publish|pnpm\s+publish|rm\s+-rf)\b/i;
55
76
 
56
77
  // ---------------------------------------------------------------------------
57
78
  // Config loading
@@ -123,6 +144,213 @@ function saveState(state) { saveJSON(module.exports.STATE_PATH, state); }
123
144
  function loadConstraints() { return loadJSON(module.exports.CONSTRAINTS_PATH); }
124
145
  function saveConstraints(constraints) { saveJSON(module.exports.CONSTRAINTS_PATH, constraints); }
125
146
 
147
+ function normalizePosix(filePath) {
148
+ return String(filePath || '')
149
+ .replace(/\\/g, '/')
150
+ .replace(/^\.\//, '')
151
+ .replace(/^\/+/, '')
152
+ .trim();
153
+ }
154
+
155
+ function normalizeGlob(glob) {
156
+ return normalizePosix(glob).replace(/\/+$/, '');
157
+ }
158
+
159
+ function sanitizeGlobList(globs) {
160
+ if (!Array.isArray(globs)) return [];
161
+ return [...new Set(globs.map((glob) => normalizeGlob(glob)).filter(Boolean))];
162
+ }
163
+
164
+ function globToRegExp(glob) {
165
+ const normalized = normalizeGlob(glob);
166
+ let pattern = '^';
167
+ for (let i = 0; i < normalized.length; i++) {
168
+ const char = normalized[i];
169
+ const next = normalized[i + 1];
170
+ if (char === '*') {
171
+ if (next === '*') {
172
+ pattern += '.*';
173
+ i += 1;
174
+ } else {
175
+ pattern += '[^/]*';
176
+ }
177
+ continue;
178
+ }
179
+ if ('\\^$+?.()|{}[]'.includes(char)) {
180
+ pattern += `\\${char}`;
181
+ continue;
182
+ }
183
+ pattern += char;
184
+ }
185
+ pattern += '$';
186
+ return new RegExp(pattern);
187
+ }
188
+
189
+ function matchesGlob(filePath, glob) {
190
+ if (!glob) return false;
191
+ try {
192
+ return globToRegExp(glob).test(normalizePosix(filePath));
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ function matchesAnyGlob(filePath, globs) {
199
+ return sanitizeGlobList(globs).some((glob) => matchesGlob(filePath, glob));
200
+ }
201
+
202
+ function clampTtlMs(value, fallbackMs) {
203
+ const fallback = Number.isFinite(fallbackMs) ? fallbackMs : PROTECTED_APPROVAL_TTL_MS;
204
+ const numeric = Number(value);
205
+ if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
206
+ return Math.min(Math.max(numeric, 60 * 1000), 24 * 60 * 60 * 1000);
207
+ }
208
+
209
+ function loadGovernanceState() {
210
+ const raw = loadJSON(module.exports.GOVERNANCE_STATE_PATH);
211
+ const state = {
212
+ taskScope: raw && raw.taskScope && typeof raw.taskScope === 'object' ? raw.taskScope : null,
213
+ protectedApprovals: Array.isArray(raw && raw.protectedApprovals) ? raw.protectedApprovals : [],
214
+ branchGovernance: raw && raw.branchGovernance && typeof raw.branchGovernance === 'object'
215
+ ? raw.branchGovernance
216
+ : null,
217
+ };
218
+ const now = Date.now();
219
+ const activeApprovals = state.protectedApprovals.filter((entry) => {
220
+ if (!entry || typeof entry !== 'object') return false;
221
+ if (!entry.timestamp || !entry.expiresAt) return false;
222
+ return now < entry.expiresAt;
223
+ });
224
+ if (activeApprovals.length !== state.protectedApprovals.length) {
225
+ state.protectedApprovals = activeApprovals;
226
+ saveGovernanceState(state);
227
+ }
228
+ return state;
229
+ }
230
+
231
+ function saveGovernanceState(state) {
232
+ const next = {
233
+ taskScope: state && state.taskScope ? state.taskScope : null,
234
+ protectedApprovals: Array.isArray(state && state.protectedApprovals) ? state.protectedApprovals : [],
235
+ branchGovernance: state && state.branchGovernance ? state.branchGovernance : null,
236
+ };
237
+ saveJSON(module.exports.GOVERNANCE_STATE_PATH, next);
238
+ }
239
+
240
+ function setTaskScope(scopeInput = {}) {
241
+ if (scopeInput && scopeInput.clear === true) {
242
+ const currentState = loadGovernanceState();
243
+ const cleared = {
244
+ taskScope: null,
245
+ protectedApprovals: currentState.protectedApprovals,
246
+ branchGovernance: currentState.branchGovernance,
247
+ };
248
+ saveGovernanceState(cleared);
249
+ return null;
250
+ }
251
+
252
+ const allowedPaths = sanitizeGlobList(scopeInput.allowedPaths);
253
+ if (allowedPaths.length === 0) {
254
+ throw new Error('allowedPaths must be a non-empty array');
255
+ }
256
+
257
+ const protectedPaths = sanitizeGlobList(
258
+ Array.isArray(scopeInput.protectedPaths) && scopeInput.protectedPaths.length > 0
259
+ ? scopeInput.protectedPaths
260
+ : DEFAULT_PROTECTED_FILE_GLOBS
261
+ );
262
+ const taskScope = {
263
+ taskId: String(scopeInput.taskId || '').trim() || null,
264
+ summary: String(scopeInput.summary || '').trim() || null,
265
+ allowedPaths,
266
+ protectedPaths,
267
+ localOnly: scopeInput.localOnly === true,
268
+ repoPath: String(scopeInput.repoPath || '').trim() || null,
269
+ createdAt: new Date().toISOString(),
270
+ timestamp: Date.now(),
271
+ };
272
+ const state = loadGovernanceState();
273
+ state.taskScope = taskScope;
274
+ saveGovernanceState(state);
275
+ if (taskScope.localOnly) {
276
+ setConstraint('local_only', true);
277
+ }
278
+ return taskScope;
279
+ }
280
+
281
+ function approveProtectedAction(input = {}) {
282
+ const pathGlobs = sanitizeGlobList(input.pathGlobs);
283
+ if (pathGlobs.length === 0) {
284
+ throw new Error('pathGlobs must be a non-empty array');
285
+ }
286
+ const reason = String(input.reason || '').trim();
287
+ if (!reason) {
288
+ throw new Error('reason is required');
289
+ }
290
+
291
+ const ttlMs = clampTtlMs(input.ttlMs, PROTECTED_APPROVAL_TTL_MS);
292
+ const now = Date.now();
293
+ const entry = {
294
+ id: `approval_${now}_${Math.random().toString(36).slice(2, 8)}`,
295
+ pathGlobs,
296
+ reason,
297
+ evidence: String(input.evidence || '').trim() || null,
298
+ taskId: String(input.taskId || '').trim() || null,
299
+ timestamp: now,
300
+ expiresAt: now + ttlMs,
301
+ };
302
+
303
+ const state = loadGovernanceState();
304
+ state.protectedApprovals.push(entry);
305
+ saveGovernanceState(state);
306
+ return entry;
307
+ }
308
+
309
+ function setBranchGovernance(input = {}) {
310
+ if (input && input.clear === true) {
311
+ const state = loadGovernanceState();
312
+ state.branchGovernance = null;
313
+ saveGovernanceState(state);
314
+ return null;
315
+ }
316
+
317
+ const branchName = String(input.branchName || '').trim() || null;
318
+ const baseBranch = String(input.baseBranch || '').trim() || DEFAULT_BASE_BRANCH;
319
+ const releaseSensitiveGlobs = sanitizeGlobList(
320
+ Array.isArray(input.releaseSensitiveGlobs) ? input.releaseSensitiveGlobs : []
321
+ );
322
+ const governance = {
323
+ branchName,
324
+ baseBranch,
325
+ prRequired: input.prRequired !== false,
326
+ prNumber: String(input.prNumber || '').trim() || null,
327
+ prUrl: String(input.prUrl || '').trim() || null,
328
+ queueRequired: input.queueRequired === true,
329
+ localOnly: input.localOnly === true,
330
+ releaseVersion: String(input.releaseVersion || '').trim() || null,
331
+ releaseEvidence: String(input.releaseEvidence || '').trim() || null,
332
+ releaseSensitiveGlobs,
333
+ timestamp: Date.now(),
334
+ createdAt: new Date().toISOString(),
335
+ };
336
+
337
+ const state = loadGovernanceState();
338
+ state.branchGovernance = governance;
339
+ saveGovernanceState(state);
340
+ if (governance.localOnly) {
341
+ setConstraint('local_only', true);
342
+ }
343
+ return governance;
344
+ }
345
+
346
+ function getScopeState() {
347
+ return loadGovernanceState();
348
+ }
349
+
350
+ function getBranchGovernanceState() {
351
+ return loadGovernanceState().branchGovernance;
352
+ }
353
+
126
354
  function setConstraint(key, value) {
127
355
  const constraints = loadConstraints();
128
356
  constraints[key] = {
@@ -198,6 +426,273 @@ function recordStat(gateId, action, gate) {
198
426
  // Reasoning chain builder
199
427
  // ---------------------------------------------------------------------------
200
428
 
429
+ function getHybridFeedbackModule() {
430
+ try {
431
+ return require('./hybrid-feedback-context');
432
+ } catch {
433
+ return null;
434
+ }
435
+ }
436
+
437
+ function safeExecFileLines(binary, args, cwd) {
438
+ try {
439
+ const output = execFileSync(binary, args, {
440
+ cwd,
441
+ encoding: 'utf8',
442
+ stdio: ['ignore', 'pipe', 'ignore'],
443
+ }).trim();
444
+ if (!output) return [];
445
+ return output.split('\n').map((line) => line.trim()).filter(Boolean);
446
+ } catch {
447
+ return [];
448
+ }
449
+ }
450
+
451
+ function resolveRepoRoot(toolInput = {}) {
452
+ const candidates = [
453
+ toolInput.repoPath,
454
+ toolInput.cwd,
455
+ process.cwd(),
456
+ ]
457
+ .filter(Boolean)
458
+ .map((value) => path.resolve(String(value)));
459
+
460
+ for (const cwd of candidates) {
461
+ try {
462
+ const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
463
+ cwd,
464
+ encoding: 'utf8',
465
+ stdio: ['ignore', 'pipe', 'ignore'],
466
+ }).trim();
467
+ if (root) return root;
468
+ } catch {
469
+ continue;
470
+ }
471
+ }
472
+
473
+ return null;
474
+ }
475
+
476
+ function toRepoRelativePath(filePath, repoRoot) {
477
+ const value = String(filePath || '').trim();
478
+ if (!value) return '';
479
+ if (repoRoot && path.isAbsolute(value)) {
480
+ const relative = path.relative(repoRoot, value);
481
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
482
+ return normalizePosix(relative);
483
+ }
484
+ }
485
+ return normalizePosix(value);
486
+ }
487
+
488
+ function collectInlineAffectedFiles(toolInput = {}, repoRoot) {
489
+ const collected = [];
490
+ const arrayFields = [
491
+ toolInput.changed_files,
492
+ toolInput.changedFiles,
493
+ toolInput.files,
494
+ toolInput.file_paths,
495
+ toolInput.filePaths,
496
+ toolInput.paths,
497
+ ];
498
+
499
+ for (const field of arrayFields) {
500
+ if (!Array.isArray(field)) continue;
501
+ for (const entry of field) {
502
+ const normalized = toRepoRelativePath(entry, repoRoot);
503
+ if (normalized) collected.push(normalized);
504
+ }
505
+ }
506
+
507
+ const scalarFields = [
508
+ toolInput.file_path,
509
+ toolInput.filePath,
510
+ toolInput.path,
511
+ ];
512
+ for (const field of scalarFields) {
513
+ const normalized = toRepoRelativePath(field, repoRoot);
514
+ if (normalized) collected.push(normalized);
515
+ }
516
+
517
+ return [...new Set(collected)];
518
+ }
519
+
520
+ function getUpstreamRef(repoRoot) {
521
+ const upstream = safeExecFileLines('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], repoRoot)[0];
522
+ if (upstream) return upstream;
523
+ const remoteHead = safeExecFileLines('git', ['symbolic-ref', 'refs/remotes/origin/HEAD'], repoRoot)[0];
524
+ if (remoteHead) return remoteHead.replace(/^refs\/remotes\//, '');
525
+ return null;
526
+ }
527
+
528
+ function getBranchDiffFiles(repoRoot) {
529
+ const upstream = getUpstreamRef(repoRoot);
530
+ if (upstream) {
531
+ return safeExecFileLines('git', ['diff', '--name-only', `${upstream}...HEAD`], repoRoot);
532
+ }
533
+ const headParent = safeExecFileLines('git', ['rev-parse', '--verify', 'HEAD~1'], repoRoot)[0];
534
+ if (headParent) {
535
+ return safeExecFileLines('git', ['diff', '--name-only', 'HEAD~1..HEAD'], repoRoot);
536
+ }
537
+ return safeExecFileLines('git', ['diff', '--name-only'], repoRoot);
538
+ }
539
+
540
+ function extractAffectedFiles(toolName, toolInput = {}) {
541
+ const repoRoot = resolveRepoRoot(toolInput);
542
+ const files = new Set(collectInlineAffectedFiles(toolInput, repoRoot));
543
+ const command = String(toolInput.command || '');
544
+
545
+ if (toolName === 'Bash' && repoRoot && command) {
546
+ if (/\bgit\s+commit\b/i.test(command)) {
547
+ for (const filePath of safeExecFileLines('git', ['diff', '--cached', '--name-only'], repoRoot)) {
548
+ files.add(normalizePosix(filePath));
549
+ }
550
+ }
551
+
552
+ if (/\bgit\s+add\b/i.test(command)) {
553
+ for (const filePath of safeExecFileLines('git', ['diff', '--name-only'], repoRoot)) {
554
+ files.add(normalizePosix(filePath));
555
+ }
556
+ for (const filePath of safeExecFileLines('git', ['ls-files', '--others', '--exclude-standard'], repoRoot)) {
557
+ files.add(normalizePosix(filePath));
558
+ }
559
+ }
560
+
561
+ if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command)) {
562
+ for (const filePath of getBranchDiffFiles(repoRoot)) {
563
+ files.add(normalizePosix(filePath));
564
+ }
565
+ }
566
+ }
567
+
568
+ return {
569
+ repoRoot,
570
+ files: [...files].filter(Boolean),
571
+ };
572
+ }
573
+
574
+ function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
575
+ if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
576
+ if (toolName !== 'Bash') return false;
577
+ const command = String(toolInput.command || '');
578
+ return HIGH_RISK_BASH_PATTERN.test(command);
579
+ }
580
+
581
+ function isScopeEnforcedAction(toolName, toolInput = {}, affectedFiles = []) {
582
+ if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
583
+ if (toolName !== 'Bash') return false;
584
+ const command = String(toolInput.command || '');
585
+ if (!HIGH_RISK_BASH_PATTERN.test(command)) return false;
586
+ return affectedFiles.length > 0;
587
+ }
588
+
589
+ function shouldEnforceTaskScope(gate, governanceState, toolName, toolInput = {}, affectedFiles = []) {
590
+ if (gate.scopeMode === 'declared-only') {
591
+ return Boolean(governanceState && governanceState.taskScope) &&
592
+ EDIT_LIKE_TOOLS.has(toolName) &&
593
+ affectedFiles.length > 0;
594
+ }
595
+ return isScopeEnforcedAction(toolName, toolInput, affectedFiles);
596
+ }
597
+
598
+ function formatFileList(files, limit = 5) {
599
+ const items = Array.isArray(files) ? files.filter(Boolean) : [];
600
+ if (items.length === 0) return 'none';
601
+ if (items.length <= limit) return items.join(', ');
602
+ return `${items.slice(0, limit).join(', ')} (+${items.length - limit} more)`;
603
+ }
604
+
605
+ function buildTaskScopeViolation(taskScope, affectedFiles) {
606
+ if (!Array.isArray(affectedFiles) || affectedFiles.length === 0) return null;
607
+ if (!taskScope || !Array.isArray(taskScope.allowedPaths) || taskScope.allowedPaths.length === 0) {
608
+ return {
609
+ reasonCode: 'missing_task_scope',
610
+ outsideFiles: affectedFiles.slice(),
611
+ allowedPaths: [],
612
+ summary: null,
613
+ };
614
+ }
615
+ const outsideFiles = affectedFiles.filter((filePath) => !matchesAnyGlob(filePath, taskScope.allowedPaths));
616
+ if (outsideFiles.length === 0) return null;
617
+ return {
618
+ reasonCode: 'outside_declared_scope',
619
+ outsideFiles,
620
+ allowedPaths: taskScope.allowedPaths.slice(),
621
+ summary: taskScope.summary || null,
622
+ };
623
+ }
624
+
625
+ function buildProtectedApprovalViolation(protectedGlobs, approvals, affectedFiles) {
626
+ const normalizedProtected = sanitizeGlobList(protectedGlobs);
627
+ if (normalizedProtected.length === 0 || !Array.isArray(affectedFiles) || affectedFiles.length === 0) {
628
+ return null;
629
+ }
630
+ const protectedFiles = affectedFiles.filter((filePath) => matchesAnyGlob(filePath, normalizedProtected));
631
+ if (protectedFiles.length === 0) return null;
632
+
633
+ const activeApprovals = Array.isArray(approvals) ? approvals : [];
634
+ const missingApprovalFiles = protectedFiles.filter((filePath) => {
635
+ return !activeApprovals.some((entry) => matchesAnyGlob(filePath, entry.pathGlobs || []));
636
+ });
637
+ if (missingApprovalFiles.length === 0) return null;
638
+
639
+ return {
640
+ protectedFiles,
641
+ missingApprovalFiles,
642
+ protectedGlobs: normalizedProtected,
643
+ };
644
+ }
645
+
646
+ function buildBranchGovernanceViolation(governanceState, toolInput = {}, affectedFiles = [], repoRoot = null, requireReleaseReadiness = false) {
647
+ const command = String(toolInput.command || '').trim();
648
+ if (!command) return null;
649
+
650
+ const integrity = evaluateOperationalIntegrity({
651
+ repoPath: repoRoot || (governanceState && governanceState.taskScope && governanceState.taskScope.repoPath) || process.cwd(),
652
+ branchGovernance: governanceState ? governanceState.branchGovernance : null,
653
+ changedFiles: affectedFiles,
654
+ command,
655
+ requireVersionNotBehindBase: requireReleaseReadiness,
656
+ });
657
+
658
+ if (!integrity || integrity.blockers.length === 0) {
659
+ return null;
660
+ }
661
+
662
+ return {
663
+ blockers: integrity.blockers,
664
+ currentBranch: integrity.currentBranch,
665
+ baseBranch: integrity.baseBranch,
666
+ releaseSensitiveFiles: integrity.releaseSensitiveFiles,
667
+ packageVersion: integrity.packageVersion,
668
+ baseVersion: integrity.baseVersion,
669
+ };
670
+ }
671
+
672
+ function buildGateMessage(gate, matchDetails) {
673
+ if (matchDetails && matchDetails.taskScopeViolation) {
674
+ const violation = matchDetails.taskScopeViolation;
675
+ if (violation.reasonCode === 'missing_task_scope') {
676
+ return `No task scope is declared for this high-risk action. Affected files: ${formatFileList(violation.outsideFiles)}.`;
677
+ }
678
+ return `Action touches files outside the declared task scope: ${formatFileList(violation.outsideFiles)}. Allowed paths: ${formatFileList(violation.allowedPaths)}.`;
679
+ }
680
+
681
+ if (matchDetails && matchDetails.protectedApprovalViolation) {
682
+ const violation = matchDetails.protectedApprovalViolation;
683
+ return `Protected files require explicit approval before editing or publishing. Missing approval for: ${formatFileList(violation.missingApprovalFiles)}.`;
684
+ }
685
+
686
+ if (matchDetails && matchDetails.branchGovernanceViolation) {
687
+ const [firstBlocker] = matchDetails.branchGovernanceViolation.blockers || [];
688
+ if (firstBlocker && firstBlocker.message) {
689
+ return firstBlocker.message;
690
+ }
691
+ }
692
+
693
+ return gate.message;
694
+ }
695
+
201
696
  /**
202
697
  * Build a human-readable reasoning chain explaining WHY a gate decision was made.
203
698
  * Returns an array of evidence steps — each a short sentence a developer can scan.
@@ -210,10 +705,14 @@ function recordStat(gateId, action, gate) {
210
705
  */
211
706
  function buildReasoning(gate, toolName, toolInput, extras = {}) {
212
707
  const steps = [];
213
- const text = toolInput.command || toolInput.file_path || toolInput.path || '';
708
+ const text = extras.matchText || toolInput.command || toolInput.file_path || toolInput.path || '';
214
709
 
215
710
  // 1. What matched
216
- steps.push(`Pattern /${gate.pattern}/ matched "${text.length > 80 ? text.slice(0, 80) + '…' : text}"`);
711
+ if (gate.pattern) {
712
+ steps.push(`Pattern /${gate.pattern}/ matched "${text.length > 80 ? text.slice(0, 80) + '…' : text}"`);
713
+ } else {
714
+ steps.push(`Structural gate ${gate.id} matched requested action on "${text.length > 80 ? text.slice(0, 80) + '…' : text}"`);
715
+ }
217
716
 
218
717
  // 2. Gate identity
219
718
  steps.push(`Gate ${gate.id} [${gate.action}] — layer: ${gate.layer || 'Execution'}, severity: ${gate.severity || 'medium'}`);
@@ -232,6 +731,39 @@ function buildReasoning(gate, toolName, toolInput, extras = {}) {
232
731
  steps.push(`Active because constraint ${keys} is set`);
233
732
  }
234
733
 
734
+ if (extras.affectedFiles && extras.affectedFiles.length > 0) {
735
+ steps.push(`Affected files: ${formatFileList(extras.affectedFiles)}`);
736
+ }
737
+
738
+ if (extras.taskScopeViolation) {
739
+ if (extras.taskScopeViolation.reasonCode === 'missing_task_scope') {
740
+ steps.push('No active task scope is declared for this high-risk action');
741
+ } else {
742
+ steps.push(`Outside declared task scope: ${formatFileList(extras.taskScopeViolation.outsideFiles)}`);
743
+ steps.push(`Declared scope: ${formatFileList(extras.taskScopeViolation.allowedPaths)}`);
744
+ }
745
+ }
746
+
747
+ if (extras.protectedApprovalViolation) {
748
+ steps.push(`Protected files without approval: ${formatFileList(extras.protectedApprovalViolation.missingApprovalFiles)}`);
749
+ }
750
+
751
+ if (extras.branchGovernanceViolation) {
752
+ if (extras.branchGovernanceViolation.currentBranch || extras.branchGovernanceViolation.baseBranch) {
753
+ steps.push(`Branch governance context: ${extras.branchGovernanceViolation.currentBranch || 'unknown'} -> ${extras.branchGovernanceViolation.baseBranch || 'unknown'}`);
754
+ }
755
+ if (extras.branchGovernanceViolation.releaseSensitiveFiles && extras.branchGovernanceViolation.releaseSensitiveFiles.length > 0) {
756
+ steps.push(`Release-sensitive files: ${formatFileList(extras.branchGovernanceViolation.releaseSensitiveFiles)}`);
757
+ }
758
+ for (const blocker of extras.branchGovernanceViolation.blockers || []) {
759
+ steps.push(`Branch governance blocker: ${blocker.code} — ${blocker.message}`);
760
+ }
761
+ }
762
+
763
+ if (extras.memoryGuard && extras.memoryGuard.reason) {
764
+ steps.push(`Memory guard matched (${extras.memoryGuard.source}): ${extras.memoryGuard.reason}`);
765
+ }
766
+
235
767
  // 5. Unless condition status
236
768
  if (gate.unless) {
237
769
  steps.push(`Bypassable via satisfy_gate("${gate.unless}") — not currently satisfied`);
@@ -269,26 +801,176 @@ function checkWhenClause(when, constraints) {
269
801
  return true;
270
802
  }
271
803
 
272
- function matchesGate(gate, _toolName, toolInput) {
273
- // Build the text to match against: for Bash it's the command, for Edit it's the file path
274
- const text = toolInput.command || toolInput.file_path || toolInput.path || '';
275
-
276
- // 1. Check Regex Pattern
277
- try {
278
- const regex = new RegExp(gate.pattern);
279
- if (!regex.test(text)) return false;
280
- } catch {
281
- return false;
804
+ function matchGate(gate, toolName, toolInput = {}) {
805
+ const matchText = toolInput.command || toolInput.file_path || toolInput.path || '';
806
+ const affected = extractAffectedFiles(toolName, toolInput);
807
+ const affectedFiles = affected.files;
808
+ const repoRoot = affected.repoRoot;
809
+ const governanceState = loadGovernanceState();
810
+
811
+ if (Array.isArray(gate.toolNames) && gate.toolNames.length > 0 && !gate.toolNames.includes(toolName)) {
812
+ return { matched: false, matchText, affectedFiles };
813
+ }
814
+
815
+ if (gate.pattern) {
816
+ try {
817
+ const regex = new RegExp(gate.pattern);
818
+ if (!regex.test(matchText)) return { matched: false, matchText, affectedFiles };
819
+ } catch {
820
+ return { matched: false, matchText, affectedFiles };
821
+ }
282
822
  }
283
823
 
284
- // 2. Check Executable Hash (New: Layer 5 Anti-Bypass)
285
- // If a hash is specified, we must verify the content of the binary
286
824
  if (gate.executable_hash && toolInput.command) {
287
825
  const actualHash = computeExecutableHash(toolInput.command);
288
- if (actualHash !== gate.executable_hash) return false;
826
+ if (actualHash !== gate.executable_hash) return { matched: false, matchText, affectedFiles };
289
827
  }
290
828
 
291
- return true;
829
+ if (Array.isArray(gate.fileGlobs) && gate.fileGlobs.length > 0) {
830
+ const scopedFiles = affectedFiles.filter((filePath) => matchesAnyGlob(filePath, gate.fileGlobs));
831
+ if (scopedFiles.length === 0) return { matched: false, matchText, affectedFiles };
832
+ }
833
+
834
+ let taskScopeViolation = null;
835
+ if (gate.requireTaskScope) {
836
+ if (!shouldEnforceTaskScope(gate, governanceState, toolName, toolInput, affectedFiles)) {
837
+ return { matched: false, matchText, affectedFiles };
838
+ }
839
+ taskScopeViolation = buildTaskScopeViolation(governanceState.taskScope, affectedFiles);
840
+ if (!taskScopeViolation) return { matched: false, matchText, affectedFiles };
841
+ }
842
+
843
+ let protectedApprovalViolation = null;
844
+ if (gate.requireProtectedApproval) {
845
+ const protectedGlobs = sanitizeGlobList(
846
+ Array.isArray(gate.protectedGlobs) && gate.protectedGlobs.length > 0
847
+ ? gate.protectedGlobs
848
+ : (governanceState.taskScope && governanceState.taskScope.protectedPaths) || DEFAULT_PROTECTED_FILE_GLOBS
849
+ );
850
+ protectedApprovalViolation = buildProtectedApprovalViolation(
851
+ protectedGlobs,
852
+ governanceState.protectedApprovals,
853
+ affectedFiles,
854
+ );
855
+ if (!protectedApprovalViolation) return { matched: false, matchText, affectedFiles };
856
+ }
857
+
858
+ let branchGovernanceViolation = null;
859
+ if (gate.requireBranchGovernance || gate.requireReleaseReadiness) {
860
+ branchGovernanceViolation = buildBranchGovernanceViolation(
861
+ governanceState,
862
+ toolInput,
863
+ affectedFiles,
864
+ repoRoot,
865
+ gate.requireReleaseReadiness === true,
866
+ );
867
+ if (!branchGovernanceViolation) return { matched: false, matchText, affectedFiles };
868
+ }
869
+
870
+ return {
871
+ matched: true,
872
+ matchText,
873
+ affectedFiles,
874
+ taskScopeViolation,
875
+ protectedApprovalViolation,
876
+ branchGovernanceViolation,
877
+ };
878
+ }
879
+
880
+ function matchesGate(gate, toolName, toolInput) {
881
+ return matchGate(gate, toolName, toolInput).matched;
882
+ }
883
+
884
+ function evaluateMemoryGuard(toolName, toolInput = {}) {
885
+ const affected = extractAffectedFiles(toolName, toolInput);
886
+ const affectedFiles = affected.files;
887
+ if (!isHighRiskAction(toolName, toolInput, affectedFiles)) {
888
+ return null;
889
+ }
890
+ const governanceState = loadGovernanceState();
891
+
892
+ if (isScopeEnforcedAction(toolName, toolInput, affectedFiles)) {
893
+ const scopeViolation = buildTaskScopeViolation(governanceState.taskScope, affectedFiles);
894
+ if (!scopeViolation) {
895
+ return null;
896
+ }
897
+ }
898
+
899
+ const command = String(toolInput.command || '');
900
+ if (toolName === 'Bash' && /\bgh\s+pr\s+create\b/i.test(command) && isConditionSatisfied('pr_create_allowed')) {
901
+ const branchGovernanceViolation = buildBranchGovernanceViolation(
902
+ governanceState,
903
+ toolInput,
904
+ affectedFiles,
905
+ affected.repoRoot,
906
+ /\b(?:npm|yarn|pnpm)\s+publish\b|\bgh\s+release\s+create\b|\bgit\s+tag\b/i.test(command),
907
+ );
908
+ if (!branchGovernanceViolation) {
909
+ return null;
910
+ }
911
+ }
912
+
913
+ if (toolName === 'Bash' && /\b(?:gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|git\s+tag\b|(?:npm|yarn|pnpm)\s+publish\b)\b/i.test(command)) {
914
+ const branchGovernanceViolation = buildBranchGovernanceViolation(
915
+ governanceState,
916
+ toolInput,
917
+ affectedFiles,
918
+ affected.repoRoot,
919
+ /\b(?:npm|yarn|pnpm)\s+publish\b|\bgh\s+release\s+create\b|\bgit\s+tag\b/i.test(command),
920
+ );
921
+ if (!branchGovernanceViolation) {
922
+ return null;
923
+ }
924
+ }
925
+
926
+ const protectedGlobs = sanitizeGlobList(
927
+ (governanceState.taskScope && governanceState.taskScope.protectedPaths) || DEFAULT_PROTECTED_FILE_GLOBS
928
+ );
929
+ if (affectedFiles.length > 0 && protectedGlobs.length > 0) {
930
+ const protectedApprovalViolation = buildProtectedApprovalViolation(
931
+ protectedGlobs,
932
+ governanceState.protectedApprovals,
933
+ affectedFiles,
934
+ );
935
+ if (!protectedApprovalViolation && affectedFiles.some((filePath) => matchesAnyGlob(filePath, protectedGlobs))) {
936
+ return null;
937
+ }
938
+ }
939
+
940
+ const hybrid = getHybridFeedbackModule();
941
+ if (!hybrid || typeof hybrid.evaluatePretool !== 'function') {
942
+ return null;
943
+ }
944
+
945
+ const serializedInput = JSON.stringify({
946
+ toolName,
947
+ command: toolInput.command || null,
948
+ filePath: toolInput.file_path || toolInput.path || null,
949
+ affectedFiles,
950
+ });
951
+ const guard = hybrid.evaluatePretool(toolName, serializedInput);
952
+ if (!guard || guard.mode === 'allow') {
953
+ return null;
954
+ }
955
+
956
+ const message = `Recurring negative memory matched a high-risk action. Denied by default until scope/approval is made explicit. ${guard.reason}`;
957
+ return {
958
+ decision: 'deny',
959
+ gate: 'memory-high-risk-default-deny',
960
+ message,
961
+ severity: 'critical',
962
+ reasoning: buildReasoning({
963
+ id: 'memory-high-risk-default-deny',
964
+ action: 'block',
965
+ layer: 'Memory',
966
+ severity: 'critical',
967
+ message,
968
+ }, toolName, toolInput, {
969
+ matchText: toolInput.command || toolInput.file_path || toolInput.path || '',
970
+ affectedFiles,
971
+ memoryGuard: guard,
972
+ }),
973
+ };
292
974
  }
293
975
 
294
976
  async function checkMetricCondition(metricCondition) {
@@ -320,7 +1002,8 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
320
1002
  const skipMetrics = METRIC_SKIP_TOOLS.includes(toolName);
321
1003
 
322
1004
  for (const gate of config.gates) {
323
- if (!matchesGate(gate, toolName, toolInput)) continue;
1005
+ const matchDetails = matchGate(gate, toolName, toolInput);
1006
+ if (!matchDetails.matched) continue;
324
1007
 
325
1008
  // EvoSkill Hardening: check contextual 'when' clause
326
1009
  if (gate.when && !checkWhenClause(gate.when, constraints)) {
@@ -352,25 +1035,45 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
352
1035
  continue;
353
1036
  }
354
1037
 
355
- const reasoning = buildReasoning(gate, toolName, toolInput, { metricFailed });
1038
+ const message = buildGateMessage(gate, matchDetails);
1039
+ const reasoning = buildReasoning(gate, toolName, toolInput, {
1040
+ metricFailed,
1041
+ ...matchDetails,
1042
+ });
356
1043
 
357
1044
  if (gate.action === 'block') {
358
1045
  recordStat(gate.id, 'block', gate);
359
- const result = { decision: 'deny', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
360
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
1046
+ const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
1047
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
361
1048
  auditToFeedback(auditRecord);
362
1049
  return result;
363
1050
  }
364
1051
 
365
1052
  if (gate.action === 'warn') {
366
1053
  recordStat(gate.id, 'warn', gate);
367
- const result = { decision: 'warn', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
368
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
1054
+ const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
1055
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
369
1056
  auditToFeedback(auditRecord);
370
1057
  return result;
371
1058
  }
372
1059
  }
373
1060
 
1061
+ const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
1062
+ if (memoryGuard) {
1063
+ recordStat(memoryGuard.gate, 'block');
1064
+ const auditRecord = recordAuditEvent({
1065
+ toolName,
1066
+ toolInput,
1067
+ decision: 'deny',
1068
+ gateId: memoryGuard.gate,
1069
+ message: memoryGuard.message,
1070
+ severity: memoryGuard.severity,
1071
+ source: 'gates-engine',
1072
+ });
1073
+ auditToFeedback(auditRecord);
1074
+ return memoryGuard;
1075
+ }
1076
+
374
1077
  // Audit trail: record allow (no gate matched)
375
1078
  recordAuditEvent({ toolName, toolInput, decision: 'allow', source: 'gates-engine' });
376
1079
  return null;
@@ -388,7 +1091,8 @@ function evaluateGates(toolName, toolInput, configPath) {
388
1091
  const constraints = loadConstraints();
389
1092
 
390
1093
  for (const gate of config.gates) {
391
- if (!matchesGate(gate, toolName, toolInput)) continue;
1094
+ const matchDetails = matchGate(gate, toolName, toolInput);
1095
+ if (!matchDetails.matched) continue;
392
1096
 
393
1097
  // EvoSkill Hardening: check contextual 'when' clause
394
1098
  if (gate.when && !checkWhenClause(gate.when, constraints)) {
@@ -400,25 +1104,42 @@ function evaluateGates(toolName, toolInput, configPath) {
400
1104
  continue;
401
1105
  }
402
1106
 
403
- const reasoning = buildReasoning(gate, toolName, toolInput);
1107
+ const message = buildGateMessage(gate, matchDetails);
1108
+ const reasoning = buildReasoning(gate, toolName, toolInput, matchDetails);
404
1109
 
405
1110
  if (gate.action === 'block') {
406
1111
  recordStat(gate.id, 'block', gate);
407
- const result = { decision: 'deny', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
408
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
1112
+ const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
1113
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
409
1114
  auditToFeedback(auditRecord);
410
1115
  return result;
411
1116
  }
412
1117
 
413
1118
  if (gate.action === 'warn') {
414
1119
  recordStat(gate.id, 'warn', gate);
415
- const result = { decision: 'warn', gate: gate.id, message: gate.message, severity: gate.severity, reasoning };
416
- const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: gate.message, severity: gate.severity, source: 'gates-engine' });
1120
+ const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
1121
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
417
1122
  auditToFeedback(auditRecord);
418
1123
  return result;
419
1124
  }
420
1125
  }
421
1126
 
1127
+ const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
1128
+ if (memoryGuard) {
1129
+ recordStat(memoryGuard.gate, 'block');
1130
+ const auditRecord = recordAuditEvent({
1131
+ toolName,
1132
+ toolInput,
1133
+ decision: 'deny',
1134
+ gateId: memoryGuard.gate,
1135
+ message: memoryGuard.message,
1136
+ severity: memoryGuard.severity,
1137
+ source: 'gates-engine',
1138
+ });
1139
+ auditToFeedback(auditRecord);
1140
+ return memoryGuard;
1141
+ }
1142
+
422
1143
  // Audit trail: record allow
423
1144
  recordAuditEvent({ toolName, toolInput, decision: 'allow', source: 'gates-engine' });
424
1145
  return null;
@@ -760,6 +1481,13 @@ module.exports = {
760
1481
  loadConstraints,
761
1482
  saveConstraints,
762
1483
  setConstraint,
1484
+ loadGovernanceState,
1485
+ saveGovernanceState,
1486
+ setTaskScope,
1487
+ setBranchGovernance,
1488
+ approveProtectedAction,
1489
+ getScopeState,
1490
+ getBranchGovernanceState,
763
1491
  isConditionSatisfied,
764
1492
  satisfyCondition,
765
1493
  loadStats,
@@ -789,8 +1517,11 @@ module.exports = {
789
1517
  STATS_PATH,
790
1518
  SESSION_ACTIONS_PATH,
791
1519
  CUSTOM_CLAIM_GATES_PATH,
1520
+ GOVERNANCE_STATE_PATH,
792
1521
  TTL_MS,
793
1522
  SESSION_ACTION_TTL_MS,
1523
+ PROTECTED_APPROVAL_TTL_MS,
1524
+ DEFAULT_PROTECTED_FILE_GLOBS,
794
1525
  };
795
1526
 
796
1527
  // ---------------------------------------------------------------------------