Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions packages/loopover-miner/lib/ams-policy.d.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import type { AmsPolicySpec } from "@loopover/engine";
export function resolveAmsPolicyConfigPath(env?: Record<string, string | undefined>): string;

export type AmsPolicySource = "local" | "default";

export type ResolvedAmsPolicy = {
spec: AmsPolicySpec;
source: AmsPolicySource;
warnings: string[];
spec: AmsPolicySpec;
source: AmsPolicySource;
warnings: string[];
};

export function resolveAmsPolicy(
repoFullName: string,
options?: {
export type AmsPolicyOptions = {
/** Accepted for forward/API compatibility with callers that pass a fetch override; unused today since this
* resolver never fetches (see the module doc comment above). */
fetchImpl?: unknown;
readFileSync?: (path: string, encoding: "utf8") => string;
existsSync?: (path: string) => boolean;
env?: Record<string, string | undefined>;
},
): Promise<ResolvedAmsPolicy>;
};
/** Resolve the operator's local AMS policy file path: explicit env var > `LOOPOVER_MINER_CONFIG_DIR` >
* `XDG_CONFIG_HOME`/`~/.config`, mirroring every other local-store path in this package. */
export declare function resolveAmsPolicyConfigPath(env?: Record<string, string | undefined>): string;
/**
* Resolve the real, effective AMS execution policy for one attempt: the operator's own local
* `.loopover-ams.yml` when present (source: "local"), else the engine's safe defaults (source: "default").
* Never throws -- an unreadable/malformed local file degrades through the tolerant parser to the safe
* defaults, same discipline as every other tolerant parser in this pipeline.
*
* `repoFullName` is accepted for API compatibility with callers that resolve policy per target repo, but the
* resolver intentionally does not fetch or trust target-repository AMS policy content.
*/
export declare function resolveAmsPolicy(repoFullName: string, options?: AmsPolicyOptions): Promise<ResolvedAmsPolicy>;
60 changes: 24 additions & 36 deletions packages/loopover-miner/lib/ams-policy.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 88 additions & 0 deletions packages/loopover-miner/lib/ams-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { existsSync, readFileSync } from "node:fs";
import type { AmsPolicySpec } from "@loopover/engine";
import { DEFAULT_AMS_POLICY_SPEC, parseAmsPolicySpecContent } from "@loopover/engine";
import { resolveLocalStoreDbPath } from "./local-store.js";

// Resolver for the operator-local `.loopover-ams.yml` (#5132, Wave 3.5 follow-up). AmsPolicySpec
// (ams-policy-spec.ts, engine package) is the type/parser surface; this module is the actual local
// read+resolve caller.
//
// This is deliberately NOT the same resolution shape as self-review-context.js/rejection-signal.js, which
// read from the target repo: AmsPolicySpec's fields are the OPERATOR's own execution-risk policy, so an
// untrusted target repo must never get final say over them.

const AMS_POLICY_FILENAME = ".loopover-ams.yml";

export type AmsPolicySource = "local" | "default";

export type ResolvedAmsPolicy = {
spec: AmsPolicySpec;
source: AmsPolicySource;
warnings: string[];
};

export type AmsPolicyOptions = {
/** Accepted for forward/API compatibility with callers that pass a fetch override; unused today since this
* resolver never fetches (see the module doc comment above). */
fetchImpl?: unknown;
readFileSync?: (path: string, encoding: "utf8") => string;
existsSync?: (path: string) => boolean;
env?: Record<string, string | undefined>;
};

type NormalizedAmsPolicyOptions = {
readFileSync: (path: string, encoding: "utf8") => string;
existsSync: (path: string) => boolean;
env: Record<string, string | undefined>;
};

/** Resolve the operator's local AMS policy file path: explicit env var > `LOOPOVER_MINER_CONFIG_DIR` >
* `XDG_CONFIG_HOME`/`~/.config`, mirroring every other local-store path in this package. */
export function resolveAmsPolicyConfigPath(env: Record<string, string | undefined> = process.env): string {
return resolveLocalStoreDbPath(AMS_POLICY_FILENAME, "LOOPOVER_MINER_AMS_POLICY_PATH", env);
}

function normalizeOptions(options: AmsPolicyOptions = {}): NormalizedAmsPolicyOptions {
return {
readFileSync: options.readFileSync ?? readFileSync,
existsSync: options.existsSync ?? existsSync,
env: options.env ?? process.env,
};
}

/** Read the operator's own local `.loopover-ams.yml`, if one exists. Never throws: an unreadable file is
* treated the same as an absent one, falling through to the next resolution layer. */
function readLocalAmsPolicyContent(resolved: NormalizedAmsPolicyOptions): string | null {
const path = resolveAmsPolicyConfigPath(resolved.env);
if (!resolved.existsSync(path)) return null;
try {
return resolved.readFileSync(path, "utf8");
} catch {
return null;
}
}

/**
* Resolve the real, effective AMS execution policy for one attempt: the operator's own local
* `.loopover-ams.yml` when present (source: "local"), else the engine's safe defaults (source: "default").
* Never throws -- an unreadable/malformed local file degrades through the tolerant parser to the safe
* defaults, same discipline as every other tolerant parser in this pipeline.
*
* `repoFullName` is accepted for API compatibility with callers that resolve policy per target repo, but the
* resolver intentionally does not fetch or trust target-repository AMS policy content.
*/
export async function resolveAmsPolicy(
repoFullName: string,
options: AmsPolicyOptions = {},
): Promise<ResolvedAmsPolicy> {
void repoFullName;
const resolved = normalizeOptions(options);

const localContent = readLocalAmsPolicyContent(resolved);
if (localContent !== null) {
const parsed = parseAmsPolicySpecContent(localContent);
return { spec: parsed.spec, source: "local", warnings: parsed.warnings };
}

return { spec: DEFAULT_AMS_POLICY_SPEC, source: "default", warnings: [] };
}
116 changes: 60 additions & 56 deletions packages/loopover-miner/lib/attempt-runner.d.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,67 @@
import type {
CodingAgentDriver,
GovernorDecision,
IterateLoopInput,
IterateLoopResult,
LocalWriteActionSpec,
} from "@loopover/engine";
import type { HarnessSubmissionDecision, HarnessSubmissionEventLedger } from "./harness-submission-trigger.js";
import type { SubmissionFreshnessClaimLedger, LiveIssueSnapshot, FreshnessAbortReason } from "./submission-freshness-check.js";
import type { CodingAgentDriver, GovernorDecision, IterateLoopInput, IterateLoopResult, IterateLoopShouldAbort, LocalWriteActionSpec } from "@loopover/engine";
import type { FreshnessAbortReason, LiveIssueSnapshot, SubmissionFreshnessClaimLedger } from "./submission-freshness-check.js";
import type { GovernorChokepointInputPersisted } from "./governor-chokepoint-persisted.js";
import type { GovernorState } from "./governor-state.js";

export const ATTEMPT_OUTCOMES: readonly ["abandon", "stale", "blocked", "governed", "submitted"];

// rateLimitBuckets/rateLimitBackoffAttempts/capUsage are optional here (via GovernorChokepointInputPersisted,
// not the engine's own GovernorChokepointInput) so a caller can omit them and let evaluateGovernorChokepointGatePersisted
// (#5134) auto-supply real persisted state -- forcing them required at this layer would make every caller
// hand-thread honest-but-stale zero defaults on every invocation, silently defeating that persistence.
import type { HarnessSubmissionDecision, HarnessSubmissionEventLedger } from "./harness-submission-trigger.js";
export declare const ATTEMPT_OUTCOMES: readonly ["abandon", "stale", "blocked", "governed", "submitted"];
export type AttemptGovernorContext = Omit<GovernorChokepointInputPersisted, "actionClass" | "repoFullName" | "nowMs" | "wouldBeAction">;

export type AttemptInput = {
loopInput: IterateLoopInput;
issueNumber: number;
minerLogin: string;
base: string;
killSwitchScope: "global" | "repo" | "none";
slopThreshold: "clean" | "low" | "elevated" | "high";
submissionMode: "observe" | "enforce";
maxConsecutiveGateBlocks?: number;
draft?: boolean;
governor: AttemptGovernorContext;
loopInput: IterateLoopInput;
issueNumber: number;
minerLogin: string;
base: string;
killSwitchScope: "global" | "repo" | "none";
slopThreshold: "clean" | "low" | "elevated" | "high";
submissionMode: "observe" | "enforce";
maxConsecutiveGateBlocks?: number;
draft?: boolean;
governor: AttemptGovernorContext;
};

export type AttemptDeps = {
driver: CodingAgentDriver;
runSlopAssessment: (input: unknown) => unknown;
appendAttemptLogEvent: (event: unknown) => void;
claimLedger: SubmissionFreshnessClaimLedger;
fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise<LiveIssueSnapshot | null>;
eventLedger: HarnessSubmissionEventLedger;
/** Injected governor-ledger append (mirrors evaluateGovernorChokepointGate's own `options.append`); omitted
* falls back to that function's own default (the real default governor ledger). */
governorLedgerAppend?: (event: unknown) => unknown;
/** Injected governor-state store (#5134); omitted falls back to evaluateGovernorChokepointGatePersisted's
* own default (opens + closes the real default governor-state store for this one call). */
governorState?: GovernorState;
sessionStartMs?: number;
nowMs: number;
executeLocalWrite: (spec: LocalWriteActionSpec) => Promise<unknown>;
/** Mid-attempt kill-switch probe threaded into `runIterateLoop` (#5670). */
shouldAbort?: () => import("@loopover/engine").IterateLoopShouldAbort;
/** Live kill-switch scope resolver after handoff (#5670); defaults to the frozen attempt-start scope. */
resolveKillSwitchScope?: () => "global" | "repo" | "none";
driver: CodingAgentDriver;
runSlopAssessment: (input: unknown) => unknown;
appendAttemptLogEvent: (event: unknown) => void;
claimLedger: SubmissionFreshnessClaimLedger;
fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise<LiveIssueSnapshot | null>;
eventLedger: HarnessSubmissionEventLedger;
/** Injected governor-ledger append (mirrors evaluateGovernorChokepointGate's own `options.append`); omitted
* falls back to that function's own default (the real default governor ledger). */
governorLedgerAppend?: (event: unknown) => unknown;
/** Injected governor-state store (#5134); omitted falls back to evaluateGovernorChokepointGatePersisted's
* own default (opens + closes the real default governor-state store for this one call). */
governorState?: GovernorState;
sessionStartMs?: number;
nowMs: number;
executeLocalWrite: (spec: LocalWriteActionSpec) => Promise<unknown>;
/** Mid-attempt kill-switch probe threaded into `runIterateLoop` (#5670). */
shouldAbort?: () => IterateLoopShouldAbort;
/** Live kill-switch scope resolver after handoff (#5670); defaults to the frozen attempt-start scope. */
resolveKillSwitchScope?: () => "global" | "repo" | "none";
};
export type AttemptResult = {
outcome: "abandon";
loopResult: IterateLoopResult;
} | {
outcome: "stale";
reason: FreshnessAbortReason;
loopResult: IterateLoopResult;
} | {
outcome: "blocked";
decision: HarnessSubmissionDecision;
loopResult: IterateLoopResult;
} | {
outcome: "governed";
decision: GovernorDecision;
loopResult: IterateLoopResult;
} | {
outcome: "submitted";
spec: LocalWriteActionSpec;
execResult: unknown;
loopResult: IterateLoopResult;
};

export type AttemptResult =
| { outcome: "abandon"; loopResult: IterateLoopResult }
| { outcome: "stale"; reason: FreshnessAbortReason; loopResult: IterateLoopResult }
| { outcome: "blocked"; decision: HarnessSubmissionDecision; loopResult: IterateLoopResult }
| { outcome: "governed"; decision: GovernorDecision; loopResult: IterateLoopResult }
| { outcome: "submitted"; spec: LocalWriteActionSpec; execResult: unknown; loopResult: IterateLoopResult };

export function runMinerAttempt(input: AttemptInput, deps: AttemptDeps): Promise<AttemptResult>;
/**
* Run one full attempt end to end: iterate-loop -> (on handoff) freshness -> submission-gate -> Governor
* chokepoint -> (on allowed:true) build + execute the real open_pr command. Fails closed (throws) on malformed
* input/deps, mirroring every sibling module in this pipeline.
*/
export declare function runMinerAttempt(input: AttemptInput, deps: AttemptDeps): Promise<AttemptResult>;
Loading
Loading