@loopover/miner 0.1.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 (218) hide show
  1. package/DEPLOYMENT.md +156 -0
  2. package/Dockerfile +38 -0
  3. package/README.md +269 -0
  4. package/bin/gittensory-miner-mcp.d.ts +64 -0
  5. package/bin/gittensory-miner-mcp.js +279 -0
  6. package/bin/gittensory-miner.js +196 -0
  7. package/docs/coding-agent-driver.md +140 -0
  8. package/docs/config-precedence.md +96 -0
  9. package/docs/cross-repo-discovery-phase1.md +55 -0
  10. package/docs/discovery-index-contract.md +60 -0
  11. package/docs/discovery-plane-operator-guide.md +101 -0
  12. package/docs/env-reference.md +30 -0
  13. package/docs/fleet-run-manifest.md +50 -0
  14. package/docs/miner-goal-spec.md +76 -0
  15. package/docs/miner-selfimprove-calibration.md +105 -0
  16. package/docs/observability.md +60 -0
  17. package/docs/operations-runbook.md +209 -0
  18. package/docs/repo-agnostic-capability-audit.md +129 -0
  19. package/docs/unattended-scheduling.md +107 -0
  20. package/expected-engine.version +1 -0
  21. package/lib/ams-policy.d.ts +20 -0
  22. package/lib/ams-policy.js +68 -0
  23. package/lib/attempt-cli.d.ts +99 -0
  24. package/lib/attempt-cli.js +523 -0
  25. package/lib/attempt-input-builder.d.ts +24 -0
  26. package/lib/attempt-input-builder.js +91 -0
  27. package/lib/attempt-log.d.ts +37 -0
  28. package/lib/attempt-log.js +177 -0
  29. package/lib/attempt-runner.d.ts +59 -0
  30. package/lib/attempt-runner.js +164 -0
  31. package/lib/attempt-worktree.d.ts +31 -0
  32. package/lib/attempt-worktree.js +94 -0
  33. package/lib/calibration-cli.d.ts +1 -0
  34. package/lib/calibration-cli.js +92 -0
  35. package/lib/calibration-run.d.ts +161 -0
  36. package/lib/calibration-run.js +232 -0
  37. package/lib/calibration-types.d.ts +46 -0
  38. package/lib/calibration-types.js +74 -0
  39. package/lib/calibration.d.ts +24 -0
  40. package/lib/calibration.js +99 -0
  41. package/lib/ci-poller.d.ts +33 -0
  42. package/lib/ci-poller.js +234 -0
  43. package/lib/claim-adjudication.d.ts +21 -0
  44. package/lib/claim-adjudication.js +36 -0
  45. package/lib/claim-conflict-resolver.d.ts +29 -0
  46. package/lib/claim-conflict-resolver.js +101 -0
  47. package/lib/claim-ledger-cli.d.ts +59 -0
  48. package/lib/claim-ledger-cli.js +320 -0
  49. package/lib/claim-ledger-expiry.d.ts +20 -0
  50. package/lib/claim-ledger-expiry.js +41 -0
  51. package/lib/claim-ledger.d.ts +63 -0
  52. package/lib/claim-ledger.js +303 -0
  53. package/lib/cli-error.d.ts +3 -0
  54. package/lib/cli-error.js +27 -0
  55. package/lib/cli.d.ts +3 -0
  56. package/lib/cli.js +72 -0
  57. package/lib/coding-agent-construction.d.ts +17 -0
  58. package/lib/coding-agent-construction.js +103 -0
  59. package/lib/coding-agent-house-rules.d.ts +24 -0
  60. package/lib/coding-agent-house-rules.js +64 -0
  61. package/lib/coding-task-spec.d.ts +44 -0
  62. package/lib/coding-task-spec.js +183 -0
  63. package/lib/config-precedence.d.ts +25 -0
  64. package/lib/config-precedence.js +38 -0
  65. package/lib/deny-check.d.ts +11 -0
  66. package/lib/deny-check.js +76 -0
  67. package/lib/deny-hook-synthesis.d.ts +93 -0
  68. package/lib/deny-hook-synthesis.js +399 -0
  69. package/lib/deny-hooks.d.ts +28 -0
  70. package/lib/deny-hooks.js +155 -0
  71. package/lib/deployment-docs-audit.d.ts +41 -0
  72. package/lib/deployment-docs-audit.js +115 -0
  73. package/lib/discover-cli.d.ts +94 -0
  74. package/lib/discover-cli.js +284 -0
  75. package/lib/discovery-throttle.d.ts +8 -0
  76. package/lib/discovery-throttle.js +33 -0
  77. package/lib/env-file-indirection.d.ts +4 -0
  78. package/lib/env-file-indirection.js +45 -0
  79. package/lib/event-ledger-cli.d.ts +82 -0
  80. package/lib/event-ledger-cli.js +266 -0
  81. package/lib/event-ledger.d.ts +37 -0
  82. package/lib/event-ledger.js +210 -0
  83. package/lib/execute-local-write.d.ts +14 -0
  84. package/lib/execute-local-write.js +50 -0
  85. package/lib/feasibility-cli.d.ts +25 -0
  86. package/lib/feasibility-cli.js +80 -0
  87. package/lib/forge-config.d.ts +17 -0
  88. package/lib/forge-config.js +37 -0
  89. package/lib/governor-action-mode.d.ts +26 -0
  90. package/lib/governor-action-mode.js +47 -0
  91. package/lib/governor-chokepoint-persisted.d.ts +18 -0
  92. package/lib/governor-chokepoint-persisted.js +46 -0
  93. package/lib/governor-chokepoint.d.ts +14 -0
  94. package/lib/governor-chokepoint.js +52 -0
  95. package/lib/governor-kill-switch.d.ts +26 -0
  96. package/lib/governor-kill-switch.js +47 -0
  97. package/lib/governor-ledger-cli.d.ts +32 -0
  98. package/lib/governor-ledger-cli.js +155 -0
  99. package/lib/governor-ledger.d.ts +46 -0
  100. package/lib/governor-ledger.js +203 -0
  101. package/lib/governor-open-pr.d.ts +21 -0
  102. package/lib/governor-open-pr.js +27 -0
  103. package/lib/governor-pause-cli.d.ts +23 -0
  104. package/lib/governor-pause-cli.js +171 -0
  105. package/lib/governor-run-halt.d.ts +37 -0
  106. package/lib/governor-run-halt.js +58 -0
  107. package/lib/governor-state.d.ts +63 -0
  108. package/lib/governor-state.js +363 -0
  109. package/lib/governor-write-rate-limit.d.ts +30 -0
  110. package/lib/governor-write-rate-limit.js +64 -0
  111. package/lib/harness-submission-trigger.d.ts +69 -0
  112. package/lib/harness-submission-trigger.js +138 -0
  113. package/lib/http-retry.d.ts +12 -0
  114. package/lib/http-retry.js +52 -0
  115. package/lib/laptop-init.d.ts +53 -0
  116. package/lib/laptop-init.js +332 -0
  117. package/lib/live-issue-snapshot.d.ts +16 -0
  118. package/lib/live-issue-snapshot.js +117 -0
  119. package/lib/local-store.d.ts +18 -0
  120. package/lib/local-store.js +62 -0
  121. package/lib/logger.d.ts +58 -0
  122. package/lib/logger.js +167 -0
  123. package/lib/loop-cli.d.ts +66 -0
  124. package/lib/loop-cli.js +540 -0
  125. package/lib/loop-closure.d.ts +35 -0
  126. package/lib/loop-closure.js +66 -0
  127. package/lib/loop-reentry.d.ts +51 -0
  128. package/lib/loop-reentry.js +124 -0
  129. package/lib/manage-poll.d.ts +79 -0
  130. package/lib/manage-poll.js +254 -0
  131. package/lib/manage-status.d.ts +70 -0
  132. package/lib/manage-status.js +241 -0
  133. package/lib/metrics-cli.d.ts +9 -0
  134. package/lib/metrics-cli.js +50 -0
  135. package/lib/migrate-cli.d.ts +24 -0
  136. package/lib/migrate-cli.js +111 -0
  137. package/lib/miner-goal-spec.d.ts +12 -0
  138. package/lib/miner-goal-spec.js +67 -0
  139. package/lib/opportunity-fanout.d.ts +86 -0
  140. package/lib/opportunity-fanout.js +597 -0
  141. package/lib/opportunity-ranker.d.ts +36 -0
  142. package/lib/opportunity-ranker.js +118 -0
  143. package/lib/orb-export.d.ts +54 -0
  144. package/lib/orb-export.js +206 -0
  145. package/lib/plan-store-cli.d.ts +37 -0
  146. package/lib/plan-store-cli.js +151 -0
  147. package/lib/plan-store.d.ts +51 -0
  148. package/lib/plan-store.js +231 -0
  149. package/lib/policy-doc-cache.d.ts +25 -0
  150. package/lib/policy-doc-cache.js +79 -0
  151. package/lib/policy-verdict-cache.d.ts +35 -0
  152. package/lib/policy-verdict-cache.js +106 -0
  153. package/lib/portfolio-dashboard.d.ts +30 -0
  154. package/lib/portfolio-dashboard.js +105 -0
  155. package/lib/portfolio-discovery.d.ts +29 -0
  156. package/lib/portfolio-discovery.js +100 -0
  157. package/lib/portfolio-queue-cli.d.ts +87 -0
  158. package/lib/portfolio-queue-cli.js +509 -0
  159. package/lib/portfolio-queue-expiry.d.ts +20 -0
  160. package/lib/portfolio-queue-expiry.js +51 -0
  161. package/lib/portfolio-queue-manager.d.ts +50 -0
  162. package/lib/portfolio-queue-manager.js +134 -0
  163. package/lib/portfolio-queue.d.ts +62 -0
  164. package/lib/portfolio-queue.js +351 -0
  165. package/lib/pr-disposition-poller.d.ts +26 -0
  166. package/lib/pr-disposition-poller.js +163 -0
  167. package/lib/pr-number-parse.d.ts +4 -0
  168. package/lib/pr-number-parse.js +23 -0
  169. package/lib/pr-outcome.d.ts +41 -0
  170. package/lib/pr-outcome.js +90 -0
  171. package/lib/prediction-ledger.d.ts +47 -0
  172. package/lib/prediction-ledger.js +222 -0
  173. package/lib/pretooluse-hook.d.ts +31 -0
  174. package/lib/pretooluse-hook.js +93 -0
  175. package/lib/process-lifecycle.d.ts +31 -0
  176. package/lib/process-lifecycle.js +106 -0
  177. package/lib/purge-cli.d.ts +44 -0
  178. package/lib/purge-cli.js +198 -0
  179. package/lib/rejection-signal.d.ts +6 -0
  180. package/lib/rejection-signal.js +101 -0
  181. package/lib/rejection-state-machine.d.ts +37 -0
  182. package/lib/rejection-state-machine.js +81 -0
  183. package/lib/rejection-templates.d.ts +12 -0
  184. package/lib/rejection-templates.js +71 -0
  185. package/lib/replay-objective-anchor.d.ts +79 -0
  186. package/lib/replay-objective-anchor.js +179 -0
  187. package/lib/replay-snapshot.d.ts +41 -0
  188. package/lib/replay-snapshot.js +288 -0
  189. package/lib/replay-task-generation.d.ts +126 -0
  190. package/lib/replay-task-generation.js +223 -0
  191. package/lib/repo-clone.d.ts +19 -0
  192. package/lib/repo-clone.js +102 -0
  193. package/lib/run-state-cli.d.ts +27 -0
  194. package/lib/run-state-cli.js +154 -0
  195. package/lib/run-state.d.ts +37 -0
  196. package/lib/run-state.js +150 -0
  197. package/lib/schema-version.d.ts +17 -0
  198. package/lib/schema-version.js +71 -0
  199. package/lib/self-review-context.d.ts +28 -0
  200. package/lib/self-review-context.js +339 -0
  201. package/lib/slop-assessment.d.ts +3 -0
  202. package/lib/slop-assessment.js +20 -0
  203. package/lib/stack-detection.d.ts +41 -0
  204. package/lib/stack-detection.js +248 -0
  205. package/lib/status.d.ts +64 -0
  206. package/lib/status.js +441 -0
  207. package/lib/store-maintenance.d.ts +31 -0
  208. package/lib/store-maintenance.js +183 -0
  209. package/lib/submission-freshness-check.d.ts +32 -0
  210. package/lib/submission-freshness-check.js +93 -0
  211. package/lib/update-check.d.ts +36 -0
  212. package/lib/update-check.js +161 -0
  213. package/lib/version.d.ts +3 -0
  214. package/lib/version.js +10 -0
  215. package/lib/worktree-allocator.d.ts +39 -0
  216. package/lib/worktree-allocator.js +262 -0
  217. package/package.json +50 -0
  218. package/schema/miner-goal-spec.schema.json +111 -0
@@ -0,0 +1,17 @@
1
+ /** Per-tenant forge configuration (#4784). Every field is a string knob defaulting to the github.com value in
2
+ * `DEFAULT_FORGE_CONFIG`; a tenant overrides only what differs for their forge. */
3
+ export type ForgeConfig = {
4
+ apiBaseUrl: string;
5
+ apiVersion: string;
6
+ apiVersionHeader: string;
7
+ acceptHeader: string;
8
+ userAgent: string;
9
+ repoPathPrefix: string;
10
+ searchEndpoint: string;
11
+ searchQualifiers: string;
12
+ tokenEnvVar: string;
13
+ };
14
+
15
+ export const DEFAULT_FORGE_CONFIG: Readonly<ForgeConfig>;
16
+
17
+ export function resolveForgeConfig(overrides?: Partial<ForgeConfig>): ForgeConfig;
@@ -0,0 +1,37 @@
1
+ /** Per-tenant forge configuration (#4784): the GitHub-specific protocol details that discovery used to hardcode,
2
+ * gathered behind one resolver so a non-github.com tenant (GitHub Enterprise, or another GitHub-compatible forge)
3
+ * can override them. gittensory's own github.com conventions survive only as `DEFAULT_FORGE_CONFIG` — calling
4
+ * `resolveForgeConfig()` with no overrides is byte-identical to the pre-#4784 hardcoded fan-out behavior, which is
5
+ * what keeps the existing gittensory discovery path unchanged. Executes the #4780 repo-agnostic-capability-audit
6
+ * checklist (forge abstraction, configurable credential env var, configurable user-agent). */
7
+
8
+ /** The github.com defaults every forge field falls back to. Frozen so a caller can't mutate the shared baseline. */
9
+ export const DEFAULT_FORGE_CONFIG = Object.freeze({
10
+ apiBaseUrl: "https://api.github.com",
11
+ apiVersion: "2022-11-28",
12
+ apiVersionHeader: "x-github-api-version",
13
+ acceptHeader: "application/vnd.github+json",
14
+ userAgent: "loopover-miner",
15
+ repoPathPrefix: "/repos",
16
+ searchEndpoint: "/search/issues",
17
+ searchQualifiers: "state:open type:issue",
18
+ tokenEnvVar: "GITHUB_TOKEN",
19
+ });
20
+
21
+ function trimmedStringOr(value, fallback) {
22
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
23
+ }
24
+
25
+ /**
26
+ * Resolve a full forge config from partial per-tenant overrides. Every field is an independent string knob that
27
+ * falls back to its github.com default when the override is missing, non-string, or blank — so a partial override
28
+ * (say, only `apiBaseUrl` for a GitHub Enterprise host) still yields a complete, usable config.
29
+ */
30
+ export function resolveForgeConfig(overrides = {}) {
31
+ const source = overrides && typeof overrides === "object" ? overrides : {};
32
+ const resolved = {};
33
+ for (const [key, fallback] of Object.entries(DEFAULT_FORGE_CONFIG)) {
34
+ resolved[key] = trimmedStringOr(source[key], fallback);
35
+ }
36
+ return resolved;
37
+ }
@@ -0,0 +1,26 @@
1
+ import type { MinerActionMode, MinerKillSwitchScope } from "@loopover/engine";
2
+ import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js";
3
+
4
+ export type ResolveMinerActionModeGateInput = {
5
+ killSwitchScope: MinerKillSwitchScope;
6
+ repoLiveModeOptIn?: unknown;
7
+ env?: Record<string, string | undefined>;
8
+ };
9
+
10
+ export type ResolveMinerActionModeGateResult = {
11
+ mode: MinerActionMode;
12
+ executes: boolean;
13
+ };
14
+
15
+ export function resolveMinerActionModeGate(input: ResolveMinerActionModeGateInput): ResolveMinerActionModeGateResult;
16
+
17
+ export type RecordMinerDryRunShadowInput = {
18
+ repoFullName?: string;
19
+ actionClass: string;
20
+ wouldBeAction: Record<string, unknown>;
21
+ };
22
+
23
+ export function recordMinerDryRunShadow(
24
+ input: RecordMinerDryRunShadowInput,
25
+ options?: { append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry },
26
+ ): GovernorLedgerEntry;
@@ -0,0 +1,47 @@
1
+ // Governor dry-run-by-default gate (#2342). Resolves the miner's overall action mode (paused > dry_run > live,
2
+ // "safest wins") and records dry-run SHADOW actions to the append-only governor ledger. A freshly-configured
3
+ // miner defaults to dry_run -- live execution requires explicit, hard-to-fat-finger operator and repo opt-ins.
4
+
5
+ import {
6
+ buildMinerDryRunGovernorLedgerEvent,
7
+ isGlobalMinerLiveModeOptIn,
8
+ minerActionModeExecutes,
9
+ resolveMinerActionMode,
10
+ } from "@loopover/engine";
11
+ import { appendGovernorEvent } from "./governor-ledger.js";
12
+
13
+ /**
14
+ * Resolve the miner's overall action mode from the kill-switch scope (see `checkMinerKillSwitch` in
15
+ * `./governor-kill-switch.js`), the repo's own `.gittensory-miner.yml` opt-in, and the operator's global env
16
+ * opt-in. Both sides must opt in before real writes execute; repo config alone only preserves dry-run.
17
+ *
18
+ * @param {object} input
19
+ * @param {import("@loopover/engine").MinerKillSwitchScope} input.killSwitchScope
20
+ * @param {unknown} [input.repoLiveModeOptIn] `MinerGoalSpec.execution.liveModeOptIn` from the target repo
21
+ * @param {Record<string, string | undefined>} [input.env]
22
+ * @returns {{ mode: import("@loopover/engine").MinerActionMode, executes: boolean }}
23
+ */
24
+ export function resolveMinerActionModeGate(input) {
25
+ const env = input.env ?? process.env;
26
+ const mode = resolveMinerActionMode({
27
+ killSwitchScope: input.killSwitchScope,
28
+ repoLiveModeOptIn: input.repoLiveModeOptIn,
29
+ globalLiveModeOptIn: isGlobalMinerLiveModeOptIn(env),
30
+ });
31
+ return { mode, executes: minerActionModeExecutes(mode) };
32
+ }
33
+
34
+ /**
35
+ * Record a dry-run shadow action (the WOULD-BE `LocalWriteActionSpec`) to the governor ledger, without ever
36
+ * invoking the actual command.
37
+ *
38
+ * @param {object} input
39
+ * @param {string} [input.repoFullName]
40
+ * @param {string} input.actionClass
41
+ * @param {Record<string, unknown>} input.wouldBeAction
42
+ * @param {{ append?: typeof appendGovernorEvent }} [options]
43
+ */
44
+ export function recordMinerDryRunShadow(input, options = {}) {
45
+ const append = options.append ?? appendGovernorEvent;
46
+ return append(buildMinerDryRunGovernorLedgerEvent(input));
47
+ }
@@ -0,0 +1,18 @@
1
+ import type { GovernorChokepointInput } from "@loopover/engine";
2
+ import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js";
3
+ import type { EvaluateGovernorChokepointGateResult } from "./governor-chokepoint.js";
4
+ import type { GovernorState } from "./governor-state.js";
5
+
6
+ // rateLimitBuckets/rateLimitBackoffAttempts/capUsage are required on GovernorChokepointInput itself, but this
7
+ // wrapper auto-supplies them from persisted state when the caller omits them -- loosen just those three to
8
+ // optional so a caller that WANTS the persisted defaults doesn't have to fake a value just to satisfy the type.
9
+ export type GovernorChokepointInputPersisted = Omit<GovernorChokepointInput, "rateLimitBuckets" | "rateLimitBackoffAttempts" | "capUsage"> &
10
+ Partial<Pick<GovernorChokepointInput, "rateLimitBuckets" | "rateLimitBackoffAttempts" | "capUsage">>;
11
+
12
+ export function evaluateGovernorChokepointGatePersisted(
13
+ input: GovernorChokepointInputPersisted,
14
+ options?: {
15
+ governorState?: GovernorState;
16
+ append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry;
17
+ },
18
+ ): EvaluateGovernorChokepointGateResult;
@@ -0,0 +1,46 @@
1
+ import { evaluateGovernorChokepointGate } from "./governor-chokepoint.js";
2
+ import { openGovernorState } from "./governor-state.js";
3
+
4
+ // The real cross-attempt integration point for #5134: composes governor-chokepoint.js's existing, UNMODIFIED
5
+ // evaluateGovernorChokepointGate (still exactly as pure-per-call as before -- every existing caller/test of
6
+ // it is untouched) with governor-state.js's persistence, so attempt N+1's decision actually sees attempt N's
7
+ // rate-limit/backoff outcome. Kept as a separate composing function rather than changing
8
+ // evaluateGovernorChokepointGate itself: this issue is flagged as the safety-critical core of its gap-fill
9
+ // batch, and a caller-controlled wrapper is a smaller, more isolated surface to review than a behavior change
10
+ // to an already-relied-upon function.
11
+ //
12
+ // capUsage is LOADED here (so a caller that doesn't track its own running totals still gets real prior state
13
+ // instead of silently starting from zero every call) but deliberately NOT saved here: budget-cap.ts's
14
+ // GovernorCapUsage has no mutator (unlike write-rate-limit.ts's buckets/backoff, nothing computes "the next
15
+ // capUsage" from a verdict -- the caller is the only one who knows how much THIS attempt actually spent,
16
+ // which isn't known until after the attempt runs, not at the gate-check moment). Saving the next capUsage is
17
+ // the caller's job via `saveCapUsage` once the attempt's real spend/turns/elapsed are known.
18
+
19
+ /**
20
+ * @param {import("./governor-chokepoint-persisted.js").GovernorChokepointInputPersisted} input
21
+ * @param {{
22
+ * governorState?: import("./governor-state.js").GovernorState,
23
+ * append?: (event: unknown) => unknown,
24
+ * }} [options]
25
+ * @returns {import("./governor-chokepoint.js").EvaluateGovernorChokepointGateResult}
26
+ */
27
+ export function evaluateGovernorChokepointGatePersisted(input, options = {}) {
28
+ const ownsGovernorState = options.governorState === undefined;
29
+ const governorState = options.governorState ?? openGovernorState();
30
+ try {
31
+ const persistedRateLimit = governorState.loadRateLimitState();
32
+ const persistedCapUsage = governorState.loadCapUsage();
33
+ const resolvedInput = {
34
+ ...input,
35
+ rateLimitBuckets: input.rateLimitBuckets ?? persistedRateLimit.buckets,
36
+ rateLimitBackoffAttempts: input.rateLimitBackoffAttempts ?? persistedRateLimit.backoffAttempts,
37
+ capUsage: input.capUsage ?? persistedCapUsage,
38
+ };
39
+ const gateOptions = options.append === undefined ? {} : { append: options.append };
40
+ const result = evaluateGovernorChokepointGate(resolvedInput, gateOptions);
41
+ governorState.saveRateLimitState({ buckets: result.rateLimitBuckets, backoffAttempts: result.rateLimitBackoffAttempts });
42
+ return result;
43
+ } finally {
44
+ if (ownsGovernorState) governorState.close();
45
+ }
46
+ }
@@ -0,0 +1,14 @@
1
+ import type { GovernorChokepointInput, GovernorDecision, WriteRateLimitBackoffStore, WriteRateLimitBucketStore } from "@loopover/engine";
2
+ import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js";
3
+
4
+ export type EvaluateGovernorChokepointGateResult = {
5
+ decision: GovernorDecision;
6
+ recorded: GovernorLedgerEntry;
7
+ rateLimitBuckets: WriteRateLimitBucketStore;
8
+ rateLimitBackoffAttempts: WriteRateLimitBackoffStore;
9
+ };
10
+
11
+ export function evaluateGovernorChokepointGate(
12
+ input: GovernorChokepointInput,
13
+ options?: { append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry },
14
+ ): EvaluateGovernorChokepointGateResult;
@@ -0,0 +1,52 @@
1
+ // The Governor chokepoint gate (#2340). Wraps the pure `evaluateGovernorChokepoint` engine decision with the
2
+ // two stateful side effects every caller needs: persisting the resulting ledger event, and (only when the
3
+ // rate-limit stage actually ran) advancing/backing-off the rate-limit bucket state. This is the ONLY sanctioned
4
+ // call site a real write action (open_pr, file_issue, apply_labels, post_eligibility_comment, create_branch,
5
+ // delete_branch, generate_tests) should be gated through.
6
+
7
+ import {
8
+ clearWriteRateLimitBackoff,
9
+ evaluateGovernorChokepoint,
10
+ recordWriteRateLimitAllowed,
11
+ recordWriteRateLimitDenied,
12
+ } from "@loopover/engine";
13
+ import { appendGovernorEvent } from "./governor-ledger.js";
14
+
15
+ /**
16
+ * Evaluate a write action against the full Governor precedence ladder, persist the resulting ledger event, and
17
+ * advance rate-limit bucket/backoff state when the rate-limit stage actually ran (kill-switch and dry-run
18
+ * short-circuit before rate-limit is evaluated, so bucket state is untouched in those cases).
19
+ *
20
+ * @param {import("@loopover/engine").GovernorChokepointInput} input
21
+ * @param {{ append?: typeof appendGovernorEvent }} [options]
22
+ * @returns {{
23
+ * decision: import("@loopover/engine").GovernorDecision,
24
+ * recorded: import("./governor-ledger.js").GovernorLedgerEntry,
25
+ * rateLimitBuckets: import("@loopover/engine").WriteRateLimitBucketStore,
26
+ * rateLimitBackoffAttempts: import("@loopover/engine").WriteRateLimitBackoffStore,
27
+ * }}
28
+ */
29
+ export function evaluateGovernorChokepointGate(input, options = {}) {
30
+ const append = options.append ?? appendGovernorEvent;
31
+ const decision = evaluateGovernorChokepoint(input);
32
+ const recorded = append(decision.ledgerEvent);
33
+
34
+ let rateLimitBuckets = input.rateLimitBuckets;
35
+ let rateLimitBackoffAttempts = input.rateLimitBackoffAttempts;
36
+ if (decision.detail.rateLimit) {
37
+ if (decision.detail.rateLimit.allowed) {
38
+ rateLimitBuckets = recordWriteRateLimitAllowed(
39
+ input.rateLimitBuckets,
40
+ input.actionClass,
41
+ input.repoFullName,
42
+ input.nowMs,
43
+ input.rateLimitPolicies,
44
+ );
45
+ rateLimitBackoffAttempts = clearWriteRateLimitBackoff(input.rateLimitBackoffAttempts, input.actionClass, input.repoFullName);
46
+ } else {
47
+ rateLimitBackoffAttempts = recordWriteRateLimitDenied(input.rateLimitBackoffAttempts, input.actionClass, input.repoFullName);
48
+ }
49
+ }
50
+
51
+ return { decision, recorded, rateLimitBuckets, rateLimitBackoffAttempts };
52
+ }
@@ -0,0 +1,26 @@
1
+ import type { MinerKillSwitchScope } from "@loopover/engine";
2
+ import type { AppendGovernorEventInput, GovernorLedgerEntry } from "./governor-ledger.js";
3
+
4
+ export type CheckMinerKillSwitchInput = {
5
+ repoPaused?: boolean;
6
+ env?: Record<string, string | undefined>;
7
+ };
8
+
9
+ export type CheckMinerKillSwitchResult = {
10
+ scope: MinerKillSwitchScope;
11
+ active: boolean;
12
+ };
13
+
14
+ export function checkMinerKillSwitch(input?: CheckMinerKillSwitchInput): CheckMinerKillSwitchResult;
15
+
16
+ export type RecordMinerKillSwitchTransitionInput = {
17
+ repoFullName?: string;
18
+ actionClass: string;
19
+ previousScope: MinerKillSwitchScope;
20
+ scope: MinerKillSwitchScope;
21
+ };
22
+
23
+ export function recordMinerKillSwitchTransition(
24
+ input: RecordMinerKillSwitchTransitionInput,
25
+ options?: { append?: (event: AppendGovernorEventInput) => GovernorLedgerEntry },
26
+ ): GovernorLedgerEntry | null;
@@ -0,0 +1,47 @@
1
+ // Governor kill-switch gate (#2341). Resolves whether miner write activity is currently halted (globally, via
2
+ // env, or for one repo, via its .gittensory-miner.yml MinerGoalSpec) and records STATE TRANSITIONS to the
3
+ // append-only governor ledger. Every-check allow/deny recording for a real write action is the fail-closed
4
+ // Governor chokepoint's job (#2340), which consults this module first in its "safest wins" precedence.
5
+
6
+ import {
7
+ buildMinerKillSwitchTransitionGovernorLedgerEvent,
8
+ isGlobalMinerKillSwitch,
9
+ isMinerKillSwitchActive,
10
+ resolveMinerKillSwitch,
11
+ } from "@loopover/engine";
12
+ import { appendGovernorEvent } from "./governor-ledger.js";
13
+
14
+ /**
15
+ * Resolve the current kill-switch scope for a repo from process env plus a per-repo paused flag (typically
16
+ * `MinerGoalSpec.killSwitch.paused` from the repo's parsed `.gittensory-miner.yml`).
17
+ *
18
+ * @param {object} [input]
19
+ * @param {boolean} [input.repoPaused]
20
+ * @param {Record<string, string | undefined>} [input.env]
21
+ * @returns {{ scope: import("@loopover/engine").MinerKillSwitchScope, active: boolean }}
22
+ */
23
+ export function checkMinerKillSwitch(input = {}) {
24
+ const env = input.env ?? process.env;
25
+ const global = isGlobalMinerKillSwitch(env);
26
+ const scope = resolveMinerKillSwitch({ global, repoPaused: input.repoPaused });
27
+ return { scope, active: isMinerKillSwitchActive(scope) };
28
+ }
29
+
30
+ /**
31
+ * Record a kill-switch state transition to the governor ledger. No-op (returns null, appends nothing) when the
32
+ * scope has not actually changed since the previous check — callers own tracking the previous scope (in-memory
33
+ * or persisted); this module holds no state of its own.
34
+ *
35
+ * @param {object} input
36
+ * @param {string} [input.repoFullName]
37
+ * @param {string} input.actionClass
38
+ * @param {import("@loopover/engine").MinerKillSwitchScope} input.previousScope
39
+ * @param {import("@loopover/engine").MinerKillSwitchScope} input.scope
40
+ * @param {{ append?: typeof appendGovernorEvent }} [options]
41
+ */
42
+ export function recordMinerKillSwitchTransition(input, options = {}) {
43
+ const event = buildMinerKillSwitchTransitionGovernorLedgerEvent(input);
44
+ if (!event) return null;
45
+ const append = options.append ?? appendGovernorEvent;
46
+ return append(event);
47
+ }
@@ -0,0 +1,32 @@
1
+ import type { GovernorLedger, GovernorLedgerEntry } from "./governor-ledger.js";
2
+ import type { GovernorPauseCliOptions } from "./governor-pause-cli.js";
3
+
4
+ export type GovernorLedgerEventType = "allowed" | "denied" | "throttled" | "kill_switch";
5
+
6
+ export type ParsedGovernorListArgs =
7
+ | {
8
+ json: boolean;
9
+ repoFullName: string | null;
10
+ type: GovernorLedgerEventType | null;
11
+ }
12
+ | { error: string };
13
+
14
+ export function parseGovernorListArgs(args: string[]): ParsedGovernorListArgs;
15
+
16
+ export function filterGovernorEvents(
17
+ events: GovernorLedgerEntry[],
18
+ options?: { type?: string | null },
19
+ ): GovernorLedgerEntry[];
20
+
21
+ export function renderGovernorTable(events: GovernorLedgerEntry[]): string;
22
+
23
+ export function runGovernorList(
24
+ args: string[],
25
+ options?: { initGovernorLedger?: () => GovernorLedger },
26
+ ): Promise<number>;
27
+
28
+ export function runGovernorCli(
29
+ subcommand: string | undefined,
30
+ args: string[],
31
+ options?: { initGovernorLedger?: () => GovernorLedger } & GovernorPauseCliOptions,
32
+ ): Promise<number>;
@@ -0,0 +1,155 @@
1
+ import { runGovernorPause, runGovernorResume, runGovernorStatus } from "./governor-pause-cli.js";
2
+
3
+ /** Must match `GOVERNOR_LEDGER_EVENT_TYPES` in `@loopover/engine`. */
4
+ import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
5
+
6
+ const GOVERNOR_LEDGER_EVENT_TYPES = Object.freeze([
7
+ "allowed",
8
+ "denied",
9
+ "throttled",
10
+ "kill_switch",
11
+ ]);
12
+
13
+ const GOVERNOR_LIST_USAGE =
14
+ "Usage: gittensory-miner governor list [--repo <owner/repo>] [--type allowed|denied|throttled|kill_switch] [--json]";
15
+
16
+ const GOVERNOR_SUBCOMMAND_USAGE = [
17
+ GOVERNOR_LIST_USAGE,
18
+ " gittensory-miner governor pause [--reason <text>] [--json]",
19
+ " gittensory-miner governor resume [--json]",
20
+ " gittensory-miner governor status [--json]",
21
+ ].join("\n");
22
+
23
+ function parseRepoArg(value, usage) {
24
+ if (!value) return { error: usage };
25
+ const trimmed = value.trim();
26
+ const [owner, repo, extra] = trimmed.split("/");
27
+ if (!owner || !repo || extra !== undefined) {
28
+ return { error: "Repository must be in owner/repo form." };
29
+ }
30
+ return { repoFullName: `${owner}/${repo}` };
31
+ }
32
+
33
+ export function parseGovernorListArgs(args) {
34
+ const options = { json: false, repoFullName: null, type: null };
35
+ const positional = [];
36
+
37
+ for (let index = 0; index < args.length; index += 1) {
38
+ const token = args[index];
39
+ if (token === "--json") {
40
+ options.json = true;
41
+ continue;
42
+ }
43
+ if (token === "--repo") {
44
+ const repoArg = args[index + 1];
45
+ if (!repoArg || repoArg.startsWith("-")) return { error: GOVERNOR_LIST_USAGE };
46
+ const repo = parseRepoArg(repoArg, GOVERNOR_LIST_USAGE);
47
+ if ("error" in repo) return repo;
48
+ options.repoFullName = repo.repoFullName;
49
+ index += 1;
50
+ continue;
51
+ }
52
+ if (token === "--type") {
53
+ const type = args[index + 1];
54
+ if (!type || type.startsWith("-")) return { error: GOVERNOR_LIST_USAGE };
55
+ const trimmed = type.trim();
56
+ if (!GOVERNOR_LEDGER_EVENT_TYPES.includes(trimmed)) {
57
+ return {
58
+ error: `Invalid type: ${trimmed}. Expected one of ${GOVERNOR_LEDGER_EVENT_TYPES.join(", ")}.`,
59
+ };
60
+ }
61
+ options.type = trimmed;
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (token.startsWith("-")) return { error: `Unknown option: ${token}` };
66
+ positional.push(token);
67
+ }
68
+
69
+ if (positional.length > 0) return { error: GOVERNOR_LIST_USAGE };
70
+ return options;
71
+ }
72
+
73
+ export function filterGovernorEvents(events, options = {}) {
74
+ if (!Array.isArray(events)) return [];
75
+ const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null;
76
+ if (!type) return events;
77
+ return events.filter((entry) => entry.eventType === type);
78
+ }
79
+
80
+ function display(value) {
81
+ if (value === null || value === undefined) return "-";
82
+ return String(value);
83
+ }
84
+
85
+ export function renderGovernorTable(events) {
86
+ if (!Array.isArray(events) || events.length === 0) return "no governor ledger entries";
87
+ const header = [
88
+ "id".padStart(4),
89
+ "type".padEnd(12),
90
+ "repo".padEnd(24),
91
+ "action".padEnd(10),
92
+ "decision".padEnd(10),
93
+ "ts".padEnd(24),
94
+ ].join(" ");
95
+ const lines = events.map((entry) =>
96
+ [
97
+ String(entry.id).padStart(4),
98
+ entry.eventType.padEnd(12),
99
+ display(entry.repoFullName).padEnd(24),
100
+ entry.actionClass.padEnd(10),
101
+ entry.decision.padEnd(10),
102
+ display(entry.ts).padEnd(24),
103
+ ].join(" "),
104
+ );
105
+ return [header, ...lines].join("\n");
106
+ }
107
+
108
+ async function withGovernorLedger(options, run) {
109
+ const ownsLedger = options.initGovernorLedger === undefined;
110
+ const initGovernorLedger =
111
+ options.initGovernorLedger ?? (await import("./governor-ledger.js")).initGovernorLedger;
112
+ const governorLedger = initGovernorLedger();
113
+ try {
114
+ return run(governorLedger);
115
+ } finally {
116
+ if (ownsLedger) governorLedger.close();
117
+ }
118
+ }
119
+
120
+ export async function runGovernorList(args, options = {}) {
121
+ const parsed = parseGovernorListArgs(args);
122
+ if ("error" in parsed) {
123
+ return reportCliFailure(argsWantJson(args), parsed.error);
124
+ }
125
+
126
+ try {
127
+ return await withGovernorLedger(options, (governorLedger) => {
128
+ const events = filterGovernorEvents(
129
+ governorLedger.readGovernorEvents({
130
+ repoFullName: parsed.repoFullName,
131
+ }),
132
+ { type: parsed.type },
133
+ );
134
+ if (parsed.json) {
135
+ console.log(JSON.stringify({ events }, null, 2));
136
+ } else {
137
+ console.log(renderGovernorTable(events));
138
+ }
139
+ return 0;
140
+ });
141
+ } catch (error) {
142
+ return reportCliFailure(parsed.json, describeCliError(error));
143
+ }
144
+ }
145
+
146
+ export async function runGovernorCli(subcommand, args, options = {}) {
147
+ if (subcommand === "list") return runGovernorList(args, options);
148
+ if (subcommand === "pause") return runGovernorPause(args, options);
149
+ if (subcommand === "resume") return runGovernorResume(args, options);
150
+ if (subcommand === "status") return runGovernorStatus(args, options);
151
+ return reportCliFailure(
152
+ argsWantJson(args),
153
+ `Unknown governor subcommand: ${subcommand ?? ""}.\n${GOVERNOR_SUBCOMMAND_USAGE}`,
154
+ );
155
+ }
@@ -0,0 +1,46 @@
1
+ export type GovernorLedgerEntry = {
2
+ id: number;
3
+ ts: string;
4
+ eventType: string;
5
+ repoFullName: string | null;
6
+ actionClass: string;
7
+ decision: string;
8
+ reason: string;
9
+ payload: Record<string, unknown>;
10
+ };
11
+
12
+ export type AppendGovernorEventInput = {
13
+ eventType: string;
14
+ repoFullName?: string | null;
15
+ actionClass: string;
16
+ decision: string;
17
+ reason: string;
18
+ payload?: Record<string, unknown>;
19
+ };
20
+
21
+ export type ReadGovernorEventsFilter = {
22
+ repoFullName?: string | null;
23
+ };
24
+
25
+ /** The public decision-log projection (#5159): every {@link GovernorLedgerEntry} field EXCEPT `payload`. */
26
+ export type GovernorDecisionEntry = Omit<GovernorLedgerEntry, "payload">;
27
+
28
+ export type GovernorLedger = {
29
+ dbPath: string;
30
+ appendGovernorEvent(event: AppendGovernorEventInput): GovernorLedgerEntry;
31
+ readGovernorEvents(filter?: ReadGovernorEventsFilter): GovernorLedgerEntry[];
32
+ /** Read-only decision-log projection; excludes `payload` by construction (explicit named-column SELECT). */
33
+ readGovernorDecisions(filter?: ReadGovernorEventsFilter): GovernorDecisionEntry[];
34
+ purgeByRepo(repoFullName: string): number;
35
+ close(): void;
36
+ };
37
+
38
+ export function resolveGovernorLedgerDbPath(env?: Record<string, string | undefined>): string;
39
+
40
+ export function initGovernorLedger(dbPath?: string): GovernorLedger;
41
+
42
+ export function appendGovernorEvent(event: AppendGovernorEventInput): GovernorLedgerEntry;
43
+
44
+ export function readGovernorEvents(filter?: ReadGovernorEventsFilter): GovernorLedgerEntry[];
45
+
46
+ export function closeDefaultGovernorLedger(): void;