@loopover/engine 1.0.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 (272) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +661 -0
  3. package/README.md +676 -0
  4. package/dist/advisory/gate-advisory.d.ts +151 -0
  5. package/dist/advisory/gate-advisory.js +512 -0
  6. package/dist/ai-policy-map.d.ts +97 -0
  7. package/dist/ai-policy-map.js +377 -0
  8. package/dist/ams-policy-spec.d.ts +56 -0
  9. package/dist/ams-policy-spec.js +165 -0
  10. package/dist/calibration-dashboard.d.ts +23 -0
  11. package/dist/calibration-dashboard.js +65 -0
  12. package/dist/calibration-trend.d.ts +40 -0
  13. package/dist/calibration-trend.js +60 -0
  14. package/dist/contributor-fit.d.ts +24 -0
  15. package/dist/contributor-fit.js +50 -0
  16. package/dist/discovery-index-contract.d.ts +74 -0
  17. package/dist/discovery-index-contract.js +184 -0
  18. package/dist/discovery-soft-claim.d.ts +35 -0
  19. package/dist/discovery-soft-claim.js +50 -0
  20. package/dist/duplicate-winner.d.ts +57 -0
  21. package/dist/duplicate-winner.js +98 -0
  22. package/dist/feasibility.d.ts +27 -0
  23. package/dist/feasibility.js +86 -0
  24. package/dist/finding-severity-calibration.d.ts +116 -0
  25. package/dist/finding-severity-calibration.js +457 -0
  26. package/dist/fleet-run-manifest.d.ts +41 -0
  27. package/dist/fleet-run-manifest.js +151 -0
  28. package/dist/focus-manifest/guidance.d.ts +21 -0
  29. package/dist/focus-manifest/guidance.js +222 -0
  30. package/dist/focus-manifest.d.ts +826 -0
  31. package/dist/focus-manifest.js +2730 -0
  32. package/dist/gate-verdict-calibration.d.ts +111 -0
  33. package/dist/gate-verdict-calibration.js +411 -0
  34. package/dist/github/constants.d.ts +1 -0
  35. package/dist/github/constants.js +1 -0
  36. package/dist/github/sanitize-public-comment.d.ts +1 -0
  37. package/dist/github/sanitize-public-comment.js +26 -0
  38. package/dist/goal-model.d.ts +7 -0
  39. package/dist/goal-model.js +87 -0
  40. package/dist/governor/action-mode.d.ts +55 -0
  41. package/dist/governor/action-mode.js +74 -0
  42. package/dist/governor/budget-cap.d.ts +49 -0
  43. package/dist/governor/budget-cap.js +34 -0
  44. package/dist/governor/chokepoint.d.ts +58 -0
  45. package/dist/governor/chokepoint.js +269 -0
  46. package/dist/governor/kill-switch.d.ts +38 -0
  47. package/dist/governor/kill-switch.js +62 -0
  48. package/dist/governor/rate-limit.d.ts +40 -0
  49. package/dist/governor/rate-limit.js +63 -0
  50. package/dist/governor/reputation-throttle.d.ts +50 -0
  51. package/dist/governor/reputation-throttle.js +94 -0
  52. package/dist/governor/run-halt.d.ts +39 -0
  53. package/dist/governor/run-halt.js +83 -0
  54. package/dist/governor/self-plagiarism.d.ts +47 -0
  55. package/dist/governor/self-plagiarism.js +200 -0
  56. package/dist/governor/write-rate-limit.d.ts +52 -0
  57. package/dist/governor/write-rate-limit.js +132 -0
  58. package/dist/governor-ledger.d.ts +25 -0
  59. package/dist/governor-ledger.js +91 -0
  60. package/dist/index.d.ts +99 -0
  61. package/dist/index.js +120 -0
  62. package/dist/issue-plan-decomposition.d.ts +18 -0
  63. package/dist/issue-plan-decomposition.js +56 -0
  64. package/dist/issue-rag-query.d.ts +12 -0
  65. package/dist/issue-rag-query.js +35 -0
  66. package/dist/local-scorer.d.ts +34 -0
  67. package/dist/local-scorer.js +36 -0
  68. package/dist/metadata-best-min-score.d.ts +9 -0
  69. package/dist/metadata-best-min-score.js +9 -0
  70. package/dist/metadata-best-pick.d.ts +9 -0
  71. package/dist/metadata-best-pick.js +9 -0
  72. package/dist/metadata-min-score.d.ts +9 -0
  73. package/dist/metadata-min-score.js +13 -0
  74. package/dist/metadata-top-min-score.d.ts +9 -0
  75. package/dist/metadata-top-min-score.js +13 -0
  76. package/dist/metadata-top-pick.d.ts +9 -0
  77. package/dist/metadata-top-pick.js +13 -0
  78. package/dist/miner/acceptance-criteria.d.ts +43 -0
  79. package/dist/miner/acceptance-criteria.js +77 -0
  80. package/dist/miner/agent-sdk-driver.d.ts +39 -0
  81. package/dist/miner/agent-sdk-driver.js +162 -0
  82. package/dist/miner/attempt-log.d.ts +29 -0
  83. package/dist/miner/attempt-log.js +100 -0
  84. package/dist/miner/attempt-metering.d.ts +39 -0
  85. package/dist/miner/attempt-metering.js +67 -0
  86. package/dist/miner/cli-subprocess-driver.d.ts +68 -0
  87. package/dist/miner/cli-subprocess-driver.js +272 -0
  88. package/dist/miner/coding-agent-driver.d.ts +33 -0
  89. package/dist/miner/coding-agent-driver.js +36 -0
  90. package/dist/miner/coding-agent-invoke.d.ts +11 -0
  91. package/dist/miner/coding-agent-invoke.js +80 -0
  92. package/dist/miner/coding-agent-mode.d.ts +25 -0
  93. package/dist/miner/coding-agent-mode.js +43 -0
  94. package/dist/miner/driver-factory.d.ts +82 -0
  95. package/dist/miner/driver-factory.js +169 -0
  96. package/dist/miner/harness-submission-trigger.d.ts +37 -0
  97. package/dist/miner/harness-submission-trigger.js +46 -0
  98. package/dist/miner/iterate-loop.d.ts +97 -0
  99. package/dist/miner/iterate-loop.js +278 -0
  100. package/dist/miner/iterate-policy.d.ts +98 -0
  101. package/dist/miner/iterate-policy.js +98 -0
  102. package/dist/miner/lint-guard.d.ts +50 -0
  103. package/dist/miner/lint-guard.js +68 -0
  104. package/dist/miner/local-write-tools.d.ts +81 -0
  105. package/dist/miner/local-write-tools.js +113 -0
  106. package/dist/miner/loop-reentry-policy.d.ts +36 -0
  107. package/dist/miner/loop-reentry-policy.js +53 -0
  108. package/dist/miner/repo-map.d.ts +51 -0
  109. package/dist/miner/repo-map.js +268 -0
  110. package/dist/miner/self-review-adapter.d.ts +106 -0
  111. package/dist/miner/self-review-adapter.js +83 -0
  112. package/dist/miner/submission-gate.d.ts +40 -0
  113. package/dist/miner/submission-gate.js +89 -0
  114. package/dist/miner/worktree-allocator.d.ts +59 -0
  115. package/dist/miner/worktree-allocator.js +60 -0
  116. package/dist/miner/worktree-pool.d.ts +55 -0
  117. package/dist/miner/worktree-pool.js +65 -0
  118. package/dist/miner-goal-lane-fit.d.ts +18 -0
  119. package/dist/miner-goal-lane-fit.js +76 -0
  120. package/dist/miner-goal-spec.d.ts +139 -0
  121. package/dist/miner-goal-spec.js +281 -0
  122. package/dist/miner-prediction-metrics.d.ts +16 -0
  123. package/dist/miner-prediction-metrics.js +61 -0
  124. package/dist/miner-telemetry.d.ts +32 -0
  125. package/dist/miner-telemetry.js +87 -0
  126. package/dist/objective-anchor.d.ts +125 -0
  127. package/dist/objective-anchor.js +401 -0
  128. package/dist/opportunity-competition.d.ts +6 -0
  129. package/dist/opportunity-competition.js +29 -0
  130. package/dist/opportunity-freshness.d.ts +18 -0
  131. package/dist/opportunity-freshness.js +55 -0
  132. package/dist/opportunity-metadata.d.ts +53 -0
  133. package/dist/opportunity-metadata.js +204 -0
  134. package/dist/opportunity-ranker.d.ts +46 -0
  135. package/dist/opportunity-ranker.js +74 -0
  136. package/dist/pairwise-calibration.d.ts +49 -0
  137. package/dist/pairwise-calibration.js +106 -0
  138. package/dist/phase7-calibration-loop.d.ts +136 -0
  139. package/dist/phase7-calibration-loop.js +437 -0
  140. package/dist/plan-blocked.d.ts +6 -0
  141. package/dist/plan-blocked.js +24 -0
  142. package/dist/plan-completed.d.ts +5 -0
  143. package/dist/plan-completed.js +6 -0
  144. package/dist/plan-completion.d.ts +6 -0
  145. package/dist/plan-completion.js +7 -0
  146. package/dist/plan-empty.d.ts +5 -0
  147. package/dist/plan-empty.js +6 -0
  148. package/dist/plan-export.d.ts +27 -0
  149. package/dist/plan-export.js +86 -0
  150. package/dist/plan-failure.d.ts +5 -0
  151. package/dist/plan-failure.js +6 -0
  152. package/dist/plan-overall-status.d.ts +6 -0
  153. package/dist/plan-overall-status.js +25 -0
  154. package/dist/plan-pending.d.ts +5 -0
  155. package/dist/plan-pending.js +6 -0
  156. package/dist/plan-progress-complete.d.ts +7 -0
  157. package/dist/plan-progress-complete.js +10 -0
  158. package/dist/plan-ready.d.ts +6 -0
  159. package/dist/plan-ready.js +12 -0
  160. package/dist/plan-running.d.ts +5 -0
  161. package/dist/plan-running.js +6 -0
  162. package/dist/plan-skipped.d.ts +5 -0
  163. package/dist/plan-skipped.js +6 -0
  164. package/dist/plan-step-count.d.ts +5 -0
  165. package/dist/plan-step-count.js +6 -0
  166. package/dist/plan-step-stats.d.ts +5 -0
  167. package/dist/plan-step-stats.js +6 -0
  168. package/dist/plan-templates.d.ts +25 -0
  169. package/dist/plan-templates.js +107 -0
  170. package/dist/plan-terminated.d.ts +6 -0
  171. package/dist/plan-terminated.js +9 -0
  172. package/dist/portfolio/non-convergence.d.ts +37 -0
  173. package/dist/portfolio/non-convergence.js +57 -0
  174. package/dist/portfolio/queue.d.ts +30 -0
  175. package/dist/portfolio/queue.js +150 -0
  176. package/dist/predicted-gate.d.ts +103 -0
  177. package/dist/predicted-gate.js +218 -0
  178. package/dist/prompt-packet.d.ts +11 -0
  179. package/dist/prompt-packet.js +34 -0
  180. package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
  181. package/dist/ranked-opportunity-best-min-score.js +9 -0
  182. package/dist/ranked-opportunity-best-pick.d.ts +8 -0
  183. package/dist/ranked-opportunity-best-pick.js +9 -0
  184. package/dist/ranked-opportunity-min-score.d.ts +8 -0
  185. package/dist/ranked-opportunity-min-score.js +13 -0
  186. package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
  187. package/dist/ranked-opportunity-top-min-score.js +13 -0
  188. package/dist/review/advisory-ai-routing-config.d.ts +8 -0
  189. package/dist/review/advisory-ai-routing-config.js +40 -0
  190. package/dist/review/check-names.d.ts +1 -0
  191. package/dist/review/check-names.js +1 -0
  192. package/dist/review/cla-check.d.ts +49 -0
  193. package/dist/review/cla-check.js +71 -0
  194. package/dist/review/diff-file-priority.d.ts +1 -0
  195. package/dist/review/diff-file-priority.js +12 -0
  196. package/dist/review/enrichment-analyzer-names.d.ts +3 -0
  197. package/dist/review/enrichment-analyzer-names.js +63 -0
  198. package/dist/review/guardrail-config.d.ts +10 -0
  199. package/dist/review/guardrail-config.js +62 -0
  200. package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
  201. package/dist/review/linked-issue-hard-rules-config.js +79 -0
  202. package/dist/review/linked-issue-label-propagation.d.ts +9 -0
  203. package/dist/review/linked-issue-label-propagation.js +114 -0
  204. package/dist/review/pre-merge-checks.d.ts +28 -0
  205. package/dist/review/pre-merge-checks.js +66 -0
  206. package/dist/review/review-thread-findings.d.ts +1 -0
  207. package/dist/review/review-thread-findings.js +1 -0
  208. package/dist/review/safe-url.d.ts +5 -0
  209. package/dist/review/safe-url.js +137 -0
  210. package/dist/review/screenshot-table-gate.d.ts +87 -0
  211. package/dist/review/screenshot-table-gate.js +340 -0
  212. package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
  213. package/dist/review/unlinked-issue-guardrail-config.js +44 -0
  214. package/dist/reviewer-consensus-calibration.d.ts +113 -0
  215. package/dist/reviewer-consensus-calibration.js +493 -0
  216. package/dist/reward-risk.d.ts +223 -0
  217. package/dist/reward-risk.js +618 -0
  218. package/dist/scoring/label-match.d.ts +4 -0
  219. package/dist/scoring/label-match.js +145 -0
  220. package/dist/scoring/model.d.ts +31 -0
  221. package/dist/scoring/model.js +157 -0
  222. package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
  223. package/dist/scoring/pending-pr-scenarios.js +145 -0
  224. package/dist/scoring/preview.d.ts +246 -0
  225. package/dist/scoring/preview.js +1108 -0
  226. package/dist/scoring/types.d.ts +131 -0
  227. package/dist/scoring/types.js +7 -0
  228. package/dist/settings/auto-close-exempt.d.ts +10 -0
  229. package/dist/settings/auto-close-exempt.js +58 -0
  230. package/dist/settings/autonomy.d.ts +33 -0
  231. package/dist/settings/autonomy.js +80 -0
  232. package/dist/settings/command-authorization.d.ts +49 -0
  233. package/dist/settings/command-authorization.js +222 -0
  234. package/dist/settings/contributor-blacklist.d.ts +17 -0
  235. package/dist/settings/contributor-blacklist.js +86 -0
  236. package/dist/settings/moderation-rules.d.ts +51 -0
  237. package/dist/settings/moderation-rules.js +104 -0
  238. package/dist/settings/pr-type-label.d.ts +70 -0
  239. package/dist/settings/pr-type-label.js +149 -0
  240. package/dist/signals/change-guardrail.d.ts +49 -0
  241. package/dist/signals/change-guardrail.js +164 -0
  242. package/dist/signals/check-summary.d.ts +9 -0
  243. package/dist/signals/check-summary.js +12 -0
  244. package/dist/signals/contributor-calibration.d.ts +23 -0
  245. package/dist/signals/contributor-calibration.js +47 -0
  246. package/dist/signals/duplicate-winner.d.ts +9 -0
  247. package/dist/signals/duplicate-winner.js +9 -0
  248. package/dist/signals/path-matchers.d.ts +36 -0
  249. package/dist/signals/path-matchers.js +371 -0
  250. package/dist/signals/predicted-gate-engine.d.ts +62 -0
  251. package/dist/signals/predicted-gate-engine.js +895 -0
  252. package/dist/signals/preflight-limits.d.ts +14 -0
  253. package/dist/signals/preflight-limits.js +14 -0
  254. package/dist/signals/slop.d.ts +56 -0
  255. package/dist/signals/slop.js +324 -0
  256. package/dist/signals/test-evidence.d.ts +25 -0
  257. package/dist/signals/test-evidence.js +108 -0
  258. package/dist/subprocess-env.d.ts +23 -0
  259. package/dist/subprocess-env.js +74 -0
  260. package/dist/track-record-summary.d.ts +92 -0
  261. package/dist/track-record-summary.js +301 -0
  262. package/dist/types/manifest-deps-types.d.ts +514 -0
  263. package/dist/types/manifest-deps-types.js +4 -0
  264. package/dist/types/predicted-gate-types.d.ts +338 -0
  265. package/dist/types/predicted-gate-types.js +3 -0
  266. package/dist/types/reward-risk-types.d.ts +319 -0
  267. package/dist/types/reward-risk-types.js +11 -0
  268. package/dist/utils/json.d.ts +1 -0
  269. package/dist/utils/json.js +3 -0
  270. package/dist/version.d.ts +2 -0
  271. package/dist/version.js +3 -0
  272. package/package.json +80 -0
@@ -0,0 +1,55 @@
1
+ import type { GovernorLedgerEvent } from "../governor-ledger.js";
2
+ import { type MinerKillSwitchScope } from "./kill-switch.js";
3
+ /** Whether the miner actually executes a write, only shadow-logs what it WOULD do, or is halted entirely. */
4
+ export type MinerActionMode = "paused" | "dry_run" | "live";
5
+ /**
6
+ * The ONLY value that opts a miner into LIVE write execution. Deliberately a specific string literal, not a
7
+ * boolean -- a fat-fingered `liveModeOptIn: true`, `"yes"`, `"on"`, or `GITTENSORY_MINER_LIVE_MODE=1` must never
8
+ * accidentally unlock writes the way a truthy-coerced flag could. Per the issue's explicit requirement: "not a
9
+ * generic boolean flag that could be accidentally true."
10
+ */
11
+ export declare const MINER_LIVE_MODE_OPT_IN = "live";
12
+ /** Env var an operator sets (to exactly {@link MINER_LIVE_MODE_OPT_IN}) to opt their own miner instance into
13
+ * live write execution. Repo-side opt-in alone is never enough to execute writes. */
14
+ export declare const MINER_LIVE_MODE_ENV_VAR = "GITTENSORY_MINER_LIVE_MODE";
15
+ /** True only when `value` is EXACTLY the {@link MINER_LIVE_MODE_OPT_IN} string -- no truthy coercion, no case
16
+ * folding, no alternate spellings. Everything else (including `true`, `"Live"`, `"1"`) reads as not opted in. */
17
+ export declare function isExplicitMinerLiveModeOptIn(value: unknown): boolean;
18
+ /** True when the operator's global env-level live-mode opt-in is set to exactly {@link MINER_LIVE_MODE_OPT_IN}. */
19
+ export declare function isGlobalMinerLiveModeOptIn(env: Record<string, string | undefined>): boolean;
20
+ /**
21
+ * Resolve the miner's overall action mode. Precedence (safest wins, mirroring `resolveAgentActionMode`):
22
+ * 1. Kill-switch active (either scope, #2341) -> `"paused"` -- always wins, regardless of any live-mode opt-in.
23
+ * 2. BOTH the operator's global env config AND the target repo's own `.gittensory-miner.yml`
24
+ * (`MinerGoalSpec.execution.liveModeOptIn`) explicitly opt in -> `"live"`. The repo field is a repo-side
25
+ * allowance, not an operator-authored authorization to execute writes under the miner's credentials.
26
+ * 3. Otherwise -> `"dry_run"`. No config anywhere, either side omitted, or a malformed/partial config that fails
27
+ * to normalize to the exact opt-in literal, all fall through to this branch -- absence or ambiguity always
28
+ * means dry-run.
29
+ *
30
+ * A target repo that wants to guarantee it never receives live automated writes -- even from an operator whose
31
+ * own miner instance is globally live -- can omit its repo opt-in or set its OWN kill-switch (`killSwitch.paused:
32
+ * true`, #2341), which takes precedence over any live-mode opt-in per step 1 above.
33
+ */
34
+ export declare function resolveMinerActionMode(input: {
35
+ killSwitchScope: MinerKillSwitchScope;
36
+ repoLiveModeOptIn?: unknown;
37
+ globalLiveModeOptIn: boolean;
38
+ }): MinerActionMode;
39
+ /** True only for `"live"` -- the only mode that performs a real write. `"paused"` does nothing; `"dry_run"`
40
+ * records a shadow action but never mutates. */
41
+ export declare function minerActionModeExecutes(mode: MinerActionMode): boolean;
42
+ /**
43
+ * Governor-ledger row for a dry-run SHADOW action (#2342's "logs the WOULD-BE action... without ever invoking
44
+ * the actual command" deliverable). `eventType` stays within the existing closed vocabulary (`"allowed"` -- the
45
+ * Governor's other checks did not deny this action, dry-run mode is simply choosing to shadow-log instead of
46
+ * execute); `decision: "dry_run"` is the distinct marker this deliverable calls for. `wouldBeAction` is left as
47
+ * a generic record (not the concrete `LocalWriteActionSpec` type) so this package stays decoupled from the
48
+ * main app's `src/mcp/local-write-tools.ts` -- the caller wiring a real action spec into this call owns that
49
+ * shape.
50
+ */
51
+ export declare function buildMinerDryRunGovernorLedgerEvent(input: {
52
+ repoFullName?: string | null | undefined;
53
+ actionClass: string;
54
+ wouldBeAction: Record<string, unknown>;
55
+ }): GovernorLedgerEvent;
@@ -0,0 +1,74 @@
1
+ // Governor dry-run-by-default enforcement (#2342): resolves the miner's overall action mode -- "safest wins"
2
+ // precedence mirroring `resolveAgentActionMode` (`src/settings/agent-execution.ts`): paused > dry_run > live.
3
+ // A freshly-configured miner (no opt-in present anywhere) MUST default to dry_run, never live -- this is the
4
+ // deny-by-default floor `src/settings/autonomy.ts`'s `DEFAULT_AUTONOMY_LEVEL = "observe"` establishes for the
5
+ // review-stack, extended here to the miner's own runtime.
6
+ //
7
+ // DETECTOR ONLY -- no IO, no persistence. Consulting this alongside the other pure calculators (rate-limit,
8
+ // budget caps, reputation, self-plagiarism, non-convergence) and recording every CHECK is the Governor
9
+ // chokepoint's job (#2340), which consults this module (after the kill-switch) in its precedence ladder.
10
+ import { isMinerKillSwitchActive } from "./kill-switch.js";
11
+ /**
12
+ * The ONLY value that opts a miner into LIVE write execution. Deliberately a specific string literal, not a
13
+ * boolean -- a fat-fingered `liveModeOptIn: true`, `"yes"`, `"on"`, or `GITTENSORY_MINER_LIVE_MODE=1` must never
14
+ * accidentally unlock writes the way a truthy-coerced flag could. Per the issue's explicit requirement: "not a
15
+ * generic boolean flag that could be accidentally true."
16
+ */
17
+ export const MINER_LIVE_MODE_OPT_IN = "live";
18
+ /** Env var an operator sets (to exactly {@link MINER_LIVE_MODE_OPT_IN}) to opt their own miner instance into
19
+ * live write execution. Repo-side opt-in alone is never enough to execute writes. */
20
+ export const MINER_LIVE_MODE_ENV_VAR = "GITTENSORY_MINER_LIVE_MODE";
21
+ /** True only when `value` is EXACTLY the {@link MINER_LIVE_MODE_OPT_IN} string -- no truthy coercion, no case
22
+ * folding, no alternate spellings. Everything else (including `true`, `"Live"`, `"1"`) reads as not opted in. */
23
+ export function isExplicitMinerLiveModeOptIn(value) {
24
+ return value === MINER_LIVE_MODE_OPT_IN;
25
+ }
26
+ /** True when the operator's global env-level live-mode opt-in is set to exactly {@link MINER_LIVE_MODE_OPT_IN}. */
27
+ export function isGlobalMinerLiveModeOptIn(env) {
28
+ return env[MINER_LIVE_MODE_ENV_VAR] === MINER_LIVE_MODE_OPT_IN;
29
+ }
30
+ /**
31
+ * Resolve the miner's overall action mode. Precedence (safest wins, mirroring `resolveAgentActionMode`):
32
+ * 1. Kill-switch active (either scope, #2341) -> `"paused"` -- always wins, regardless of any live-mode opt-in.
33
+ * 2. BOTH the operator's global env config AND the target repo's own `.gittensory-miner.yml`
34
+ * (`MinerGoalSpec.execution.liveModeOptIn`) explicitly opt in -> `"live"`. The repo field is a repo-side
35
+ * allowance, not an operator-authored authorization to execute writes under the miner's credentials.
36
+ * 3. Otherwise -> `"dry_run"`. No config anywhere, either side omitted, or a malformed/partial config that fails
37
+ * to normalize to the exact opt-in literal, all fall through to this branch -- absence or ambiguity always
38
+ * means dry-run.
39
+ *
40
+ * A target repo that wants to guarantee it never receives live automated writes -- even from an operator whose
41
+ * own miner instance is globally live -- can omit its repo opt-in or set its OWN kill-switch (`killSwitch.paused:
42
+ * true`, #2341), which takes precedence over any live-mode opt-in per step 1 above.
43
+ */
44
+ export function resolveMinerActionMode(input) {
45
+ if (isMinerKillSwitchActive(input.killSwitchScope))
46
+ return "paused";
47
+ if (input.globalLiveModeOptIn && isExplicitMinerLiveModeOptIn(input.repoLiveModeOptIn))
48
+ return "live";
49
+ return "dry_run";
50
+ }
51
+ /** True only for `"live"` -- the only mode that performs a real write. `"paused"` does nothing; `"dry_run"`
52
+ * records a shadow action but never mutates. */
53
+ export function minerActionModeExecutes(mode) {
54
+ return mode === "live";
55
+ }
56
+ /**
57
+ * Governor-ledger row for a dry-run SHADOW action (#2342's "logs the WOULD-BE action... without ever invoking
58
+ * the actual command" deliverable). `eventType` stays within the existing closed vocabulary (`"allowed"` -- the
59
+ * Governor's other checks did not deny this action, dry-run mode is simply choosing to shadow-log instead of
60
+ * execute); `decision: "dry_run"` is the distinct marker this deliverable calls for. `wouldBeAction` is left as
61
+ * a generic record (not the concrete `LocalWriteActionSpec` type) so this package stays decoupled from the
62
+ * main app's `src/mcp/local-write-tools.ts` -- the caller wiring a real action spec into this call owns that
63
+ * shape.
64
+ */
65
+ export function buildMinerDryRunGovernorLedgerEvent(input) {
66
+ return {
67
+ eventType: "allowed",
68
+ repoFullName: input.repoFullName ?? null,
69
+ actionClass: input.actionClass,
70
+ decision: "dry_run",
71
+ reason: "dry_run_mode_active",
72
+ payload: { wouldBeAction: input.wouldBeAction },
73
+ };
74
+ }
@@ -0,0 +1,49 @@
1
+ import type { GovernorLedgerEventType } from "../governor-ledger.js";
2
+ /** The three independent ceilings for a whole run. A dimension with a ceiling of 0 permits no usage at all
3
+ * (any usage reaches it), mirroring rate-limit.ts treating `limit: 0` as "nothing allowed". */
4
+ export type GovernorCapLimits = {
5
+ /** Maximum cumulative budget/cost units permitted for the run (may be fractional, e.g. a dollar cost). */
6
+ budget: number;
7
+ /** Maximum cumulative turns/iterations permitted for the run (whole counts). */
8
+ turns: number;
9
+ /** Termination ceiling: maximum elapsed session time in milliseconds. */
10
+ elapsedMs: number;
11
+ };
12
+ /** A run's cumulative usage so far. Caller-supplied — this module never reads a clock or a meter itself. */
13
+ export type GovernorCapUsage = {
14
+ /** Budget/cost already spent this run. */
15
+ budgetSpent: number;
16
+ /** Turns/iterations already taken this run. */
17
+ turnsTaken: number;
18
+ /** Elapsed session time so far in milliseconds. */
19
+ elapsedMs: number;
20
+ };
21
+ /** One dimension's evaluation. `remaining` is headroom before the ceiling and is never negative. */
22
+ export type GovernorCapDimension = {
23
+ /** The normalized ceiling for this dimension. */
24
+ limit: number;
25
+ /** The normalized usage measured against that ceiling. */
26
+ used: number;
27
+ /** Headroom left before the ceiling (0 once reached; never negative). */
28
+ remaining: number;
29
+ /** True once usage has reached OR passed the ceiling. */
30
+ exceeded: boolean;
31
+ };
32
+ /** The combined report. `verdict` is drawn from GOVERNOR_LEDGER_EVENT_TYPES (not a parallel vocabulary) so it
33
+ * aligns with the events the governor-ledger records: `allowed` (all caps clear), `denied` (a budget/turn cap
34
+ * reached), or `kill_switch` (the termination ceiling reached — a hard wall-clock stop). */
35
+ export type GovernorCapReport = {
36
+ verdict: GovernorLedgerEventType;
37
+ budget: GovernorCapDimension;
38
+ turns: GovernorCapDimension;
39
+ termination: GovernorCapDimension;
40
+ };
41
+ /**
42
+ * Evaluate a run's cumulative usage against its budget/turn/termination ceilings. Pure: it reads the two typed
43
+ * inputs and returns a report without mutating anything or reading a clock. Each dimension is normalized and
44
+ * evaluated independently, then combined into one verdict — termination (a hard wall-clock ceiling) is the most
45
+ * severe (`kill_switch`), a reached budget or turn ceiling is `denied`, and everything clear is `allowed`.
46
+ * Every numeric input is normalized first, so a non-finite, negative, or fractional value can never produce a
47
+ * NaN verdict or a negative remaining-budget/turns value.
48
+ */
49
+ export declare function evaluateGovernorCaps(usage: GovernorCapUsage, limits: GovernorCapLimits): GovernorCapReport;
@@ -0,0 +1,34 @@
1
+ // Normalize any numeric input to a finite, non-negative value (a non-finite or negative value becomes 0), so no
2
+ // input can make a verdict NaN or a remaining value negative. Mirrors rate-limit.ts's finiteNonNegativeInt but
3
+ // keeps fractional precision for continuous dimensions (budget cost, elapsed milliseconds).
4
+ function finiteNonNegative(value) {
5
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
6
+ }
7
+ // Integer variant for the turn-count dimension (turns are whole iterations), matching rate-limit.ts exactly.
8
+ function finiteNonNegativeInt(value) {
9
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
10
+ }
11
+ // Evaluate one dimension: usage reaching or passing the ceiling is `exceeded`, and headroom is clamped at 0 so
12
+ // it can never go negative. Both operands are already normalized by the caller.
13
+ function evaluateDimension(used, limit) {
14
+ return { limit, used, remaining: Math.max(0, limit - used), exceeded: used >= limit };
15
+ }
16
+ /**
17
+ * Evaluate a run's cumulative usage against its budget/turn/termination ceilings. Pure: it reads the two typed
18
+ * inputs and returns a report without mutating anything or reading a clock. Each dimension is normalized and
19
+ * evaluated independently, then combined into one verdict — termination (a hard wall-clock ceiling) is the most
20
+ * severe (`kill_switch`), a reached budget or turn ceiling is `denied`, and everything clear is `allowed`.
21
+ * Every numeric input is normalized first, so a non-finite, negative, or fractional value can never produce a
22
+ * NaN verdict or a negative remaining-budget/turns value.
23
+ */
24
+ export function evaluateGovernorCaps(usage, limits) {
25
+ const budget = evaluateDimension(finiteNonNegative(usage.budgetSpent), finiteNonNegative(limits.budget));
26
+ const turns = evaluateDimension(finiteNonNegativeInt(usage.turnsTaken), finiteNonNegativeInt(limits.turns));
27
+ const termination = evaluateDimension(finiteNonNegative(usage.elapsedMs), finiteNonNegative(limits.elapsedMs));
28
+ const verdict = termination.exceeded
29
+ ? "kill_switch"
30
+ : budget.exceeded || turns.exceeded
31
+ ? "denied"
32
+ : "allowed";
33
+ return { verdict, budget, turns, termination };
34
+ }
@@ -0,0 +1,58 @@
1
+ import type { GovernorLedgerEvent } from "../governor-ledger.js";
2
+ import type { PortfolioConvergenceInput, PortfolioConvergenceThresholds, PortfolioConvergenceVerdict } from "../portfolio/non-convergence.js";
3
+ import { type MinerActionMode } from "./action-mode.js";
4
+ import type { GovernorCapLimits, GovernorCapReport, GovernorCapUsage } from "./budget-cap.js";
5
+ import { type MinerKillSwitchScope } from "./kill-switch.js";
6
+ import type { RepoOutcomeHistory, SelfReputationThresholds, SelfReputationThrottleDecision } from "./reputation-throttle.js";
7
+ import type { OwnSubmissionRecord, SelfPlagiarismCandidate, SelfPlagiarismConfig, SelfPlagiarismVerdict } from "./self-plagiarism.js";
8
+ import type { WriteRateLimitBackoffStore, WriteRateLimitBucketStore, WriteRateLimitPolicies, WriteRateLimitVerdict } from "./write-rate-limit.js";
9
+ /** Which stage of the precedence ladder produced the final verdict. */
10
+ export type GovernorDecisionStage = "kill_switch" | "dry_run" | "rate_limit" | "budget_cap" | "non_convergence" | "reputation_throttle" | "self_plagiarism" | "allow" | "internal_error";
11
+ export type GovernorChokepointInput = {
12
+ actionClass: string;
13
+ repoFullName: string;
14
+ nowMs: number;
15
+ /** Full would-be action spec, logged verbatim on a dry-run shadow (#2342) or a final denial's audit payload. */
16
+ wouldBeAction: Record<string, unknown>;
17
+ killSwitchGlobal: boolean;
18
+ killSwitchRepoPaused?: boolean | null | undefined;
19
+ liveModeGlobalOptIn: boolean;
20
+ liveModeRepoOptIn?: unknown;
21
+ rateLimitBuckets: WriteRateLimitBucketStore;
22
+ rateLimitBackoffAttempts: WriteRateLimitBackoffStore;
23
+ rateLimitPolicies?: WriteRateLimitPolicies | undefined;
24
+ rateLimitRandomFn?: (() => number) | undefined;
25
+ capUsage: GovernorCapUsage;
26
+ capLimits: GovernorCapLimits;
27
+ convergenceInput: PortfolioConvergenceInput;
28
+ convergenceThresholds?: PortfolioConvergenceThresholds | undefined;
29
+ reputationHistory?: RepoOutcomeHistory | undefined;
30
+ reputationThresholds?: SelfReputationThresholds | undefined;
31
+ selfPlagiarismCandidate?: SelfPlagiarismCandidate | undefined;
32
+ selfPlagiarismRecentSubmissions?: readonly OwnSubmissionRecord[] | undefined;
33
+ selfPlagiarismConfig?: SelfPlagiarismConfig | undefined;
34
+ };
35
+ export type GovernorDecisionDetail = {
36
+ killSwitchScope: MinerKillSwitchScope;
37
+ mode: MinerActionMode;
38
+ rateLimit?: WriteRateLimitVerdict;
39
+ budgetCap?: GovernorCapReport;
40
+ convergence?: PortfolioConvergenceVerdict;
41
+ reputation?: SelfReputationThrottleDecision;
42
+ selfPlagiarism?: SelfPlagiarismVerdict;
43
+ };
44
+ export type GovernorDecision = {
45
+ /** True only when every consulted stage allowed AND the resolved mode is `"live"`. */
46
+ allowed: boolean;
47
+ mode: MinerActionMode;
48
+ stage: GovernorDecisionStage;
49
+ reason: string;
50
+ detail: GovernorDecisionDetail;
51
+ /** The single row to append to the governor ledger for this chokepoint invocation. */
52
+ ledgerEvent: GovernorLedgerEvent;
53
+ };
54
+ /**
55
+ * Evaluate every write action against the full precedence ladder and return one fail-closed verdict. See the
56
+ * module doc comment for the exact stage order and which stages are conditional on `actionClass`.
57
+ */
58
+ export declare function evaluateGovernorChokepoint(input: GovernorChokepointInput): GovernorDecision;
@@ -0,0 +1,269 @@
1
+ // The Governor chokepoint (#2340): the single fail-closed decision point every miner write action MUST pass
2
+ // through before executing a `LocalWriteActionSpec` (`src/mcp/local-write-tools.ts`: open_pr, file_issue,
3
+ // apply_labels, post_eligibility_comment, create_branch, delete_branch, generate_tests). This composes the
4
+ // previously-built pure calculators into one verdict -- it is the reason Phase 5 exists.
5
+ //
6
+ // PRECEDENCE ("safest wins", mirroring `resolveAgentActionMode` in `src/settings/agent-execution.ts`):
7
+ // global kill-switch > per-repo pause > dry-run > rate-limit > budget/turn/termination cap > non-convergence
8
+ // > self-reputation throttle > self-plagiarism > allow.
9
+ // The issue's own deliverable names rate-limit, budget caps, and non-convergence explicitly. This module also
10
+ // composes self-reputation-throttle and self-plagiarism, per those two calculators' OWN doc comments
11
+ // (`reputation-throttle.ts`: "the chokepoint can record WHY a submission cadence was scaled"; `self-plagiarism.ts`:
12
+ // "the Governor open_pr chokepoint (#2340) composes this verdict with rate-limit, budget caps, and
13
+ // non-convergence") -- both already ship a `*LedgerEvent` builder keyed on their own boolean
14
+ // throttled/allowed field, so composing them here reuses an existing, already-reviewed gate semantic rather
15
+ // than inventing a new one. Both are evaluated only for `actionClass === "open_pr"` (their own ledger builders
16
+ // hardcode/scope to PR submissions; a label-apply or branch-delete has no diff fingerprint or "submission
17
+ // cadence" to throttle).
18
+ //
19
+ // FAIL CLOSED: any stage that throws (malformed caller input escaping this module's typed boundary) denies
20
+ // immediately with `stage: "internal_error"`, never falls through to `allow`.
21
+ //
22
+ // PURE: no IO, no bucket/ledger persistence. This returns a verdict only; the miner-lib wrapper
23
+ // (`packages/gittensory-miner/lib/governor-chokepoint.js`) owns mutating rate-limit buckets and appending the
24
+ // returned ledger event, mirroring the existing engine-pure/miner-lib-stateful split every sibling module uses.
25
+ import { classifyPortfolioConvergence, DEFAULT_PORTFOLIO_CONVERGENCE_THRESHOLDS } from "../portfolio/non-convergence.js";
26
+ import { minerActionModeExecutes, resolveMinerActionMode } from "./action-mode.js";
27
+ import { evaluateGovernorCaps } from "./budget-cap.js";
28
+ import { isMinerKillSwitchActive, resolveMinerKillSwitch } from "./kill-switch.js";
29
+ import { DEFAULT_SELF_REPUTATION_THRESHOLDS, selfReputationThrottle } from "./reputation-throttle.js";
30
+ import { DEFAULT_SELF_PLAGIARISM_CONFIG, selfPlagiarismCheck } from "./self-plagiarism.js";
31
+ import { evaluateWriteRateLimit } from "./write-rate-limit.js";
32
+ /** Action classes that carry a per-submission diff fingerprint / outcome-cadence concept. Reputation-throttle
33
+ * and self-plagiarism are evaluated only for these -- a label-apply or branch-delete has neither. */
34
+ const SELF_SUBMISSION_ACTION_CLASSES = new Set(["open_pr"]);
35
+ function denyResult(input) {
36
+ return {
37
+ allowed: false,
38
+ mode: input.mode,
39
+ stage: input.stage,
40
+ reason: input.reason,
41
+ detail: input.detail,
42
+ ledgerEvent: {
43
+ eventType: input.eventType,
44
+ repoFullName: input.repoFullName,
45
+ actionClass: input.actionClass,
46
+ decision: input.stage === "kill_switch" ? "paused" : input.eventType === "throttled" ? "throttle" : "deny",
47
+ reason: input.reason,
48
+ payload: { stage: input.stage, ...input.extraPayload },
49
+ },
50
+ };
51
+ }
52
+ /**
53
+ * Evaluate every write action against the full precedence ladder and return one fail-closed verdict. See the
54
+ * module doc comment for the exact stage order and which stages are conditional on `actionClass`.
55
+ */
56
+ export function evaluateGovernorChokepoint(input) {
57
+ const killSwitchScope = resolveMinerKillSwitch({ global: input.killSwitchGlobal, repoPaused: input.killSwitchRepoPaused });
58
+ const mode = resolveMinerActionMode({
59
+ killSwitchScope,
60
+ repoLiveModeOptIn: input.liveModeRepoOptIn,
61
+ globalLiveModeOptIn: input.liveModeGlobalOptIn,
62
+ });
63
+ const baseDetail = { killSwitchScope, mode };
64
+ if (isMinerKillSwitchActive(killSwitchScope)) {
65
+ return denyResult({
66
+ stage: "kill_switch",
67
+ reason: `${killSwitchScope}_kill_switch_active`,
68
+ mode,
69
+ detail: baseDetail,
70
+ eventType: "kill_switch",
71
+ actionClass: input.actionClass,
72
+ repoFullName: input.repoFullName,
73
+ });
74
+ }
75
+ if (!minerActionModeExecutes(mode)) {
76
+ // dry_run: shadow-log the would-be action without evaluating (or executing) anything further. The other
77
+ // stages are intentionally NOT consulted here -- the ladder's own documented order places dry-run before
78
+ // rate-limit, and a caller wanting a full "what-would-the-full-verdict-be" preview can call this function
79
+ // again with a synthetic live opt-in in a non-production dry-run harness.
80
+ return {
81
+ allowed: false,
82
+ mode,
83
+ stage: "dry_run",
84
+ reason: "dry_run_mode_active",
85
+ detail: baseDetail,
86
+ ledgerEvent: {
87
+ eventType: "allowed",
88
+ repoFullName: input.repoFullName,
89
+ actionClass: input.actionClass,
90
+ decision: "dry_run",
91
+ reason: "dry_run_mode_active",
92
+ payload: { wouldBeAction: input.wouldBeAction },
93
+ },
94
+ };
95
+ }
96
+ let rateLimit;
97
+ try {
98
+ rateLimit = evaluateWriteRateLimit({
99
+ actionClass: input.actionClass,
100
+ repoFullName: input.repoFullName,
101
+ buckets: input.rateLimitBuckets,
102
+ backoffAttempts: input.rateLimitBackoffAttempts,
103
+ nowMs: input.nowMs,
104
+ ...(input.rateLimitPolicies ? { policies: input.rateLimitPolicies } : {}),
105
+ ...(input.rateLimitRandomFn ? { randomFn: input.rateLimitRandomFn } : {}),
106
+ });
107
+ }
108
+ catch (error) {
109
+ return denyResult({
110
+ stage: "internal_error",
111
+ reason: `rate_limit_calculator_error: ${error instanceof Error ? error.message : String(error)}`,
112
+ mode,
113
+ detail: baseDetail,
114
+ eventType: "denied",
115
+ actionClass: input.actionClass,
116
+ repoFullName: input.repoFullName,
117
+ });
118
+ }
119
+ const detailWithRateLimit = { ...baseDetail, rateLimit };
120
+ if (!rateLimit.allowed) {
121
+ return denyResult({
122
+ stage: "rate_limit",
123
+ reason: rateLimit.reason,
124
+ mode,
125
+ detail: detailWithRateLimit,
126
+ eventType: "throttled",
127
+ actionClass: input.actionClass,
128
+ repoFullName: input.repoFullName,
129
+ extraPayload: { retryAfterMs: rateLimit.retryAfterMs, blockedBy: rateLimit.blockedBy },
130
+ });
131
+ }
132
+ let budgetCap;
133
+ try {
134
+ budgetCap = evaluateGovernorCaps(input.capUsage, input.capLimits);
135
+ }
136
+ catch (error) {
137
+ return denyResult({
138
+ stage: "internal_error",
139
+ reason: `budget_cap_calculator_error: ${error instanceof Error ? error.message : String(error)}`,
140
+ mode,
141
+ detail: detailWithRateLimit,
142
+ eventType: "denied",
143
+ actionClass: input.actionClass,
144
+ repoFullName: input.repoFullName,
145
+ });
146
+ }
147
+ const detailWithBudget = { ...detailWithRateLimit, budgetCap };
148
+ if (budgetCap.verdict !== "allowed") {
149
+ return denyResult({
150
+ stage: "budget_cap",
151
+ reason: `budget_cap_${budgetCap.verdict}`,
152
+ mode,
153
+ detail: detailWithBudget,
154
+ eventType: budgetCap.verdict,
155
+ actionClass: input.actionClass,
156
+ repoFullName: input.repoFullName,
157
+ extraPayload: { budget: budgetCap.budget, turns: budgetCap.turns, termination: budgetCap.termination },
158
+ });
159
+ }
160
+ let convergence;
161
+ try {
162
+ convergence = classifyPortfolioConvergence(input.convergenceInput, input.convergenceThresholds ?? DEFAULT_PORTFOLIO_CONVERGENCE_THRESHOLDS);
163
+ }
164
+ catch (error) {
165
+ return denyResult({
166
+ stage: "internal_error",
167
+ reason: `non_convergence_calculator_error: ${error instanceof Error ? error.message : String(error)}`,
168
+ mode,
169
+ detail: detailWithBudget,
170
+ eventType: "denied",
171
+ actionClass: input.actionClass,
172
+ repoFullName: input.repoFullName,
173
+ });
174
+ }
175
+ const detailWithConvergence = { ...detailWithBudget, convergence };
176
+ if (convergence.status === "non_convergent") {
177
+ return denyResult({
178
+ stage: "non_convergence",
179
+ reason: convergence.reasons.join(" "),
180
+ mode,
181
+ detail: detailWithConvergence,
182
+ eventType: "denied",
183
+ actionClass: input.actionClass,
184
+ repoFullName: input.repoFullName,
185
+ });
186
+ }
187
+ const isSelfSubmissionAction = SELF_SUBMISSION_ACTION_CLASSES.has(input.actionClass);
188
+ let detailWithReputation = detailWithConvergence;
189
+ // `!== undefined` (not a truthy check): an omitted key means "skip this stage"; any OTHER value the caller
190
+ // supplied -- including a bad `null` from a malformed upstream source -- must reach the calculator and, if it
191
+ // cannot handle it, fail closed via the catch below, never silently skip.
192
+ if (isSelfSubmissionAction && input.reputationHistory !== undefined) {
193
+ let reputation;
194
+ try {
195
+ reputation = selfReputationThrottle(input.reputationHistory, input.reputationThresholds ?? DEFAULT_SELF_REPUTATION_THRESHOLDS);
196
+ }
197
+ catch (error) {
198
+ return denyResult({
199
+ stage: "internal_error",
200
+ reason: `reputation_throttle_calculator_error: ${error instanceof Error ? error.message : String(error)}`,
201
+ mode,
202
+ detail: detailWithConvergence,
203
+ eventType: "denied",
204
+ actionClass: input.actionClass,
205
+ repoFullName: input.repoFullName,
206
+ });
207
+ }
208
+ detailWithReputation = { ...detailWithConvergence, reputation };
209
+ if (reputation.throttled) {
210
+ return denyResult({
211
+ stage: "reputation_throttle",
212
+ reason: reputation.reason,
213
+ mode,
214
+ detail: detailWithReputation,
215
+ eventType: "throttled",
216
+ actionClass: input.actionClass,
217
+ repoFullName: input.repoFullName,
218
+ extraPayload: { cadenceFactor: reputation.cadenceFactor, unfavorableRatio: reputation.unfavorableRatio },
219
+ });
220
+ }
221
+ }
222
+ let finalDetail = detailWithReputation;
223
+ // Same `!== undefined` reasoning as the reputation-throttle stage above.
224
+ if (isSelfSubmissionAction && input.selfPlagiarismCandidate !== undefined) {
225
+ let selfPlagiarism;
226
+ try {
227
+ selfPlagiarism = selfPlagiarismCheck(input.selfPlagiarismCandidate, input.selfPlagiarismRecentSubmissions ?? [], input.selfPlagiarismConfig ?? DEFAULT_SELF_PLAGIARISM_CONFIG);
228
+ }
229
+ catch (error) {
230
+ return denyResult({
231
+ stage: "internal_error",
232
+ reason: `self_plagiarism_calculator_error: ${error instanceof Error ? error.message : String(error)}`,
233
+ mode,
234
+ detail: detailWithReputation,
235
+ eventType: "denied",
236
+ actionClass: input.actionClass,
237
+ repoFullName: input.repoFullName,
238
+ });
239
+ }
240
+ finalDetail = { ...detailWithReputation, selfPlagiarism };
241
+ if (!selfPlagiarism.allowed) {
242
+ return denyResult({
243
+ stage: "self_plagiarism",
244
+ reason: selfPlagiarism.reason,
245
+ mode,
246
+ detail: finalDetail,
247
+ eventType: selfPlagiarism.eventType,
248
+ actionClass: input.actionClass,
249
+ repoFullName: input.repoFullName,
250
+ extraPayload: { similarity: selfPlagiarism.similarity ?? null },
251
+ });
252
+ }
253
+ }
254
+ return {
255
+ allowed: true,
256
+ mode,
257
+ stage: "allow",
258
+ reason: "all_governor_checks_passed",
259
+ detail: finalDetail,
260
+ ledgerEvent: {
261
+ eventType: "allowed",
262
+ repoFullName: input.repoFullName,
263
+ actionClass: input.actionClass,
264
+ decision: "allow",
265
+ reason: "all_governor_checks_passed",
266
+ payload: {},
267
+ },
268
+ };
269
+ }
@@ -0,0 +1,38 @@
1
+ import type { GovernorLedgerEvent } from "../governor-ledger.js";
2
+ /** Env var an operator sets to halt ALL miner write activity, across every repo, immediately. */
3
+ export declare const MINER_KILL_SWITCH_ENV_VAR = "GITTENSORY_MINER_KILL_SWITCH";
4
+ /** Which trigger (if any) is currently halting miner write activity for a given repo. */
5
+ export type MinerKillSwitchScope = "global" | "repo" | "none";
6
+ /**
7
+ * True when the operator's global env-level kill-switch is set. Mirrors `isGlobalAgentPause`'s idiom exactly
8
+ * (case-insensitive `1`/`true`/`yes`/`on`) — absence or any other value reads as not tripped. This function
9
+ * does not itself fail closed; the caller composing it into a decision (the Governor chokepoint) is
10
+ * responsible for that.
11
+ */
12
+ export declare function isGlobalMinerKillSwitch(env: Record<string, string | undefined>): boolean;
13
+ /**
14
+ * Resolve which kill-switch scope (if any) is active for a repo. Pure and stateless: identical inputs always
15
+ * yield the identical scope, so toggling either input off on the next call immediately reflects "resumed" with
16
+ * no residual state here to corrupt — any queue/attempt state a caller holds is untouched by this resolution.
17
+ * Precedence: a global halt always reports as `"global"`, regardless of the per-repo flag (never masked); a
18
+ * per-repo pause alone is sufficient to halt just that repo.
19
+ */
20
+ export declare function resolveMinerKillSwitch(input: {
21
+ global: boolean;
22
+ repoPaused?: boolean | null | undefined;
23
+ }): MinerKillSwitchScope;
24
+ /** True for any active scope (`"global"` or `"repo"`) — false only for `"none"`. */
25
+ export declare function isMinerKillSwitchActive(scope: MinerKillSwitchScope): boolean;
26
+ /**
27
+ * Governor-ledger row for a kill-switch STATE TRANSITION (#2341's "state changes are themselves recorded"
28
+ * deliverable) — call only when the scope actually changed since the previous check, not on every check (every
29
+ * check's allow/deny for a real write action is the Governor chokepoint's job, #2340, not this primitive's).
30
+ * Returns `null` when there is no transition, so a caller can unconditionally call this each check and only
31
+ * append when it returns non-null.
32
+ */
33
+ export declare function buildMinerKillSwitchTransitionGovernorLedgerEvent(input: {
34
+ repoFullName?: string | null | undefined;
35
+ actionClass: string;
36
+ previousScope: MinerKillSwitchScope;
37
+ scope: MinerKillSwitchScope;
38
+ }): GovernorLedgerEvent | null;
@@ -0,0 +1,62 @@
1
+ // Governor kill-switch (#2341): the emergency-halt primitive every write-adjacent governor decision consults
2
+ // FIRST, before any other calculator. Two independent triggers compose into one scope: a GLOBAL env-level
3
+ // switch that halts every repo at once, and a PER-REPO switch (from `.gittensory-miner.yml`'s MinerGoalSpec
4
+ // `killSwitch.paused` field) that halts only its own repo's queue while leaving the rest of the fleet running.
5
+ // Mirrors `src/settings/agent-execution.ts`'s `isGlobalAgentPause` truthy-string idiom for the review-stack's
6
+ // own kill-switch (#776) — a parallel mechanism for the miner's own local runtime, not the same one.
7
+ //
8
+ // DETECTOR ONLY — no IO, no persistence. Composing this with the other pure calculators into one fail-closed
9
+ // allow/deny verdict (and recording every CHECK, not just a transition) is the Governor chokepoint's job
10
+ // (#2340), which consults this module first in its "safest wins" precedence.
11
+ /** Truthy-string idiom shared with `isGlobalAgentPause` (`src/settings/agent-execution.ts`) — same accepted
12
+ * literal set, so an operator only needs to remember one convention across both kill-switches. */
13
+ const TRUTHY_ENV_VALUE = /^(1|true|yes|on)$/i;
14
+ /** Env var an operator sets to halt ALL miner write activity, across every repo, immediately. */
15
+ export const MINER_KILL_SWITCH_ENV_VAR = "GITTENSORY_MINER_KILL_SWITCH";
16
+ /**
17
+ * True when the operator's global env-level kill-switch is set. Mirrors `isGlobalAgentPause`'s idiom exactly
18
+ * (case-insensitive `1`/`true`/`yes`/`on`) — absence or any other value reads as not tripped. This function
19
+ * does not itself fail closed; the caller composing it into a decision (the Governor chokepoint) is
20
+ * responsible for that.
21
+ */
22
+ export function isGlobalMinerKillSwitch(env) {
23
+ return TRUTHY_ENV_VALUE.test(env[MINER_KILL_SWITCH_ENV_VAR] ?? "");
24
+ }
25
+ /**
26
+ * Resolve which kill-switch scope (if any) is active for a repo. Pure and stateless: identical inputs always
27
+ * yield the identical scope, so toggling either input off on the next call immediately reflects "resumed" with
28
+ * no residual state here to corrupt — any queue/attempt state a caller holds is untouched by this resolution.
29
+ * Precedence: a global halt always reports as `"global"`, regardless of the per-repo flag (never masked); a
30
+ * per-repo pause alone is sufficient to halt just that repo.
31
+ */
32
+ export function resolveMinerKillSwitch(input) {
33
+ if (input.global)
34
+ return "global";
35
+ if (input.repoPaused === true)
36
+ return "repo";
37
+ return "none";
38
+ }
39
+ /** True for any active scope (`"global"` or `"repo"`) — false only for `"none"`. */
40
+ export function isMinerKillSwitchActive(scope) {
41
+ return scope !== "none";
42
+ }
43
+ /**
44
+ * Governor-ledger row for a kill-switch STATE TRANSITION (#2341's "state changes are themselves recorded"
45
+ * deliverable) — call only when the scope actually changed since the previous check, not on every check (every
46
+ * check's allow/deny for a real write action is the Governor chokepoint's job, #2340, not this primitive's).
47
+ * Returns `null` when there is no transition, so a caller can unconditionally call this each check and only
48
+ * append when it returns non-null.
49
+ */
50
+ export function buildMinerKillSwitchTransitionGovernorLedgerEvent(input) {
51
+ if (input.previousScope === input.scope)
52
+ return null;
53
+ const tripped = isMinerKillSwitchActive(input.scope);
54
+ return {
55
+ eventType: "kill_switch",
56
+ repoFullName: input.repoFullName ?? null,
57
+ actionClass: input.actionClass,
58
+ decision: tripped ? "tripped" : "resumed",
59
+ reason: tripped ? `${input.scope}_kill_switch_engaged` : `${input.previousScope}_kill_switch_cleared`,
60
+ payload: { previousScope: input.previousScope, scope: input.scope },
61
+ };
62
+ }