@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,93 @@
1
+ // PreToolUse-hook-enforced house rules (#2343). Wraps the pure `evaluateDenyHooks` decision function
2
+ // (deny-hooks.js, #2295) into a real Claude Agent SDK PreToolUse hook callback -- the actual live
3
+ // interception point a CodingAgentDriver session registers via `options.hooks.PreToolUse` (the exact
4
+ // seam `agent-sdk-driver.ts`'s `hooks` passthrough documents as "#2343's stated attachment point").
5
+ //
6
+ // WHY THIS HOLDS EVEN UNDER bypassPermissions: per the Agent SDK's own documented permission-evaluation
7
+ // order (https://code.claude.com/docs/en/agent-sdk/permissions), hooks run FIRST -- before deny rules,
8
+ // ask rules, the permission mode check, and allow rules -- and "Hooks still execute and can block
9
+ // operations if needed" even when `permissionMode: 'bypassPermissions'` is set: "Deny rules
10
+ // (disallowed_tools), explicit ask rules, and hooks are evaluated before the mode check and can still
11
+ // block a tool." This module does not implement that guarantee -- the SDK does. This module's job is
12
+ // only to return a correctly-shaped, fail-closed deny decision every time; the SDK is what makes that
13
+ // decision unbypassable.
14
+ //
15
+ // FAIL CLOSED: any internal error (a malformed tool-call shape, a governor-ledger append failure) denies
16
+ // rather than silently allowing.
17
+
18
+ import { DEFAULT_DENY_RULES, evaluateDenyHooks } from "./deny-hooks.js";
19
+ import { appendGovernorEvent } from "./governor-ledger.js";
20
+
21
+ function recordDenial(append, repoFullName, reason, payload) {
22
+ try {
23
+ append({
24
+ eventType: "denied",
25
+ repoFullName: repoFullName ?? null,
26
+ actionClass: "pretooluse_hook",
27
+ decision: "deny",
28
+ reason,
29
+ payload,
30
+ });
31
+ } catch {
32
+ // A ledger append failure must never suppress or alter the deny decision itself -- the tool call is
33
+ // still blocked even if the audit write fails. Silently allowing on a logging failure would be a far
34
+ // worse outcome for a security boundary than an unrecorded (but still enforced) denial.
35
+ }
36
+ }
37
+
38
+ function denyOutput(reason) {
39
+ return {
40
+ hookSpecificOutput: {
41
+ hookEventName: "PreToolUse",
42
+ permissionDecision: "deny",
43
+ permissionDecisionReason: reason,
44
+ },
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Build a Claude Agent SDK `PreToolUse` hook callback enforcing the house-rule denylist. Register the
50
+ * returned function under `options.hooks.PreToolUse` (e.g. `{ hooks: [PreToolUse: [{ hooks: [built] }]] }`
51
+ * on the object passed to `createAgentSdkCodingAgentDriver({ hooks })`).
52
+ *
53
+ * House rules are sourced from a single, auditable list: {@link DEFAULT_DENY_RULES} by default, or an
54
+ * effective rule set built by the caller (e.g. `resolveEffectiveDenyRules` from deny-hook-synthesis.js,
55
+ * merging in maintainer-approved synthesized rules) — this module composes whatever rule set it is given,
56
+ * it does not own deriving one.
57
+ *
58
+ * @param {object} [config]
59
+ * @param {ReadonlyArray<import("./deny-hooks.js").DenyRule>} [config.rules] defaults to DEFAULT_DENY_RULES
60
+ * @param {string} [config.repoFullName] target repo, for governor-ledger scoping of recorded denials
61
+ * @param {{ append?: typeof appendGovernorEvent }} [options]
62
+ * @returns {(input: unknown, toolUseId?: string, context?: unknown) => Promise<Record<string, unknown>>}
63
+ */
64
+ export function buildHouseRulesPreToolUseHook(config = {}, options = {}) {
65
+ const rules = config.rules ?? DEFAULT_DENY_RULES;
66
+ const repoFullName = config.repoFullName;
67
+ const append = options.append ?? appendGovernorEvent;
68
+
69
+ return async function houseRulesPreToolUseHook(input) {
70
+ try {
71
+ const toolName = input && typeof input === "object" ? input.tool_name : undefined;
72
+ const toolInput = input && typeof input === "object" ? input.tool_input : undefined;
73
+ const verdict = evaluateDenyHooks({ name: toolName, input: toolInput }, rules);
74
+
75
+ if (verdict.allowed) return {};
76
+
77
+ // `verdict.blockedBy` is always set together with `!verdict.allowed` (evaluateDenyHooks's only two return
78
+ // shapes), and `.matcher` is always a defined string on it (ruleMatches gates every match on
79
+ // `typeof rule.matcher === "string"` -- a rule can never become `blockedBy` otherwise). `.reason` has no
80
+ // equivalent gate, so a caller-supplied custom rule omitting it is a real, reachable case.
81
+ const reason = verdict.blockedBy.reason ?? "House rule denylist match.";
82
+ recordDenial(append, repoFullName, reason, {
83
+ toolName: typeof toolName === "string" ? toolName : null,
84
+ matcher: verdict.blockedBy.matcher,
85
+ });
86
+ return denyOutput(reason);
87
+ } catch (error) {
88
+ const reason = `pretooluse_hook_internal_error: ${error instanceof Error ? error.message : String(error)}`;
89
+ recordDenial(append, repoFullName, reason, {});
90
+ return denyOutput(reason);
91
+ }
92
+ };
93
+ }
@@ -0,0 +1,31 @@
1
+ /** Process lifecycle / crash-safety for the miner CLI (#4826). Local stores register on open and the CLI installs
2
+ * signal/error handlers once at startup so an interrupted run closes every open ledger cleanly. */
3
+
4
+ /** A closable store (`{ close() }`) or a plain cleanup callback. */
5
+ export type CleanupResource = { close: () => void } | (() => void);
6
+
7
+ /** The subset of `process` the handlers use; injectable for tests. */
8
+ export type ProcessLike = {
9
+ on: (event: string, listener: (...args: unknown[]) => void) => unknown;
10
+ exit: (code?: number) => void;
11
+ };
12
+
13
+ export type InstallCliSignalHandlersOptions = {
14
+ process?: ProcessLike;
15
+ log?: (message: string) => void;
16
+ exit?: (code: number) => void;
17
+ /** Reinstall even if handlers were already installed (mainly for tests). */
18
+ force?: boolean;
19
+ };
20
+
21
+ /** Register a resource to close on exit; returns an idempotent unregister function. */
22
+ export function registerCleanupResource(resource: CleanupResource | null | undefined): () => void;
23
+
24
+ export function cleanupResourceCount(): number;
25
+
26
+ export function closeAllCleanupResources(options?: { onError?: (error: unknown) => void }): void;
27
+
28
+ /** Install signal + error handlers once. Returns false if already installed (and `force` was not set). */
29
+ export function installCliSignalHandlers(options?: InstallCliSignalHandlersOptions): boolean;
30
+
31
+ export function resetProcessLifecycleForTesting(): void;
@@ -0,0 +1,106 @@
1
+ /** Process lifecycle / crash-safety for the miner CLI (#4826). The CLI dispatches through a chain of bare
2
+ * `process.exit()` calls with no cleanup hook, so a SIGINT/SIGTERM mid-run — or an uncaught exception — used to
3
+ * kill the process mid-write, leaving whatever local SQLite ledger it was touching in an undefined state. This
4
+ * module is the single cleanup chokepoint: local stores register themselves when opened (see `local-store.js`), and
5
+ * `installCliSignalHandlers` (called once at CLI startup) flushes/closes every still-open resource before exiting
6
+ * cleanly on a signal, and logs + exits non-zero on an uncaught exception / unhandled rejection instead of crashing
7
+ * silently. Cleanup ONLY — no command business logic lives here. Every dependency (`process`, `log`, `exit`) is
8
+ * injectable so the handlers are unit-testable without actually signalling the test runner. */
9
+
10
+ // 128 + signal number, the conventional shell exit code for a process terminated by that signal (SIGINT=2 -> 130,
11
+ // SIGTERM=15 -> 143).
12
+ const SIGNAL_EXIT_CODES = Object.freeze({ SIGINT: 130, SIGTERM: 143 });
13
+
14
+ /** Resources to close on exit. A resource is either a `{ close() }` object (e.g. an open SQLite store) or a plain
15
+ * cleanup function. Held in insertion order so cleanup is deterministic. */
16
+ const cleanupResources = new Set();
17
+ let handlersInstalled = false;
18
+
19
+ /** Render any thrown value as a single log-safe string, preferring an Error's stack. */
20
+ function describeError(value) {
21
+ if (value instanceof Error) return value.stack ?? value.message;
22
+ return String(value);
23
+ }
24
+
25
+ /**
26
+ * Register a resource to be closed on clean exit or crash. Returns an idempotent unregister function (call it from
27
+ * the resource's own normal `close()` so a resource closed during the happy path is not double-closed at exit).
28
+ */
29
+ export function registerCleanupResource(resource) {
30
+ if (resource === null || resource === undefined) return () => {};
31
+ cleanupResources.add(resource);
32
+ return () => {
33
+ cleanupResources.delete(resource);
34
+ };
35
+ }
36
+
37
+ /** Number of currently-registered cleanup resources (exposed for tests / diagnostics). */
38
+ export function cleanupResourceCount() {
39
+ return cleanupResources.size;
40
+ }
41
+
42
+ /**
43
+ * Close every registered resource, swallowing each individual failure (a store that fails to close must not stop
44
+ * the others from closing) and reporting it via `options.onError`. Idempotent: the registry is emptied afterwards.
45
+ */
46
+ export function closeAllCleanupResources(options = {}) {
47
+ const onError = typeof options.onError === "function" ? options.onError : null;
48
+ for (const resource of [...cleanupResources]) {
49
+ try {
50
+ if (typeof resource === "function") resource();
51
+ else resource.close();
52
+ } catch (error) {
53
+ if (onError) onError(error);
54
+ }
55
+ }
56
+ cleanupResources.clear();
57
+ }
58
+
59
+ /**
60
+ * Install top-level signal + error handlers once. On SIGINT/SIGTERM: close all resources and exit with the
61
+ * conventional 128+signal code. On uncaughtException/unhandledRejection: log the error, close all resources, and
62
+ * exit non-zero. No-op (returns false) if already installed unless `options.force` is set. All of `process`, `log`,
63
+ * and `exit` are injectable for testing.
64
+ */
65
+ export function installCliSignalHandlers(options = {}) {
66
+ const proc = options.process ?? process;
67
+ const log = typeof options.log === "function" ? options.log : (message) => console.error(message);
68
+ const exit = typeof options.exit === "function" ? options.exit : (code) => proc.exit(code);
69
+
70
+ if (handlersInstalled && options.force !== true) return false;
71
+ handlersInstalled = true;
72
+
73
+ const runCleanup = () => {
74
+ closeAllCleanupResources({
75
+ onError: (error) => log(`gittensory-miner: cleanup error while exiting: ${describeError(error)}`),
76
+ });
77
+ };
78
+
79
+ for (const [signal, code] of Object.entries(SIGNAL_EXIT_CODES)) {
80
+ proc.on(signal, () => {
81
+ log(`gittensory-miner: received ${signal}, closing open resources and exiting.`);
82
+ runCleanup();
83
+ exit(code);
84
+ });
85
+ }
86
+
87
+ proc.on("uncaughtException", (error) => {
88
+ log(`gittensory-miner: uncaught exception: ${describeError(error)}`);
89
+ runCleanup();
90
+ exit(1);
91
+ });
92
+
93
+ proc.on("unhandledRejection", (reason) => {
94
+ log(`gittensory-miner: unhandled promise rejection: ${describeError(reason)}`);
95
+ runCleanup();
96
+ exit(1);
97
+ });
98
+
99
+ return true;
100
+ }
101
+
102
+ /** Test-only: clear the registry and the installed flag so each test starts from a clean lifecycle. */
103
+ export function resetProcessLifecycleForTesting() {
104
+ cleanupResources.clear();
105
+ handlersInstalled = false;
106
+ }
@@ -0,0 +1,44 @@
1
+ import type { ClaimLedger } from "./claim-ledger.js";
2
+ import type { EventLedger } from "./event-ledger.js";
3
+ import type { GovernorLedger } from "./governor-ledger.js";
4
+ import type { PredictionLedger } from "./prediction-ledger.js";
5
+
6
+ export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE: string;
7
+
8
+ export type ParsedPurgeArgs = { json: boolean; dryRun: boolean; repoFullName: string } | { error: string };
9
+
10
+ export function parsePurgeArgs(args: string[]): ParsedPurgeArgs;
11
+
12
+ export type PurgeStoreResult = { store: string; purged: number | null; error?: string; note?: string };
13
+ export type PurgeDryRunStoreResult = { store: string; wouldPurge: number | null; error?: string };
14
+
15
+ export type PurgeDryRunResult = {
16
+ outcome: "dry_run";
17
+ repoFullName: string;
18
+ stores: PurgeDryRunStoreResult[];
19
+ attemptLogNote: string;
20
+ attemptLogTotalRows: number;
21
+ };
22
+
23
+ export type PurgeSummary = {
24
+ outcome: "purged" | "partial";
25
+ repoFullName: string;
26
+ totalPurged: number;
27
+ stores: PurgeStoreResult[];
28
+ purgedAt: string;
29
+ };
30
+
31
+ export type PurgeCliOptions = {
32
+ openClaimLedger?: () => ClaimLedger;
33
+ initEventLedger?: () => EventLedger;
34
+ initGovernorLedger?: () => GovernorLedger;
35
+ initPredictionLedger?: () => PredictionLedger;
36
+ resolveDbPaths?: Record<string, () => string>;
37
+ };
38
+
39
+ export function runPurgeDryRun(
40
+ parsed: { repoFullName: string; json: boolean },
41
+ options?: PurgeCliOptions,
42
+ ): number;
43
+
44
+ export function runPurge(args: string[], options?: PurgeCliOptions): number;
@@ -0,0 +1,198 @@
1
+ // `gittensory-miner purge` (#5564): an explicit, operator-invoked right-to-be-forgotten path across the local
2
+ // ledgers. Deletes every row for one repo from the four stores that have a real `repoColumn` (claim-ledger,
3
+ // event-ledger, governor-ledger, prediction-ledger), via each store's own `purgeByRepo` method (which reuses
4
+ // `store-maintenance.js`'s shared, identifier-guarded `purgeStoreByRepo`). `attempt-log.js` is deliberately
5
+ // reported as not-purgeable rather than silently skipped or approximated: its payload is a free-form
6
+ // `Record<string, unknown>` with no dedicated repo column, so a precise per-repo match isn't possible there
7
+ // without risking false matches -- see store-maintenance.js's own purge-spec doc comment.
8
+ //
9
+ // Every purge is audit-observable by design (#5564's own acceptance criteria): the real (non-dry-run) path
10
+ // always prints a per-store summary, even under --json, so a purge can never be silent. A failure in one store
11
+ // does not prevent reporting what succeeded in the others -- see purgeOneStore's own per-store try/catch.
12
+ import { existsSync } from "node:fs";
13
+ import { DatabaseSync } from "node:sqlite";
14
+ import { openClaimLedger, resolveClaimLedgerDbPath } from "./claim-ledger.js";
15
+ import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js";
16
+ import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledger.js";
17
+ import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
18
+ import { resolveAttemptLogDbPath } from "./attempt-log.js";
19
+ import {
20
+ CLAIM_LEDGER_PURGE_SPEC,
21
+ EVENT_LEDGER_PURGE_SPEC,
22
+ GOVERNOR_LEDGER_PURGE_SPEC,
23
+ PREDICTION_LEDGER_PURGE_SPEC,
24
+ countStoreByRepo,
25
+ describeError,
26
+ } from "./store-maintenance.js";
27
+
28
+ const PURGE_USAGE = "Usage: gittensory-miner purge --repo <owner/repo> [--dry-run] [--json]";
29
+
30
+ export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE =
31
+ "attempt-log has no repoFullName column and cannot be purged by repo (#5564); its rows are unaffected";
32
+
33
+ const REAL_PURGE_TARGETS = [
34
+ { name: "claim-ledger", optionKey: "openClaimLedger", opener: openClaimLedger, resolveDbPath: resolveClaimLedgerDbPath, spec: CLAIM_LEDGER_PURGE_SPEC },
35
+ { name: "event-ledger", optionKey: "initEventLedger", opener: initEventLedger, resolveDbPath: resolveEventLedgerDbPath, spec: EVENT_LEDGER_PURGE_SPEC },
36
+ { name: "governor-ledger", optionKey: "initGovernorLedger", opener: initGovernorLedger, resolveDbPath: resolveGovernorLedgerDbPath, spec: GOVERNOR_LEDGER_PURGE_SPEC },
37
+ { name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC },
38
+ ];
39
+
40
+ function parseRepoArg(value, usage) {
41
+ if (!value) return { error: usage };
42
+ const trimmed = value.trim();
43
+ const [owner, repo, extra] = trimmed.split("/");
44
+ if (!owner || !repo || extra !== undefined) {
45
+ return { error: "Repository must be in owner/repo form." };
46
+ }
47
+ return { repoFullName: `${owner}/${repo}` };
48
+ }
49
+
50
+ export function parsePurgeArgs(args) {
51
+ const options = { json: false, dryRun: false, repoFullName: null };
52
+
53
+ for (let index = 0; index < args.length; index += 1) {
54
+ const token = args[index];
55
+ if (token === "--json") {
56
+ options.json = true;
57
+ continue;
58
+ }
59
+ if (token === "--dry-run") {
60
+ options.dryRun = true;
61
+ continue;
62
+ }
63
+ if (token === "--repo") {
64
+ const repoArg = args[index + 1];
65
+ // Only the flag-look-alike case is checked here ("--repo --json") -- a genuinely missing value (repoArg
66
+ // undefined) falls through to parseRepoArg's own `!value` guard below, the single source of truth for that.
67
+ if (repoArg !== undefined && repoArg.startsWith("-")) return { error: PURGE_USAGE };
68
+ const repo = parseRepoArg(repoArg, PURGE_USAGE);
69
+ if ("error" in repo) return repo;
70
+ options.repoFullName = repo.repoFullName;
71
+ index += 1;
72
+ continue;
73
+ }
74
+ return { error: `Unknown option: ${token}` };
75
+ }
76
+
77
+ if (!options.repoFullName) return { error: PURGE_USAGE };
78
+ return options;
79
+ }
80
+
81
+ /** Read-only row count against an on-disk store file, for --dry-run. `{ readOnly: true }` (camelCase) is the
82
+ * only option node:sqlite recognizes for a driver-enforced read-only connection -- the lowercase `readonly`
83
+ * key is silently ignored. Never touches a store that doesn't exist yet (opening one -- even read-only --
84
+ * requires the file to already be there; a dry run must make zero writes). */
85
+ function countExistingRows(dbPath, countFn) {
86
+ if (!existsSync(dbPath)) return 0;
87
+ const db = new DatabaseSync(dbPath, { readOnly: true });
88
+ try {
89
+ return countFn(db);
90
+ } finally {
91
+ db.close();
92
+ }
93
+ }
94
+
95
+ function renderDryRunSummary(result) {
96
+ const purgeableLine = result.stores
97
+ .map((entry) => `${entry.store}=${entry.wouldPurge}`)
98
+ .join(", ");
99
+ return [
100
+ `DRY RUN: would purge ${result.repoFullName} from: ${purgeableLine}. No writes were made.`,
101
+ `${ATTEMPT_LOG_NOT_PURGEABLE_NOTE} (${result.attemptLogTotalRows} total row(s) currently in attempt-log, all repos).`,
102
+ ].join("\n");
103
+ }
104
+
105
+ export function runPurgeDryRun(parsed, options = {}) {
106
+ const resolveDbPaths = options.resolveDbPaths ?? {};
107
+ const stores = REAL_PURGE_TARGETS.map((target) => {
108
+ const dbPath = (resolveDbPaths[target.name] ?? target.resolveDbPath)();
109
+ try {
110
+ const wouldPurge = countExistingRows(dbPath, (db) => countStoreByRepo(db, target.spec, parsed.repoFullName));
111
+ return { store: target.name, wouldPurge };
112
+ } catch (error) {
113
+ return { store: target.name, wouldPurge: null, error: describeError(error) };
114
+ }
115
+ });
116
+
117
+ const attemptLogDbPath = (resolveDbPaths["attempt-log"] ?? resolveAttemptLogDbPath)();
118
+ const attemptLogTotalRows = countExistingRows(attemptLogDbPath, (db) =>
119
+ Number(db.prepare("SELECT COUNT(*) AS count FROM attempt_log_events").get().count),
120
+ );
121
+
122
+ const result = {
123
+ outcome: "dry_run",
124
+ repoFullName: parsed.repoFullName,
125
+ stores,
126
+ attemptLogNote: ATTEMPT_LOG_NOT_PURGEABLE_NOTE,
127
+ attemptLogTotalRows,
128
+ };
129
+
130
+ if (parsed.json) {
131
+ console.log(JSON.stringify(result, null, 2));
132
+ } else {
133
+ console.log(renderDryRunSummary(result));
134
+ }
135
+ return 0;
136
+ }
137
+
138
+ function purgeOneStore(target, options, repoFullName) {
139
+ const ownsStore = options[target.optionKey] === undefined;
140
+ let store;
141
+ try {
142
+ store = (options[target.optionKey] ?? target.opener)();
143
+ const purged = store.purgeByRepo(repoFullName);
144
+ return { store: target.name, purged };
145
+ } catch (error) {
146
+ return { store: target.name, purged: null, error: describeError(error) };
147
+ } finally {
148
+ if (ownsStore) store?.close();
149
+ }
150
+ }
151
+
152
+ function renderPurgeSummary(summary) {
153
+ const perStore = summary.stores
154
+ .map((entry) => {
155
+ if ("error" in entry) return `${entry.store}=ERROR(${entry.error})`;
156
+ if (entry.purged === null) return `${entry.store}=skipped`;
157
+ return `${entry.store}=${entry.purged}`;
158
+ })
159
+ .join(", ");
160
+ return [
161
+ `Purged ${summary.totalPurged} row(s) for ${summary.repoFullName} at ${summary.purgedAt}: ${perStore}.`,
162
+ ATTEMPT_LOG_NOT_PURGEABLE_NOTE,
163
+ ].join(" ");
164
+ }
165
+
166
+ export function runPurge(args, options = {}) {
167
+ const parsed = parsePurgeArgs(args);
168
+ if ("error" in parsed) {
169
+ console.error(parsed.error);
170
+ return 2;
171
+ }
172
+
173
+ if (parsed.dryRun) {
174
+ return runPurgeDryRun(parsed, options);
175
+ }
176
+
177
+ const perStoreResults = REAL_PURGE_TARGETS.map((target) => purgeOneStore(target, options, parsed.repoFullName));
178
+ perStoreResults.push({ store: "attempt-log", purged: null, note: ATTEMPT_LOG_NOT_PURGEABLE_NOTE });
179
+
180
+ const totalPurged = perStoreResults.reduce((sum, entry) => sum + (entry.purged ?? 0), 0);
181
+ const hadError = perStoreResults.some((entry) => "error" in entry);
182
+ const summary = {
183
+ outcome: hadError ? "partial" : "purged",
184
+ repoFullName: parsed.repoFullName,
185
+ totalPurged,
186
+ stores: perStoreResults,
187
+ purgedAt: new Date().toISOString(),
188
+ };
189
+
190
+ // Audit-observable by design (#5564): print the summary in BOTH the success and partial-failure case, so a
191
+ // purge -- or a purge that only partly succeeded -- is never silent.
192
+ if (parsed.json) {
193
+ console.log(JSON.stringify(summary, null, 2));
194
+ } else {
195
+ console.log(renderPurgeSummary(summary));
196
+ }
197
+ return hadError ? 2 : 0;
198
+ }
@@ -0,0 +1,6 @@
1
+ import type { SelfReviewContextFetch } from "./self-review-context.js";
2
+
3
+ export function resolveRejectionSignaled(
4
+ repoFullName: string,
5
+ options?: { rawContentBaseUrl?: string; fetchImpl?: SelfReviewContextFetch },
6
+ ): Promise<boolean>;
@@ -0,0 +1,101 @@
1
+ import { resolveAiPolicyVerdict } from "@loopover/engine";
2
+
3
+ // Real rejectionSignaled resolver (#5132, Wave 3.5 follow-up). iterate-policy.ts's own doc comment: "True
4
+ // when the target repo (or this contributor's history with it) has signaled it does not want automated/
5
+ // AI-authored contributions -- an explicit AI-usage-policy ban, or a prior submission from this same miner
6
+ // was closed/rejected on this exact repo. The caller resolves this ... and passes it in; this policy does
7
+ // not compute it itself." This module resolves the FIRST trigger: a real AI-USAGE.md/CONTRIBUTING.md ban,
8
+ // fetched live and scanned via the engine's own resolveAiPolicyVerdict -- the same check
9
+ // opportunity-fanout.js already runs during discovery, applied here at attempt time instead.
10
+ //
11
+ // The SECOND trigger (a prior submission from this same miner was closed/rejected on this exact repo) is
12
+ // DELIBERATELY not resolved here: it would need each of this miner's recorded own-submissions
13
+ // (governor-state.js's listRecentOwnSubmissions, #5134) checked against its live PR outcome via
14
+ // rejection-state-machine.js's resolveRejection -- a second, separately-scoped fetch-and-classify pipeline.
15
+ // Not fabricated as "no rejection history" -- explicitly left as a known, documented gap for a follow-up,
16
+ // same discipline as SelfReviewContext's bounties/issueQuality (#5145) and this file's own callers should
17
+ // not assume a false result here means "no rejection signal of any kind."
18
+
19
+ const DEFAULT_RAW_CONTENT_BASE_URL = "https://raw.githubusercontent.com";
20
+ const MAX_POLICY_DOC_BYTES = 128 * 1024;
21
+
22
+ function parseRepoFullName(repoFullName) {
23
+ if (typeof repoFullName !== "string") return null;
24
+ const [owner, repo, extra] = repoFullName.split("/");
25
+ if (!owner || !repo || extra !== undefined) return null;
26
+ return { owner, repo };
27
+ }
28
+
29
+ function normalizeOptions(options = {}) {
30
+ return {
31
+ rawContentBaseUrl:
32
+ typeof options.rawContentBaseUrl === "string" && options.rawContentBaseUrl.trim() ? options.rawContentBaseUrl.trim() : DEFAULT_RAW_CONTENT_BASE_URL,
33
+ fetchImpl: options.fetchImpl ?? fetch,
34
+ };
35
+ }
36
+
37
+ async function readBoundedPolicyDoc(response) {
38
+ const contentLength = response.headers?.get?.("content-length");
39
+ if (contentLength !== undefined && contentLength !== null) {
40
+ const parsedLength = Number.parseInt(contentLength, 10);
41
+ if (Number.isFinite(parsedLength) && parsedLength > MAX_POLICY_DOC_BYTES) return null;
42
+ }
43
+
44
+ if (!response.body?.getReader) {
45
+ const text = await response.text();
46
+ return typeof text === "string" && Buffer.byteLength(text, "utf8") <= MAX_POLICY_DOC_BYTES ? text : null;
47
+ }
48
+
49
+ const reader = response.body.getReader();
50
+ const decoder = new TextDecoder();
51
+ let totalBytes = 0;
52
+ let text = "";
53
+ try {
54
+ while (true) {
55
+ const { done, value } = await reader.read();
56
+ if (done) break;
57
+ totalBytes += value.byteLength;
58
+ if (totalBytes > MAX_POLICY_DOC_BYTES) {
59
+ await reader.cancel();
60
+ return null;
61
+ }
62
+ text += decoder.decode(value, { stream: true });
63
+ }
64
+ text += decoder.decode();
65
+ return text;
66
+ } finally {
67
+ reader.releaseLock();
68
+ }
69
+ }
70
+
71
+ async function fetchPolicyDoc(target, path, resolved) {
72
+ const url = `${resolved.rawContentBaseUrl}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/HEAD/${path}`;
73
+ try {
74
+ const response = await resolved.fetchImpl(url, { method: "GET", headers: { accept: "application/json", "user-agent": "loopover-miner" } });
75
+ if (!response.ok) return null;
76
+ return await readBoundedPolicyDoc(response);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Resolve whether the target repo has an explicit, live AI-usage-policy ban -- the first of
84
+ * `rejectionSignaled`'s two documented triggers. Returns `false` (never throws) on any fetch/parse failure,
85
+ * matching resolveAiPolicyVerdict's own fail-open default for an absent/unreadable policy doc.
86
+ *
87
+ * @param {string} repoFullName
88
+ * @param {{ rawContentBaseUrl?: string, fetchImpl?: import("./self-review-context.js").SelfReviewContextFetch }} [options]
89
+ * @returns {Promise<boolean>}
90
+ */
91
+ export async function resolveRejectionSignaled(repoFullName, options = {}) {
92
+ const target = parseRepoFullName(repoFullName);
93
+ if (!target) return false;
94
+ const resolved = normalizeOptions(options);
95
+
96
+ const aiUsage = await fetchPolicyDoc(target, "AI-USAGE.md", resolved);
97
+ const contributing = aiUsage && aiUsage.trim() ? null : await fetchPolicyDoc(target, "CONTRIBUTING.md", resolved);
98
+
99
+ const verdict = resolveAiPolicyVerdict({ aiUsage, contributing });
100
+ return !verdict.allowed;
101
+ }
@@ -0,0 +1,37 @@
1
+ import type { RejectionReason, RejectionContext } from "./rejection-templates.js";
2
+
3
+ export type PrOutcomeFields = {
4
+ state: string | null;
5
+ merged: boolean;
6
+ mergedAt: string | null;
7
+ closedAt: string | null;
8
+ };
9
+
10
+ export type RejectionSignal = {
11
+ gateClosed?: boolean;
12
+ supersededByDuplicate?: boolean;
13
+ };
14
+
15
+ export type RejectionTransition = {
16
+ outcome: "disengaged";
17
+ reason: RejectionReason;
18
+ note: string;
19
+ fields: PrOutcomeFields;
20
+ };
21
+
22
+ /** Per-PR terminal outcome for a rejected (closed-without-merge) PR. */
23
+ export const DISENGAGED_OUTCOME: "disengaged";
24
+
25
+ export function extractPrOutcomeFields(prPayload: unknown): PrOutcomeFields;
26
+
27
+ export function isRejectedPr(
28
+ fields: { state?: string | null; merged?: boolean } | null | undefined,
29
+ ): boolean;
30
+
31
+ export function classifyRejectionReason(signal?: RejectionSignal): RejectionReason;
32
+
33
+ export function resolveRejection(
34
+ prPayload: unknown,
35
+ signal: RejectionSignal | undefined,
36
+ context: RejectionContext,
37
+ ): RejectionTransition | null;