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

export const PORTFOLIO_RELEASE_CHAT_ACTION: "portfolio_release";
export const PORTFOLIO_REQUEUE_CHAT_ACTION: "portfolio_requeue";

export type PortfolioChatActionItem = {
repoFullName: string;
identifier: string;
apiBaseUrl?: string;
};

export function isPortfolioItemChatParams(params: unknown): boolean;

export function registerPortfolioChatActions(options: {
releaseItem: (item: PortfolioChatActionItem) => Promise<unknown>;
requeueItem: (item: PortfolioChatActionItem) => Promise<unknown>;
registry?: ChatActionRegistry;
evaluateGate?: () => { decision: { stage: string } };
}): void;
103 changes: 103 additions & 0 deletions packages/loopover-miner/lib/chat-portfolio-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Portfolio release/requeue chat-action registrations (#6838).
//
// Child issue of the chat action-dispatch scaffolding (#6519). Registers `portfolio_release` /
// `portfolio_requeue` into a chat-action registry. Handlers MUST be wired to the miner-ui clients
// `releasePortfolioQueueItem` / `requeuePortfolioQueueItem` (apps/loopover-miner-ui/src/lib/
// portfolio-queue-actions.ts), so chat POSTs the SAME `/api/portfolio-queue/{release,requeue}` routes the
// dashboard's existing buttons already call — never portfolio-queue.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. That is what keeps chat from becoming a parallel write path (#6504's design).
//
// Release/requeue is local queue administration, not a chokepoint content-write: the route it lands on is a
// thin bridge to the same store methods the CLI's `queue release` / `queue requeue` already use
// (vite-portfolio-queue-actions-api.ts → reclaimStuckItem / requeueItem), and it invokes no chokepoint of its
// own. Requiring one only for the chat path would gate chat MORE strictly than the button beside it, which
// #6838 forbids ("No changes to the existing route or button-triggered flow"). So, exactly like
// chat-governor-actions.js's administrative pause/resume, we satisfy the registry's `governorGatedHandler`
// brand with an allow-stage evaluateGate rather than routing through governor-chokepoint.js. Execution still
// stays behind the shared LOOPOVER_MINER_CHAT_ACTIONS flag via `dispatchChatAction`.

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

export const PORTFOLIO_RELEASE_CHAT_ACTION = "portfolio_release";
export const PORTFOLIO_REQUEUE_CHAT_ACTION = "portfolio_requeue";

/** Local queue administration is not a chokepoint content-write (#6838); satisfy the registry brand only. */
const allowAdministrativeGate = () => ({ decision: { stage: "allow" } });

/**
* Params for both actions: the queue item to act on. `repoFullName` + `identifier` are required non-empty
* strings; `apiBaseUrl` is optional (the route defaults it, mirroring the client's own
* `Pick<PortfolioQueueActionItem, "repoFullName" | "identifier" | "apiBaseUrl">` shape, where the buttons
* always pass one but the CLI path does not). Unknown keys are rejected rather than ignored: a typo'd param
* from a model-authored call must fail loudly, not silently act on the wrong item.
*
* @param {unknown} params
* @returns {boolean}
*/
export function isPortfolioItemChatParams(params) {
if (params == null || typeof params !== "object" || Array.isArray(params)) return false;
const record = /** @type {Record<string, unknown>} */ (params);
for (const key of Object.keys(record)) {
if (key !== "repoFullName" && key !== "identifier" && key !== "apiBaseUrl") return false;
}
if (typeof record.repoFullName !== "string" || record.repoFullName.trim() === "") return false;
if (typeof record.identifier !== "string" || record.identifier.trim() === "") return false;
if (record.apiBaseUrl !== undefined && typeof record.apiBaseUrl !== "string") return false;
return true;
}

/**
* Narrow validated params to the client's item shape. `apiBaseUrl` is only forwarded when present, so an
* omitted one stays omitted rather than becoming an explicit `undefined` in the POST body.
*
* @param {unknown} params
* @returns {{ repoFullName: string, identifier: string, apiBaseUrl?: string }}
*/
function readPortfolioItem(params) {
const record = /** @type {{ repoFullName: string, identifier: string, apiBaseUrl?: unknown }} */ (params);
const item = { repoFullName: record.repoFullName, identifier: record.identifier };
return typeof record.apiBaseUrl === "string" ? { ...item, apiBaseUrl: record.apiBaseUrl } : item;
}

/**
* Idempotently register `portfolio_release` / `portfolio_requeue`.
*
* @param {{
* releaseItem: (item: { repoFullName: string, identifier: string, apiBaseUrl?: string }) => Promise<unknown>,
* requeueItem: (item: { repoFullName: string, identifier: string, apiBaseUrl?: string }) => Promise<unknown>,
* registry?: import("./chat-action-registry.js").ChatActionRegistry,
* evaluateGate?: () => { decision: { stage: string } },
* }} options
*/
export function registerPortfolioChatActions(options) {
const releaseItem = options?.releaseItem;
const requeueItem = options?.requeueItem;
if (typeof releaseItem !== "function") {
throw new TypeError("registerPortfolioChatActions: releaseItem must be a function");
}
if (typeof requeueItem !== "function") {
throw new TypeError("registerPortfolioChatActions: requeueItem must be a function");
}

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

if (!registry.has(PORTFOLIO_RELEASE_CHAT_ACTION)) {
registry.register(PORTFOLIO_RELEASE_CHAT_ACTION, {
paramsValidator: isPortfolioItemChatParams,
handler: governorGatedHandler(async (request) => releaseItem(readPortfolioItem(request?.params)), {
evaluateGate,
}),
});
}

if (!registry.has(PORTFOLIO_REQUEUE_CHAT_ACTION)) {
registry.register(PORTFOLIO_REQUEUE_CHAT_ACTION, {
paramsValidator: isPortfolioItemChatParams,
handler: governorGatedHandler(async (request) => requeueItem(readPortfolioItem(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/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-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