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
33 changes: 33 additions & 0 deletions packages/loopover-miner/lib/chat-discover-attempt-actions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ChatActionRegistry } from "./chat-action-registry.js";

export const DISCOVER_CHAT_ACTION: "discover";
export const ATTEMPT_CHAT_ACTION: "attempt";

export type DiscoverChatActionInput = {
targets?: string[];
search?: string;
dryRun?: boolean;
json?: boolean;
apiBaseUrl?: string;
tokenEnv?: string;
};

export type AttemptChatActionInput = {
repoFullName: string;
issueNumber: number;
minerLogin: string;
base?: string;
live?: boolean;
dryRun?: boolean;
json?: boolean;
};

export function isDiscoverChatParams(params: unknown): boolean;
export function isAttemptChatParams(params: unknown): boolean;

export function registerDiscoverAttemptChatActions(options: {
requestDiscover: (input: DiscoverChatActionInput) => Promise<unknown>;
requestAttempt: (input: AttemptChatActionInput) => Promise<unknown>;
registry?: ChatActionRegistry;
evaluateGate?: () => { decision: { stage: string } };
}): void;
141 changes: 141 additions & 0 deletions packages/loopover-miner/lib/chat-discover-attempt-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Discover/attempt chat-action registrations (#6837).
//
// The third and last child of the chat action-dispatch scaffolding (#6519) — chat-action-registry.js:4-5
// names all three families (portfolio release/requeue, governor pause/resume, discover/attempt); the other
// two already ship. Registers `discover` / `attempt` into a chat-action registry. Handlers MUST be wired to
// the miner-ui clients `requestDiscover` / `requestAttempt` (apps/loopover-miner-ui/src/lib/{discover,
// attempt}.ts), so chat POSTs the SAME `/api/discover` and `/api/attempt` routes that already exist (#6522,
// registered at vite.config.ts:36-37) — never discover-cli.js/attempt-cli.js directly, and never a
// hand-rolled fetch. The miner-ui wire module passes those clients in; this module only owns the registration
// contract + params validators.
//
// GATING — the gate lives at the endpoint, not here, and that is deliberate:
// * `attempt` INHERITS the real Governor chokepoint for free: the route calls the real, unmodified
// `runAttempt`, and attempt-runner.js routes every write through
// `evaluateGovernorChokepointGatePersisted` before executing it (vite-attempt-api.ts:7-9).
// * `discover` has no chokepoint because it performs no gated write — it only fans out, ranks and enqueues
// (vite-discover-api.ts:13-14), so the CLI has none and the route adds none.
// Re-evaluating the chokepoint here would therefore be a SECOND, competing gate on a path that already has
// one (or needs none) — exactly what those route comments rule out, and it would gate chat more strictly than
// the equivalent CLI invocation. So, like chat-governor-actions.js and chat-portfolio-actions.js, we satisfy
// the registry's `governorGatedHandler` brand with an allow-stage evaluateGate. Execution still stays behind
// the shared LOOPOVER_MINER_CHAT_ACTIONS flag via `dispatchChatAction`, and `evaluateGate` stays injectable.

import { governorGatedHandler, chatActionRegistry } from "./chat-action-registry.js";

export const DISCOVER_CHAT_ACTION = "discover";
export const ATTEMPT_CHAT_ACTION = "attempt";

/** The endpoint owns the gate (see the header note); satisfy the registry brand only. */
const allowEndpointGatedAction = () => ({ decision: { stage: "allow" } });

const DISCOVER_KEYS = new Set(["targets", "search", "dryRun", "json", "apiBaseUrl", "tokenEnv"]);
const ATTEMPT_KEYS = new Set(["repoFullName", "issueNumber", "minerLogin", "base", "live", "dryRun", "json"]);

/**
* @param {unknown} params
* @returns {Record<string, unknown> | null}
*/
function asParamsRecord(params) {
if (params == null || typeof params !== "object" || Array.isArray(params)) return null;
return /** @type {Record<string, unknown>} */ (params);
}

/** A non-empty string — the shape every required text field here needs. */
function isNonEmptyString(value) {
return typeof value === "string" && value.trim() !== "";
}

/**
* `DiscoverActionInput` — every field optional (the CLI defaults them all), so an empty object is a valid
* "discover with defaults". Unknown keys are rejected rather than ignored: these params can be model-authored,
* and a typo'd flag must fail loudly instead of silently running a different discovery than intended.
*
* @param {unknown} params
* @returns {boolean}
*/
export function isDiscoverChatParams(params) {
if (params == null) return true;
const record = asParamsRecord(params);
if (record === null) return false;
for (const key of Object.keys(record)) {
if (!DISCOVER_KEYS.has(key)) return false;
}
if (record.targets !== undefined) {
if (!Array.isArray(record.targets) || !record.targets.every(isNonEmptyString)) return false;
}
for (const key of ["search", "apiBaseUrl", "tokenEnv"]) {
if (record[key] !== undefined && typeof record[key] !== "string") return false;
}
for (const key of ["dryRun", "json"]) {
if (record[key] !== undefined && typeof record[key] !== "boolean") return false;
}
return true;
}

/**
* `AttemptActionInput` — `repoFullName` / `issueNumber` / `minerLogin` are REQUIRED (the CLI has no default
* for which issue to attempt), so unlike discover there is no valid empty form. `issueNumber` must be a
* positive integer: a float or 0 would reach the CLI as a nonsense issue reference.
*
* @param {unknown} params
* @returns {boolean}
*/
export function isAttemptChatParams(params) {
const record = asParamsRecord(params);
if (record === null) return false;
for (const key of Object.keys(record)) {
if (!ATTEMPT_KEYS.has(key)) return false;
}
if (!isNonEmptyString(record.repoFullName)) return false;
if (!isNonEmptyString(record.minerLogin)) return false;
if (!Number.isInteger(record.issueNumber) || /** @type {number} */ (record.issueNumber) <= 0) return false;
if (record.base !== undefined && typeof record.base !== "string") return false;
for (const key of ["live", "dryRun", "json"]) {
if (record[key] !== undefined && typeof record[key] !== "boolean") return false;
}
return true;
}

/**
* Idempotently register `discover` / `attempt`.
*
* @param {{
* requestDiscover: (input: object) => Promise<unknown>,
* requestAttempt: (input: object) => Promise<unknown>,
* registry?: import("./chat-action-registry.js").ChatActionRegistry,
* evaluateGate?: () => { decision: { stage: string } },
* }} options
*/
export function registerDiscoverAttemptChatActions(options) {
const requestDiscover = options?.requestDiscover;
const requestAttempt = options?.requestAttempt;
if (typeof requestDiscover !== "function") {
throw new TypeError("registerDiscoverAttemptChatActions: requestDiscover must be a function");
}
if (typeof requestAttempt !== "function") {
throw new TypeError("registerDiscoverAttemptChatActions: requestAttempt must be a function");
}

const registry = options.registry ?? chatActionRegistry;
const evaluateGate = options.evaluateGate ?? allowEndpointGatedAction;

if (!registry.has(DISCOVER_CHAT_ACTION)) {
registry.register(DISCOVER_CHAT_ACTION, {
paramsValidator: isDiscoverChatParams,
// Nullish params mean "discover with defaults" -- forwarded as {} so the client always POSTs an object.
handler: governorGatedHandler(async (request) => requestDiscover(asParamsRecord(request?.params) ?? {}), {
evaluateGate,
}),
});
}

if (!registry.has(ATTEMPT_CHAT_ACTION)) {
registry.register(ATTEMPT_CHAT_ACTION, {
paramsValidator: isAttemptChatParams,
handler: governorGatedHandler(async (request) => requestAttempt(asParamsRecord(request?.params)), {
evaluateGate,
}),
});
}
}
2 changes: 1 addition & 1 deletion packages/loopover-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"scripts": {
"benchmark": "node scripts/benchmark.mjs",
"cross-repo-eval": "node scripts/cross-repo-evaluation.mjs",
"build": "node --check bin/loopover-miner.js && node --check bin/loopover-miner-mcp.js && node --check lib/ams-policy.js && node --check lib/attempt-cli.js && node --check lib/attempt-input-builder.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.js && node --check lib/calibration-run.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/chat-action-dispatch.js && node --check lib/chat-action-registry.js && node --check lib/chat-governor-actions.js && node --check lib/chat-portfolio-actions.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-conflict-resolver.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/coding-task-spec.js && node --check lib/cross-repo-evaluation.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/deployment-docs-audit.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/execute-local-write.js && node --check lib/feasibility-cli.js && node --check lib/governor-chokepoint-persisted.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-metrics-cli.js && node --check lib/governor-pause-cli.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/harness-submission-trigger.js && node --check lib/init-wizard.js && node --check lib/laptop-init.js && node --check lib/live-issue-snapshot.js && node --check lib/local-store.js && node --check lib/logger.js && node --check lib/loop-cli.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/metrics-cli.js && node --check lib/miner-goal-spec.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/policy-doc-cache.js && node --check lib/policy-verdict-cache.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-expiry.js && node --check lib/pr-disposition-poller.js && node --check lib/pr-number-parse.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/purge-cli.js && node --check lib/ranked-candidates.js && node --check lib/rejection-signal.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-bridge.js && node --check lib/replay-task-generation.js && node --check lib/repo-clone.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/self-review-context.js && node --check lib/sentry.js && node --check lib/slop-assessment.js && node --check lib/stack-detection.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
"build": "node --check bin/loopover-miner.js && node --check bin/loopover-miner-mcp.js && node --check lib/ams-policy.js && node --check lib/attempt-cli.js && node --check lib/attempt-input-builder.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.js && node --check lib/calibration-run.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/chat-action-dispatch.js && node --check lib/chat-action-registry.js && node --check lib/chat-discover-attempt-actions.js && node --check lib/chat-governor-actions.js && node --check lib/chat-portfolio-actions.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-conflict-resolver.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-construction.js && node --check lib/coding-agent-house-rules.js && node --check lib/coding-task-spec.js && node --check lib/cross-repo-evaluation.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/deployment-docs-audit.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/execute-local-write.js && node --check lib/feasibility-cli.js && node --check lib/governor-chokepoint-persisted.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-metrics-cli.js && node --check lib/governor-pause-cli.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/harness-submission-trigger.js && node --check lib/init-wizard.js && node --check lib/laptop-init.js && node --check lib/live-issue-snapshot.js && node --check lib/local-store.js && node --check lib/logger.js && node --check lib/loop-cli.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/metrics-cli.js && node --check lib/miner-goal-spec.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/policy-doc-cache.js && node --check lib/policy-verdict-cache.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-expiry.js && node --check lib/pr-disposition-poller.js && node --check lib/pr-number-parse.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/purge-cli.js && node --check lib/ranked-candidates.js && node --check lib/rejection-signal.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-snapshot.js && node --check lib/replay-task-bridge.js && node --check lib/replay-task-generation.js && node --check lib/repo-clone.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/self-review-context.js && node --check lib/sentry.js && node --check lib/slop-assessment.js && node --check lib/stack-detection.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
},
"dependencies": {
"@loopover/engine": "^3.0.0",
Expand Down
Loading