@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.
- package/DEPLOYMENT.md +156 -0
- package/Dockerfile +38 -0
- package/README.md +269 -0
- package/bin/gittensory-miner-mcp.d.ts +64 -0
- package/bin/gittensory-miner-mcp.js +279 -0
- package/bin/gittensory-miner.js +196 -0
- package/docs/coding-agent-driver.md +140 -0
- package/docs/config-precedence.md +96 -0
- package/docs/cross-repo-discovery-phase1.md +55 -0
- package/docs/discovery-index-contract.md +60 -0
- package/docs/discovery-plane-operator-guide.md +101 -0
- package/docs/env-reference.md +30 -0
- package/docs/fleet-run-manifest.md +50 -0
- package/docs/miner-goal-spec.md +76 -0
- package/docs/miner-selfimprove-calibration.md +105 -0
- package/docs/observability.md +60 -0
- package/docs/operations-runbook.md +209 -0
- package/docs/repo-agnostic-capability-audit.md +129 -0
- package/docs/unattended-scheduling.md +107 -0
- package/expected-engine.version +1 -0
- package/lib/ams-policy.d.ts +20 -0
- package/lib/ams-policy.js +68 -0
- package/lib/attempt-cli.d.ts +99 -0
- package/lib/attempt-cli.js +523 -0
- package/lib/attempt-input-builder.d.ts +24 -0
- package/lib/attempt-input-builder.js +91 -0
- package/lib/attempt-log.d.ts +37 -0
- package/lib/attempt-log.js +177 -0
- package/lib/attempt-runner.d.ts +59 -0
- package/lib/attempt-runner.js +164 -0
- package/lib/attempt-worktree.d.ts +31 -0
- package/lib/attempt-worktree.js +94 -0
- package/lib/calibration-cli.d.ts +1 -0
- package/lib/calibration-cli.js +92 -0
- package/lib/calibration-run.d.ts +161 -0
- package/lib/calibration-run.js +232 -0
- package/lib/calibration-types.d.ts +46 -0
- package/lib/calibration-types.js +74 -0
- package/lib/calibration.d.ts +24 -0
- package/lib/calibration.js +99 -0
- package/lib/ci-poller.d.ts +33 -0
- package/lib/ci-poller.js +234 -0
- package/lib/claim-adjudication.d.ts +21 -0
- package/lib/claim-adjudication.js +36 -0
- package/lib/claim-conflict-resolver.d.ts +29 -0
- package/lib/claim-conflict-resolver.js +101 -0
- package/lib/claim-ledger-cli.d.ts +59 -0
- package/lib/claim-ledger-cli.js +320 -0
- package/lib/claim-ledger-expiry.d.ts +20 -0
- package/lib/claim-ledger-expiry.js +41 -0
- package/lib/claim-ledger.d.ts +63 -0
- package/lib/claim-ledger.js +303 -0
- package/lib/cli-error.d.ts +3 -0
- package/lib/cli-error.js +27 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +72 -0
- package/lib/coding-agent-construction.d.ts +17 -0
- package/lib/coding-agent-construction.js +103 -0
- package/lib/coding-agent-house-rules.d.ts +24 -0
- package/lib/coding-agent-house-rules.js +64 -0
- package/lib/coding-task-spec.d.ts +44 -0
- package/lib/coding-task-spec.js +183 -0
- package/lib/config-precedence.d.ts +25 -0
- package/lib/config-precedence.js +38 -0
- package/lib/deny-check.d.ts +11 -0
- package/lib/deny-check.js +76 -0
- package/lib/deny-hook-synthesis.d.ts +93 -0
- package/lib/deny-hook-synthesis.js +399 -0
- package/lib/deny-hooks.d.ts +28 -0
- package/lib/deny-hooks.js +155 -0
- package/lib/deployment-docs-audit.d.ts +41 -0
- package/lib/deployment-docs-audit.js +115 -0
- package/lib/discover-cli.d.ts +94 -0
- package/lib/discover-cli.js +284 -0
- package/lib/discovery-throttle.d.ts +8 -0
- package/lib/discovery-throttle.js +33 -0
- package/lib/env-file-indirection.d.ts +4 -0
- package/lib/env-file-indirection.js +45 -0
- package/lib/event-ledger-cli.d.ts +82 -0
- package/lib/event-ledger-cli.js +266 -0
- package/lib/event-ledger.d.ts +37 -0
- package/lib/event-ledger.js +210 -0
- package/lib/execute-local-write.d.ts +14 -0
- package/lib/execute-local-write.js +50 -0
- package/lib/feasibility-cli.d.ts +25 -0
- package/lib/feasibility-cli.js +80 -0
- package/lib/forge-config.d.ts +17 -0
- package/lib/forge-config.js +37 -0
- package/lib/governor-action-mode.d.ts +26 -0
- package/lib/governor-action-mode.js +47 -0
- package/lib/governor-chokepoint-persisted.d.ts +18 -0
- package/lib/governor-chokepoint-persisted.js +46 -0
- package/lib/governor-chokepoint.d.ts +14 -0
- package/lib/governor-chokepoint.js +52 -0
- package/lib/governor-kill-switch.d.ts +26 -0
- package/lib/governor-kill-switch.js +47 -0
- package/lib/governor-ledger-cli.d.ts +32 -0
- package/lib/governor-ledger-cli.js +155 -0
- package/lib/governor-ledger.d.ts +46 -0
- package/lib/governor-ledger.js +203 -0
- package/lib/governor-open-pr.d.ts +21 -0
- package/lib/governor-open-pr.js +27 -0
- package/lib/governor-pause-cli.d.ts +23 -0
- package/lib/governor-pause-cli.js +171 -0
- package/lib/governor-run-halt.d.ts +37 -0
- package/lib/governor-run-halt.js +58 -0
- package/lib/governor-state.d.ts +63 -0
- package/lib/governor-state.js +363 -0
- package/lib/governor-write-rate-limit.d.ts +30 -0
- package/lib/governor-write-rate-limit.js +64 -0
- package/lib/harness-submission-trigger.d.ts +69 -0
- package/lib/harness-submission-trigger.js +138 -0
- package/lib/http-retry.d.ts +12 -0
- package/lib/http-retry.js +52 -0
- package/lib/laptop-init.d.ts +53 -0
- package/lib/laptop-init.js +332 -0
- package/lib/live-issue-snapshot.d.ts +16 -0
- package/lib/live-issue-snapshot.js +117 -0
- package/lib/local-store.d.ts +18 -0
- package/lib/local-store.js +62 -0
- package/lib/logger.d.ts +58 -0
- package/lib/logger.js +167 -0
- package/lib/loop-cli.d.ts +66 -0
- package/lib/loop-cli.js +540 -0
- package/lib/loop-closure.d.ts +35 -0
- package/lib/loop-closure.js +66 -0
- package/lib/loop-reentry.d.ts +51 -0
- package/lib/loop-reentry.js +124 -0
- package/lib/manage-poll.d.ts +79 -0
- package/lib/manage-poll.js +254 -0
- package/lib/manage-status.d.ts +70 -0
- package/lib/manage-status.js +241 -0
- package/lib/metrics-cli.d.ts +9 -0
- package/lib/metrics-cli.js +50 -0
- package/lib/migrate-cli.d.ts +24 -0
- package/lib/migrate-cli.js +111 -0
- package/lib/miner-goal-spec.d.ts +12 -0
- package/lib/miner-goal-spec.js +67 -0
- package/lib/opportunity-fanout.d.ts +86 -0
- package/lib/opportunity-fanout.js +597 -0
- package/lib/opportunity-ranker.d.ts +36 -0
- package/lib/opportunity-ranker.js +118 -0
- package/lib/orb-export.d.ts +54 -0
- package/lib/orb-export.js +206 -0
- package/lib/plan-store-cli.d.ts +37 -0
- package/lib/plan-store-cli.js +151 -0
- package/lib/plan-store.d.ts +51 -0
- package/lib/plan-store.js +231 -0
- package/lib/policy-doc-cache.d.ts +25 -0
- package/lib/policy-doc-cache.js +79 -0
- package/lib/policy-verdict-cache.d.ts +35 -0
- package/lib/policy-verdict-cache.js +106 -0
- package/lib/portfolio-dashboard.d.ts +30 -0
- package/lib/portfolio-dashboard.js +105 -0
- package/lib/portfolio-discovery.d.ts +29 -0
- package/lib/portfolio-discovery.js +100 -0
- package/lib/portfolio-queue-cli.d.ts +87 -0
- package/lib/portfolio-queue-cli.js +509 -0
- package/lib/portfolio-queue-expiry.d.ts +20 -0
- package/lib/portfolio-queue-expiry.js +51 -0
- package/lib/portfolio-queue-manager.d.ts +50 -0
- package/lib/portfolio-queue-manager.js +134 -0
- package/lib/portfolio-queue.d.ts +62 -0
- package/lib/portfolio-queue.js +351 -0
- package/lib/pr-disposition-poller.d.ts +26 -0
- package/lib/pr-disposition-poller.js +163 -0
- package/lib/pr-number-parse.d.ts +4 -0
- package/lib/pr-number-parse.js +23 -0
- package/lib/pr-outcome.d.ts +41 -0
- package/lib/pr-outcome.js +90 -0
- package/lib/prediction-ledger.d.ts +47 -0
- package/lib/prediction-ledger.js +222 -0
- package/lib/pretooluse-hook.d.ts +31 -0
- package/lib/pretooluse-hook.js +93 -0
- package/lib/process-lifecycle.d.ts +31 -0
- package/lib/process-lifecycle.js +106 -0
- package/lib/purge-cli.d.ts +44 -0
- package/lib/purge-cli.js +198 -0
- package/lib/rejection-signal.d.ts +6 -0
- package/lib/rejection-signal.js +101 -0
- package/lib/rejection-state-machine.d.ts +37 -0
- package/lib/rejection-state-machine.js +81 -0
- package/lib/rejection-templates.d.ts +12 -0
- package/lib/rejection-templates.js +71 -0
- package/lib/replay-objective-anchor.d.ts +79 -0
- package/lib/replay-objective-anchor.js +179 -0
- package/lib/replay-snapshot.d.ts +41 -0
- package/lib/replay-snapshot.js +288 -0
- package/lib/replay-task-generation.d.ts +126 -0
- package/lib/replay-task-generation.js +223 -0
- package/lib/repo-clone.d.ts +19 -0
- package/lib/repo-clone.js +102 -0
- package/lib/run-state-cli.d.ts +27 -0
- package/lib/run-state-cli.js +154 -0
- package/lib/run-state.d.ts +37 -0
- package/lib/run-state.js +150 -0
- package/lib/schema-version.d.ts +17 -0
- package/lib/schema-version.js +71 -0
- package/lib/self-review-context.d.ts +28 -0
- package/lib/self-review-context.js +339 -0
- package/lib/slop-assessment.d.ts +3 -0
- package/lib/slop-assessment.js +20 -0
- package/lib/stack-detection.d.ts +41 -0
- package/lib/stack-detection.js +248 -0
- package/lib/status.d.ts +64 -0
- package/lib/status.js +441 -0
- package/lib/store-maintenance.d.ts +31 -0
- package/lib/store-maintenance.js +183 -0
- package/lib/submission-freshness-check.d.ts +32 -0
- package/lib/submission-freshness-check.js +93 -0
- package/lib/update-check.d.ts +36 -0
- package/lib/update-check.js +161 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.js +10 -0
- package/lib/worktree-allocator.d.ts +39 -0
- package/lib/worktree-allocator.js +262 -0
- package/package.json +50 -0
- package/schema/miner-goal-spec.schema.json +111 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const LOOP_REENTRY_DECISION_EVENT: "loop_reentry_decision";
|
|
2
|
+
|
|
3
|
+
export type LoopReentryOutcome = "merged" | "disengaged" | "other";
|
|
4
|
+
export type LoopReentryKillSwitchScope = "global" | "repo" | "none";
|
|
5
|
+
|
|
6
|
+
export type LoopReentryCandidateInput = {
|
|
7
|
+
/** Checked FIRST by the pure `shouldReenter` policy, before any other logic. */
|
|
8
|
+
killSwitchScope: LoopReentryKillSwitchScope;
|
|
9
|
+
repoFullName: string;
|
|
10
|
+
outcome: LoopReentryOutcome;
|
|
11
|
+
maxConsecutiveDisengagements?: number;
|
|
12
|
+
maxReentriesPerHour?: number;
|
|
13
|
+
maxReentriesPerSession?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface LoopReentryEventLedger {
|
|
17
|
+
appendEvent(event: { type: string; repoFullName?: string; payload: Record<string, unknown> }): { id: number; seq: number; type: string; repoFullName: string | null; payload: Record<string, unknown>; createdAt: string };
|
|
18
|
+
readEvents(filter?: { since?: number; repoFullName?: string }): Array<{ type: string; repoFullName?: string | null; payload?: Record<string, unknown>; createdAt: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LoopReentryPortfolioQueue {
|
|
22
|
+
dequeueNext(): { repoFullName: string; identifier: string; priority: number; status: string; enqueuedAt: string } | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LoopReentryRunState {
|
|
26
|
+
setRunState(repoFullName: string, state: string): unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type LoopReentryDeps = {
|
|
30
|
+
eventLedger: LoopReentryEventLedger;
|
|
31
|
+
portfolioQueue: LoopReentryPortfolioQueue;
|
|
32
|
+
runState?: LoopReentryRunState;
|
|
33
|
+
nowMs?: number;
|
|
34
|
+
sessionStartMs?: number;
|
|
35
|
+
/** The just-completed cycle's read-only summary (loop-closure.js's `buildLoopClosureSummary`), threaded
|
|
36
|
+
* through verbatim into the audit event's payload for traceability. Not used to compute the circuit-
|
|
37
|
+
* breaker/rate-cap tallies -- see loop-reentry.js's own comment on why. */
|
|
38
|
+
loopSummary?: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type LoopReentryResult = {
|
|
42
|
+
decision: { reenter: boolean; reasons: string[] };
|
|
43
|
+
dequeued: { repoFullName: string; identifier: string; priority: number; status: string; enqueuedAt: string } | null;
|
|
44
|
+
event: { id: number; seq: number; type: string; repoFullName: string | null; payload: Record<string, unknown>; createdAt: string };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function countConsecutiveDisengagements(eventLedger: LoopReentryEventLedger, repoFullName: string): number;
|
|
48
|
+
|
|
49
|
+
export function countReentriesSince(eventLedger: LoopReentryEventLedger, sinceMs: number): number;
|
|
50
|
+
|
|
51
|
+
export function attemptLoopReentry(candidate: LoopReentryCandidateInput, deps: LoopReentryDeps): LoopReentryResult;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { shouldReenter } from "@loopover/engine";
|
|
2
|
+
|
|
3
|
+
import { readPrOutcomes } from "./pr-outcome.js";
|
|
4
|
+
|
|
5
|
+
// Closed-loop discovery re-entry orchestrator (#2338): the real-IO half of "on a resolved outcome (merged, or
|
|
6
|
+
// rejected-and-disengaged), automatically re-invoke discovery to select the next candidate." The DECISION
|
|
7
|
+
// itself (shouldReenter, @loopover/engine) is pure; this module owns everything that decision
|
|
8
|
+
// needs real state for -- reading the repo's own pr_outcome history to compute the per-repo consecutive-
|
|
9
|
+
// disengagement tally, reading recent re-entry events for the hourly/session rate cap, and (only when allowed)
|
|
10
|
+
// actually dequeuing the next candidate and transitioning run-state.
|
|
11
|
+
//
|
|
12
|
+
// NOT WIRED INTO ANY AUTOMATIC SCHEDULE: per this issue's own "manual owner sign-off before enabling by
|
|
13
|
+
// default in any profile" deliverable, this is a callable function ready for that sign-off -- it is not invoked
|
|
14
|
+
// by manage-poll.js or any cron/scheduler as part of this change.
|
|
15
|
+
//
|
|
16
|
+
// AUDITABILITY: every call appends exactly one `loop_reentry_decision` event to the ledger, whether or not the
|
|
17
|
+
// decision allowed re-entry, so the full decision trail (including every suppressed re-entry and why) survives
|
|
18
|
+
// independently of this function's own return value.
|
|
19
|
+
|
|
20
|
+
export const LOOP_REENTRY_DECISION_EVENT = "loop_reentry_decision";
|
|
21
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
/** A `pr_outcome` "closed" decision is this module's practical proxy for "disengaged" -- pr-outcome.js's own
|
|
24
|
+
* vocabulary is exactly `"merged" | "closed"` (no separate "disengaged" literal); a PR that closed without
|
|
25
|
+
* merging IS the rejected/disengaged case rejection-state-machine.js's own `isRejectedPr` checks for. */
|
|
26
|
+
function isDisengagedOutcome(outcome) {
|
|
27
|
+
return outcome?.decision === "closed";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Count a repo's CONSECUTIVE disengaged (closed-without-merge) PR outcomes, walking backward from the most
|
|
32
|
+
* recently recorded PR for that repo until a merged outcome breaks the streak (or history runs out).
|
|
33
|
+
*/
|
|
34
|
+
export function countConsecutiveDisengagements(eventLedger, repoFullName) {
|
|
35
|
+
const outcomes = [...readPrOutcomes(eventLedger, { repoFullName }).values()];
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (let i = outcomes.length - 1; i >= 0; i -= 1) {
|
|
38
|
+
if (!isDisengagedOutcome(outcomes[i])) break;
|
|
39
|
+
count += 1;
|
|
40
|
+
}
|
|
41
|
+
return count;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Count prior re-entries (successful, i.e. `reentered: true`) recorded at or after `sinceMs`. */
|
|
45
|
+
export function countReentriesSince(eventLedger, sinceMs) {
|
|
46
|
+
return eventLedger
|
|
47
|
+
.readEvents({})
|
|
48
|
+
.filter((event) => event.type === LOOP_REENTRY_DECISION_EVENT && event.payload?.reentered === true && Date.parse(event.createdAt) >= sinceMs)
|
|
49
|
+
.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Evaluate and (if allowed) PERFORM re-entry for one resolved outcome: reads real history to compute the
|
|
54
|
+
* circuit-breaker and rate-cap tallies, consults the pure `shouldReenter` policy, and -- only when it allows --
|
|
55
|
+
* dequeues the next candidate and transitions run-state to `"discovering"`. Always appends exactly one audit
|
|
56
|
+
* event. Fails closed (throws) on a malformed candidate or missing required dependency, mirroring
|
|
57
|
+
* `recordManagePollSnapshot`'s own validation style.
|
|
58
|
+
*
|
|
59
|
+
* @param {{ killSwitchScope: "global"|"repo"|"none", repoFullName: string, outcome: "merged"|"disengaged"|"other", maxConsecutiveDisengagements?: number, maxReentriesPerHour?: number, maxReentriesPerSession?: number }} candidate
|
|
60
|
+
* @param {{ eventLedger: object, portfolioQueue: object, runState?: object, nowMs?: number, sessionStartMs?: number }} deps
|
|
61
|
+
*/
|
|
62
|
+
export function attemptLoopReentry(candidate, deps) {
|
|
63
|
+
if (!candidate || typeof candidate !== "object") throw new Error("invalid_loop_reentry_candidate");
|
|
64
|
+
if (!["global", "repo", "none"].includes(candidate.killSwitchScope)) throw new Error("invalid_kill_switch_scope");
|
|
65
|
+
const repoFullName = typeof candidate.repoFullName === "string" ? candidate.repoFullName.trim() : "";
|
|
66
|
+
if (!repoFullName) throw new Error("invalid_repo_full_name");
|
|
67
|
+
if (!["merged", "disengaged", "other"].includes(candidate.outcome)) throw new Error("invalid_outcome");
|
|
68
|
+
|
|
69
|
+
if (!deps || typeof deps !== "object") throw new Error("invalid_loop_reentry_deps");
|
|
70
|
+
const { eventLedger, portfolioQueue, runState, nowMs = Date.now(), sessionStartMs = 0 } = deps;
|
|
71
|
+
if (!eventLedger || typeof eventLedger.appendEvent !== "function" || typeof eventLedger.readEvents !== "function") {
|
|
72
|
+
throw new Error("invalid_event_ledger");
|
|
73
|
+
}
|
|
74
|
+
if (!portfolioQueue || typeof portfolioQueue.dequeueNext !== "function") {
|
|
75
|
+
throw new Error("invalid_portfolio_queue");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const consecutiveDisengagements = countConsecutiveDisengagements(eventLedger, repoFullName);
|
|
79
|
+
const reentriesThisHour = countReentriesSince(eventLedger, nowMs - HOUR_MS);
|
|
80
|
+
const reentriesThisSession = countReentriesSince(eventLedger, sessionStartMs);
|
|
81
|
+
|
|
82
|
+
const decision = shouldReenter({
|
|
83
|
+
killSwitchScope: candidate.killSwitchScope,
|
|
84
|
+
repoFullName,
|
|
85
|
+
outcome: candidate.outcome,
|
|
86
|
+
consecutiveDisengagements,
|
|
87
|
+
maxConsecutiveDisengagements: candidate.maxConsecutiveDisengagements,
|
|
88
|
+
reentriesThisHour,
|
|
89
|
+
maxReentriesPerHour: candidate.maxReentriesPerHour,
|
|
90
|
+
reentriesThisSession,
|
|
91
|
+
maxReentriesPerSession: candidate.maxReentriesPerSession,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let dequeued = null;
|
|
95
|
+
if (decision.reenter) {
|
|
96
|
+
dequeued = portfolioQueue.dequeueNext();
|
|
97
|
+
if (runState && typeof runState.setRunState === "function") {
|
|
98
|
+
runState.setRunState(repoFullName, "discovering");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const event = eventLedger.appendEvent({
|
|
103
|
+
type: LOOP_REENTRY_DECISION_EVENT,
|
|
104
|
+
repoFullName,
|
|
105
|
+
payload: {
|
|
106
|
+
killSwitchScope: candidate.killSwitchScope,
|
|
107
|
+
outcome: candidate.outcome,
|
|
108
|
+
reentered: decision.reenter,
|
|
109
|
+
reasons: decision.reasons,
|
|
110
|
+
consecutiveDisengagements,
|
|
111
|
+
reentriesThisHour,
|
|
112
|
+
reentriesThisSession,
|
|
113
|
+
dequeuedIdentifier: dequeued ? dequeued.identifier : null,
|
|
114
|
+
// The just-completed cycle's read-only summary (loop-closure.js's buildLoopClosureSummary), when the
|
|
115
|
+
// caller supplies one -- threaded through verbatim for audit traceability. Optional: the circuit-breaker
|
|
116
|
+
// and rate-cap tallies above are computed directly from pr-outcome/event-ledger history (a
|
|
117
|
+
// LoopClosureSummary's own byType COUNTS aren't detailed enough to derive a per-repo consecutive-
|
|
118
|
+
// disengagement streak from), so this is context, not a computational input.
|
|
119
|
+
loopSummary: deps.loopSummary ?? null,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { decision, dequeued, event };
|
|
124
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { PollCheckRunsOptions, PollCheckRunsResult } from "./ci-poller.js";
|
|
2
|
+
import type { EventLedger, LedgerEntry } from "./event-ledger.js";
|
|
3
|
+
import type { PortfolioQueueStore } from "./portfolio-queue.js";
|
|
4
|
+
|
|
5
|
+
export type ManagePollInput = {
|
|
6
|
+
repoFullName: string;
|
|
7
|
+
prNumber: number;
|
|
8
|
+
branch?: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ManagePollEventPayload = {
|
|
12
|
+
prNumber: number;
|
|
13
|
+
branch: string | null;
|
|
14
|
+
ciState: PollCheckRunsResult["conclusion"];
|
|
15
|
+
gateVerdict: string;
|
|
16
|
+
outcome: string;
|
|
17
|
+
lastPolledAt: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ManagePollRecordResult = {
|
|
21
|
+
pollResult: PollCheckRunsResult;
|
|
22
|
+
payload: ManagePollEventPayload;
|
|
23
|
+
event: LedgerEntry;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ParsedManagePollArgs =
|
|
27
|
+
| {
|
|
28
|
+
repoFullName: string;
|
|
29
|
+
prNumber: number;
|
|
30
|
+
branch: string | null;
|
|
31
|
+
dryRun: boolean;
|
|
32
|
+
json: boolean;
|
|
33
|
+
}
|
|
34
|
+
| { error: string };
|
|
35
|
+
|
|
36
|
+
export function mapPollConclusionToGateVerdict(
|
|
37
|
+
conclusion: PollCheckRunsResult["conclusion"],
|
|
38
|
+
): string;
|
|
39
|
+
|
|
40
|
+
export function mapPollConclusionToOutcome(conclusion: PollCheckRunsResult["conclusion"]): string;
|
|
41
|
+
|
|
42
|
+
export function buildManagePollEventPayload(
|
|
43
|
+
prNumber: number,
|
|
44
|
+
pollResult: PollCheckRunsResult,
|
|
45
|
+
options?: { branch?: string | null; lastPolledAt?: string },
|
|
46
|
+
): ManagePollEventPayload;
|
|
47
|
+
|
|
48
|
+
export function parseManagePollArgs(args?: string[]): ParsedManagePollArgs;
|
|
49
|
+
|
|
50
|
+
export function recordManagePollSnapshot(
|
|
51
|
+
input: ManagePollInput,
|
|
52
|
+
options: {
|
|
53
|
+
eventLedger: EventLedger;
|
|
54
|
+
portfolioQueue?: PortfolioQueueStore;
|
|
55
|
+
ensurePortfolioRow?: boolean;
|
|
56
|
+
pollCheckRuns?: (
|
|
57
|
+
repoFullName: string,
|
|
58
|
+
prNumber: number,
|
|
59
|
+
options?: PollCheckRunsOptions,
|
|
60
|
+
) => Promise<PollCheckRunsResult>;
|
|
61
|
+
lastPolledAt?: string;
|
|
62
|
+
} & PollCheckRunsOptions,
|
|
63
|
+
): Promise<ManagePollRecordResult>;
|
|
64
|
+
|
|
65
|
+
export function runManagePoll(
|
|
66
|
+
args?: string[],
|
|
67
|
+
options?: {
|
|
68
|
+
initEventLedger?: () => EventLedger;
|
|
69
|
+
initPortfolioQueue?: () => PortfolioQueueStore;
|
|
70
|
+
ensurePortfolioRow?: boolean;
|
|
71
|
+
pollCheckRuns?: (
|
|
72
|
+
repoFullName: string,
|
|
73
|
+
prNumber: number,
|
|
74
|
+
options?: PollCheckRunsOptions,
|
|
75
|
+
) => Promise<PollCheckRunsResult>;
|
|
76
|
+
githubToken?: string;
|
|
77
|
+
lastPolledAt?: string;
|
|
78
|
+
} & PollCheckRunsOptions,
|
|
79
|
+
): Promise<number>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { pollCheckRuns } from "./ci-poller.js";
|
|
2
|
+
import { initEventLedger } from "./event-ledger.js";
|
|
3
|
+
import {
|
|
4
|
+
MANAGE_PR_UPDATE_EVENT,
|
|
5
|
+
formatManagedPrIdentifier,
|
|
6
|
+
} from "./manage-status.js";
|
|
7
|
+
import { initPortfolioQueueStore } from "./portfolio-queue.js";
|
|
8
|
+
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
|
|
9
|
+
|
|
10
|
+
const MANAGE_POLL_USAGE =
|
|
11
|
+
"Usage: gittensory-miner manage poll <owner/repo> <pr#> [--branch <name>] [--dry-run] [--json]";
|
|
12
|
+
|
|
13
|
+
function parseRepoArg(value, usage) {
|
|
14
|
+
if (!value) return { error: usage };
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
const [owner, repo, extra] = trimmed.split("/");
|
|
17
|
+
if (!owner || !repo || extra !== undefined) {
|
|
18
|
+
return { error: "Repository must be in owner/repo form." };
|
|
19
|
+
}
|
|
20
|
+
return { repoFullName: `${owner}/${repo}` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mapPollConclusionToGateVerdict(conclusion) {
|
|
24
|
+
switch (conclusion) {
|
|
25
|
+
case "success":
|
|
26
|
+
return "pass";
|
|
27
|
+
case "failure":
|
|
28
|
+
return "block";
|
|
29
|
+
default:
|
|
30
|
+
return "advisory";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mapPollConclusionToOutcome(conclusion) {
|
|
35
|
+
switch (conclusion) {
|
|
36
|
+
case "success":
|
|
37
|
+
return "ready";
|
|
38
|
+
case "failure":
|
|
39
|
+
return "needs-work";
|
|
40
|
+
default:
|
|
41
|
+
return "open";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildManagePollEventPayload(prNumber, pollResult, options = {}) {
|
|
46
|
+
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("invalid_pr_number");
|
|
47
|
+
if (!pollResult || typeof pollResult !== "object") throw new Error("invalid_poll_result");
|
|
48
|
+
const branch = typeof options.branch === "string" && options.branch.trim() ? options.branch.trim() : null;
|
|
49
|
+
const lastPolledAt =
|
|
50
|
+
typeof options.lastPolledAt === "string" && options.lastPolledAt.trim()
|
|
51
|
+
? options.lastPolledAt.trim()
|
|
52
|
+
: new Date().toISOString();
|
|
53
|
+
return {
|
|
54
|
+
prNumber,
|
|
55
|
+
branch,
|
|
56
|
+
ciState: pollResult.conclusion,
|
|
57
|
+
gateVerdict: mapPollConclusionToGateVerdict(pollResult.conclusion),
|
|
58
|
+
outcome: mapPollConclusionToOutcome(pollResult.conclusion),
|
|
59
|
+
lastPolledAt,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseManagePollArgs(args = []) {
|
|
64
|
+
const options = { json: false, branch: null, dryRun: false };
|
|
65
|
+
const positional = [];
|
|
66
|
+
|
|
67
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
68
|
+
const token = args[index];
|
|
69
|
+
if (token === "--json") {
|
|
70
|
+
options.json = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// #4847: still runs the real (read-only) CI-check-run poll, but skips the event-ledger append and
|
|
74
|
+
// portfolio-queue enqueue.
|
|
75
|
+
if (token === "--dry-run") {
|
|
76
|
+
options.dryRun = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (token === "--branch") {
|
|
80
|
+
const branch = args[index + 1];
|
|
81
|
+
if (!branch || branch.startsWith("-")) return { error: MANAGE_POLL_USAGE };
|
|
82
|
+
options.branch = branch;
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (token.startsWith("-")) return { error: `Unknown option: ${token}` };
|
|
87
|
+
positional.push(token);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (positional.length !== 2) return { error: MANAGE_POLL_USAGE };
|
|
91
|
+
|
|
92
|
+
const repo = parseRepoArg(positional[0], MANAGE_POLL_USAGE);
|
|
93
|
+
if ("error" in repo) return repo;
|
|
94
|
+
|
|
95
|
+
const prNumber = Number(positional[1]);
|
|
96
|
+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
|
97
|
+
return { error: "Pull request number must be a positive integer." };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
repoFullName: repo.repoFullName,
|
|
102
|
+
prNumber,
|
|
103
|
+
...options,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ensureManagedPrRow(portfolioQueue, repoFullName, prNumber) {
|
|
108
|
+
const identifier = formatManagedPrIdentifier(prNumber);
|
|
109
|
+
const exists = portfolioQueue
|
|
110
|
+
.listQueue(repoFullName)
|
|
111
|
+
.some((entry) => entry.identifier === identifier);
|
|
112
|
+
if (!exists) {
|
|
113
|
+
portfolioQueue.enqueue({ repoFullName, identifier, priority: 0 });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Poll GitHub check runs for a managed PR and append a `manage_pr_update` snapshot to the local event ledger.
|
|
119
|
+
* Completes the manage-status data path introduced in #2325 / #3070 using the CI poller from #2323.
|
|
120
|
+
*/
|
|
121
|
+
export async function recordManagePollSnapshot(input, options = {}) {
|
|
122
|
+
if (!input || typeof input !== "object") throw new Error("invalid_manage_poll_input");
|
|
123
|
+
const repoFullName = typeof input.repoFullName === "string" ? input.repoFullName.trim() : "";
|
|
124
|
+
const [owner, repo, extra] = repoFullName.split("/");
|
|
125
|
+
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
|
|
126
|
+
if (!Number.isInteger(input.prNumber) || input.prNumber <= 0) throw new Error("invalid_pr_number");
|
|
127
|
+
|
|
128
|
+
const eventLedger = options.eventLedger;
|
|
129
|
+
if (!eventLedger || typeof eventLedger.appendEvent !== "function") {
|
|
130
|
+
throw new Error("invalid_event_ledger");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const portfolioQueue = options.portfolioQueue;
|
|
134
|
+
if (options.portfolioQueue !== undefined) {
|
|
135
|
+
if (!portfolioQueue || typeof portfolioQueue.enqueue !== "function") {
|
|
136
|
+
throw new Error("invalid_portfolio_queue");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const pollCheckRunsFn = options.pollCheckRuns ?? pollCheckRuns;
|
|
141
|
+
const pollResult = await pollCheckRunsFn(repoFullName, input.prNumber, {
|
|
142
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
143
|
+
fetchFn: options.fetchFn,
|
|
144
|
+
githubToken: options.githubToken ?? "",
|
|
145
|
+
maxAttempts: options.maxAttempts,
|
|
146
|
+
minIntervalMs: options.minIntervalMs,
|
|
147
|
+
maxIntervalMs: options.maxIntervalMs,
|
|
148
|
+
sleepFn: options.sleepFn,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const payload = buildManagePollEventPayload(input.prNumber, pollResult, {
|
|
152
|
+
branch: input.branch,
|
|
153
|
+
lastPolledAt: options.lastPolledAt,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if ((options.ensurePortfolioRow ?? true) && portfolioQueue) {
|
|
157
|
+
ensureManagedPrRow(portfolioQueue, repoFullName, input.prNumber);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const event = eventLedger.appendEvent({
|
|
161
|
+
type: MANAGE_PR_UPDATE_EVENT,
|
|
162
|
+
repoFullName,
|
|
163
|
+
payload,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return { pollResult, payload, event };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function runManagePoll(args = [], options = {}) {
|
|
170
|
+
const parsed = parseManagePollArgs(args);
|
|
171
|
+
if ("error" in parsed) {
|
|
172
|
+
return reportCliFailure(argsWantJson(args), parsed.error);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// #4847: the CI-check-run poll itself is a real, read-only GitHub signal -- the useful "what would this
|
|
176
|
+
// record?" output -- so a dry run still performs it for real. It never opens the event ledger or portfolio
|
|
177
|
+
// queue, though: a no-op event ledger is fed through recordManagePollSnapshot so its own real payload-building
|
|
178
|
+
// logic still runs, just without ever writing to local storage (ensurePortfolioRow: false skips the queue
|
|
179
|
+
// enqueue the same way).
|
|
180
|
+
if (parsed.dryRun) {
|
|
181
|
+
const noopEventLedger = { appendEvent: () => null };
|
|
182
|
+
try {
|
|
183
|
+
const result = await recordManagePollSnapshot(
|
|
184
|
+
{ repoFullName: parsed.repoFullName, prNumber: parsed.prNumber, branch: parsed.branch },
|
|
185
|
+
{
|
|
186
|
+
eventLedger: noopEventLedger,
|
|
187
|
+
ensurePortfolioRow: false,
|
|
188
|
+
pollCheckRuns: options.pollCheckRuns,
|
|
189
|
+
fetchFn: options.fetchFn,
|
|
190
|
+
githubToken: options.githubToken ?? process.env.GITHUB_TOKEN ?? "",
|
|
191
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
192
|
+
maxAttempts: options.maxAttempts,
|
|
193
|
+
minIntervalMs: options.minIntervalMs,
|
|
194
|
+
maxIntervalMs: options.maxIntervalMs,
|
|
195
|
+
sleepFn: options.sleepFn,
|
|
196
|
+
lastPolledAt: options.lastPolledAt,
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
const dryRunResult = { outcome: "dry_run", pollResult: result.pollResult, payload: result.payload };
|
|
200
|
+
if (parsed.json) {
|
|
201
|
+
console.log(JSON.stringify(dryRunResult, null, 2));
|
|
202
|
+
} else {
|
|
203
|
+
console.log(
|
|
204
|
+
`DRY RUN: ${result.payload.ciState} (${result.payload.gateVerdict}/${result.payload.outcome}). No event-ledger or portfolio-queue write was made.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return 0;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const ownsEventLedger = options.initEventLedger === undefined;
|
|
215
|
+
const ownsPortfolioQueue = options.initPortfolioQueue === undefined;
|
|
216
|
+
const eventLedger = (options.initEventLedger ?? initEventLedger)();
|
|
217
|
+
const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = await recordManagePollSnapshot(
|
|
221
|
+
{
|
|
222
|
+
repoFullName: parsed.repoFullName,
|
|
223
|
+
prNumber: parsed.prNumber,
|
|
224
|
+
branch: parsed.branch,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
eventLedger,
|
|
228
|
+
portfolioQueue,
|
|
229
|
+
ensurePortfolioRow: options.ensurePortfolioRow ?? true,
|
|
230
|
+
pollCheckRuns: options.pollCheckRuns,
|
|
231
|
+
fetchFn: options.fetchFn,
|
|
232
|
+
githubToken: options.githubToken ?? process.env.GITHUB_TOKEN ?? "",
|
|
233
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
234
|
+
maxAttempts: options.maxAttempts,
|
|
235
|
+
minIntervalMs: options.minIntervalMs,
|
|
236
|
+
maxIntervalMs: options.maxIntervalMs,
|
|
237
|
+
sleepFn: options.sleepFn,
|
|
238
|
+
lastPolledAt: options.lastPolledAt,
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (parsed.json) {
|
|
243
|
+
console.log(JSON.stringify(result, null, 2));
|
|
244
|
+
} else {
|
|
245
|
+
console.log(`${result.payload.ciState} (${result.payload.gateVerdict}/${result.payload.outcome})`);
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return reportCliFailure(parsed.json, describeCliError(error));
|
|
250
|
+
} finally {
|
|
251
|
+
if (ownsEventLedger) eventLedger.close();
|
|
252
|
+
if (ownsPortfolioQueue) portfolioQueue.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { EventLedger, LedgerEntry } from "./event-ledger.js";
|
|
2
|
+
import type { PortfolioQueueStore, QueueStatus } from "./portfolio-queue.js";
|
|
3
|
+
import type { RunState, RunStateStore } from "./run-state.js";
|
|
4
|
+
|
|
5
|
+
export type ManageStatusRow = {
|
|
6
|
+
repoFullName: string;
|
|
7
|
+
prNumber: number;
|
|
8
|
+
branch: string | null;
|
|
9
|
+
ciState: string | null;
|
|
10
|
+
gateVerdict: string | null;
|
|
11
|
+
outcome: string | null;
|
|
12
|
+
lastPolledAt: string | null;
|
|
13
|
+
queueStatus: QueueStatus | null;
|
|
14
|
+
priority: number | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ManageStatusSources = {
|
|
18
|
+
portfolioQueue: PortfolioQueueStore;
|
|
19
|
+
eventLedger: EventLedger;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RunPortfolioSources = ManageStatusSources & {
|
|
23
|
+
runStateStore: RunStateStore;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RunPortfolioRow = {
|
|
27
|
+
repoFullName: string;
|
|
28
|
+
runState: RunState | null;
|
|
29
|
+
runStateUpdatedAt: string | null;
|
|
30
|
+
prCount: number;
|
|
31
|
+
prs: ManageStatusRow[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ManageUpdateSnapshot = {
|
|
35
|
+
repoFullName: string;
|
|
36
|
+
prNumber: number;
|
|
37
|
+
branch: string | null;
|
|
38
|
+
ciState: string | null;
|
|
39
|
+
gateVerdict: string | null;
|
|
40
|
+
outcome: string | null;
|
|
41
|
+
lastPolledAt: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const MANAGE_PR_UPDATE_EVENT: "manage_pr_update";
|
|
45
|
+
export const MANAGED_PR_IDENTIFIER_PREFIX: "pr:";
|
|
46
|
+
|
|
47
|
+
export function parseManagedPrIdentifier(identifier: string): number | null;
|
|
48
|
+
|
|
49
|
+
export function formatManagedPrIdentifier(prNumber: number): string;
|
|
50
|
+
|
|
51
|
+
export function indexLatestManageUpdates(events: LedgerEntry[]): Map<string, ManageUpdateSnapshot>;
|
|
52
|
+
|
|
53
|
+
export function collectManageStatus(sources: ManageStatusSources): ManageStatusRow[];
|
|
54
|
+
|
|
55
|
+
export function collectRunPortfolio(sources: RunPortfolioSources): RunPortfolioRow[];
|
|
56
|
+
|
|
57
|
+
export function renderManageStatusTable(rows: ManageStatusRow[]): string;
|
|
58
|
+
|
|
59
|
+
export function renderRunPortfolioTable(portfolio: RunPortfolioRow[]): string;
|
|
60
|
+
|
|
61
|
+
export function parseManageStatusArgs(args?: string[]): { json: boolean } | { error: string };
|
|
62
|
+
|
|
63
|
+
export function runManageStatus(
|
|
64
|
+
args?: string[],
|
|
65
|
+
options?: {
|
|
66
|
+
initPortfolioQueue?: () => PortfolioQueueStore;
|
|
67
|
+
initEventLedger?: () => EventLedger;
|
|
68
|
+
initRunStateStore?: () => RunStateStore;
|
|
69
|
+
},
|
|
70
|
+
): number;
|