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
1 change: 1 addition & 0 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function printHelp(input) {
" gittensory-miner governor pause [--reason <text>] [--dry-run] [--json] Stop the loop before its next cycle",
" gittensory-miner governor resume [--dry-run] [--json] Let a paused loop continue",
" gittensory-miner governor status [--json] Show whether the governor is paused",
" gittensory-miner governor metrics Print governor rate-limit/cap-usage counters in Prometheus text format",
" gittensory-miner calibration [--json] Report predicted-vs-realized gate accuracy",
" gittensory-miner feasibility <claimStatus> <duplicateClusterRisk> <issueStatus> [--not-found] [--json]",
" gittensory-miner hooks check --tool <name> --input <json> [--json]",
Expand Down
2 changes: 1 addition & 1 deletion packages/gittensory-miner/lib/governor-ledger-cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ export function runGovernorList(
export function runGovernorCli(
subcommand: string | undefined,
args: string[],
options?: { initGovernorLedger?: () => GovernorLedger } & GovernorPauseCliOptions,
options?: { initGovernorLedger?: () => GovernorLedger; nowMs?: number } & GovernorPauseCliOptions,
): Promise<number>;
3 changes: 3 additions & 0 deletions packages/gittensory-miner/lib/governor-ledger-cli.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { runGovernorPause, runGovernorResume, runGovernorStatus } from "./governor-pause-cli.js";
import { runGovernorMetrics } from "./governor-metrics-cli.js";

/** Must match `GOVERNOR_LEDGER_EVENT_TYPES` in `@loopover/engine`. */
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
Expand All @@ -18,6 +19,7 @@ const GOVERNOR_SUBCOMMAND_USAGE = [
" gittensory-miner governor pause [--reason <text>] [--json]",
" gittensory-miner governor resume [--json]",
" gittensory-miner governor status [--json]",
" gittensory-miner governor metrics",
].join("\n");

function parseRepoArg(value, usage) {
Expand Down Expand Up @@ -148,6 +150,7 @@ export async function runGovernorCli(subcommand, args, options = {}) {
if (subcommand === "pause") return runGovernorPause(args, options);
if (subcommand === "resume") return runGovernorResume(args, options);
if (subcommand === "status") return runGovernorStatus(args, options);
if (subcommand === "metrics") return runGovernorMetrics(args, options);
return reportCliFailure(
argsWantJson(args),
`Unknown governor subcommand: ${subcommand ?? ""}.\n${GOVERNOR_SUBCOMMAND_USAGE}`,
Expand Down
16 changes: 16 additions & 0 deletions packages/gittensory-miner/lib/governor-metrics-cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { GovernorCapUsage } from "@loopover/engine";
import type { GovernorRateLimitState, GovernorState } from "./governor-state.js";

export const GOVERNOR_RATE_LIMIT_REMAINING_RATIO: string;
export const GOVERNOR_CAP_USAGE_RATIO: string;

export function renderGovernorMetrics(
rateLimitState: GovernorRateLimitState,
capUsage: GovernorCapUsage,
nowMs: number,
): string;

export function runGovernorMetrics(
args: string[],
options?: { openGovernorState?: () => GovernorState; nowMs?: number },
): Promise<number>;
171 changes: 171 additions & 0 deletions packages/gittensory-miner/lib/governor-metrics-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {
DEFAULT_AMS_POLICY_SPEC,
DEFAULT_WRITE_RATE_LIMIT_POLICIES,
evaluateGovernorCaps,
evaluateLocalRateLimit,
} from "@loopover/engine";
import { openGovernorState } from "./governor-state.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

// `governor metrics` (#5187): render the governor's persisted rate-limit + cap-usage state (#5134,
// governor-state.js) as Prometheus text-exposition, so an operator's Alertmanager can page on rate-limit/
// budget pressure without hand-rolling a scrape. Strictly read-only, mirroring queue-cli.js's `queue metrics`
// (#5186) and event-ledger-cli.js's `ledger metrics` (#4841): opens the local governor-state store, composes
// its EXISTING loadRateLimitState()/loadCapUsage() with the engine's already-exported PURE calculators
// (evaluateLocalRateLimit, evaluateGovernorCaps) against the SAME defaults the production loop (loop-cli.js)
// already falls back to when no `.gittensory-ams.yml` override is configured (DEFAULT_WRITE_RATE_LIMIT_POLICIES,
// DEFAULT_AMS_POLICY_SPEC.capLimits) -- it never invents a threshold of its own, and it does not gate, retry,
// mutate, or otherwise touch governor decision logic (governor-chokepoint.js/governor-chokepoint-persisted.js
// are completely untouched by this file).
//
// capLimits is intentionally NOT read per-repo: governor-state.js's capUsage row is a single global scalar (a
// run-scoped cumulative counter, not indexed by repo -- see governor-state.js's own header comment), so a
// per-repo capLimits override from a resolved `.gittensory-miner.yml` has no matching per-repo usage row to
// pair it with here. Using the fleet-wide DEFAULT_AMS_POLICY_SPEC.capLimits is the same approximation
// loop-cli.js itself already makes for any repo without its own override.

const GOVERNOR_METRICS_USAGE = "Usage: gittensory-miner governor metrics";

export const GOVERNOR_RATE_LIMIT_REMAINING_RATIO = "gittensory_miner_governor_rate_limit_remaining_ratio";
export const GOVERNOR_CAP_USAGE_RATIO = "gittensory_miner_governor_cap_usage_ratio";

/** HELP-text escaping — backslash + newline (mirrors miner-prediction-metrics.ts's escapeHelpText). */
function escapeMetricsHelpText(help) {
return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
}

/** Prometheus label-value escaping — backslash, double-quote, newline (mirrors event-ledger-cli.js's
* escapeLabelValue). */
function escapeLabelValue(value) {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
}

/** buckets.perRepo is keyed by writeRateLimitRepoKey(actionClass, repoFullName) = "actionClass:repoFullName"
* (write-rate-limit.ts). actionClass is a fixed identifier (never contains ":"), so splitting on the FIRST
* colon recovers both parts even though repoFullName itself contains a "/". */
function splitPerRepoKey(key) {
const separatorIndex = key.indexOf(":");
if (separatorIndex === -1) return { actionClass: key, repoFullName: "" };
return { actionClass: key.slice(0, separatorIndex), repoFullName: key.slice(separatorIndex + 1) };
}

// evaluateLocalRateLimit's own `remaining` field answers "how many MORE writes are allowed AFTER one more write
// right now" (rate-limit.ts: `remaining = allowed ? limit - effectiveCount - 1 : 0`) -- it is NOT current
// headroom. At count=2/limit=3 that field is already 0, identical to a fully exhausted count=3/limit=3 bucket,
// even though the count=2 bucket still has one write available. Recover true current headroom algebraically
// instead: when allowed, decision.remaining + 1 is exactly limit - effectiveCount (undo the "-1 for this next
// write" the decision already applied); when not allowed, headroom is 0. Every actionClass this loop reaches
// has already passed the DEFAULT_WRITE_RATE_LIMIT_POLICIES lookup above, so decision.limit is always one of the
// frozen, non-zero policy limits -- no zero-limit guard needed.
function remainingRatio(decision) {
const headroom = decision.allowed ? decision.remaining + 1 : 0;
return headroom / decision.limit;
}

function collectRateLimitRows(buckets, nowMs) {
const rows = [];
for (const [actionClass, bucket] of Object.entries(buckets.global)) {
const config = DEFAULT_WRITE_RATE_LIMIT_POLICIES.global[actionClass];
if (!config) continue;
rows.push({
scope: "global",
actionClass,
repoFullName: "",
ratio: remainingRatio(evaluateLocalRateLimit(bucket, config, nowMs)),
});
}
for (const [key, bucket] of Object.entries(buckets.perRepo)) {
const { actionClass, repoFullName } = splitPerRepoKey(key);
const config = DEFAULT_WRITE_RATE_LIMIT_POLICIES.perRepo[actionClass];
if (!config) continue;
rows.push({
scope: "per_repo",
actionClass,
repoFullName,
ratio: remainingRatio(evaluateLocalRateLimit(bucket, config, nowMs)),
});
}
rows.sort((a, b) => {
if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
if (a.actionClass !== b.actionClass) return a.actionClass.localeCompare(b.actionClass);
return a.repoFullName.localeCompare(b.repoFullName);
});
return rows;
}

// DEFAULT_AMS_POLICY_SPEC.capLimits is a frozen, non-zero constant for every dimension -- no zero-limit guard
// needed, mirroring remainingRatio()'s reasoning above.
function collectCapUsageRows(capUsage) {
const report = evaluateGovernorCaps(capUsage, DEFAULT_AMS_POLICY_SPEC.capLimits);
return [
{ dimension: "budget", dimensionReport: report.budget },
{ dimension: "turns", dimensionReport: report.turns },
{ dimension: "elapsed_ms", dimensionReport: report.termination },
].map(({ dimension, dimensionReport }) => ({
dimension,
ratio: dimensionReport.used / dimensionReport.limit,
}));
}

/**
* @param {import("./governor-state.js").GovernorRateLimitState} rateLimitState
* @param {import("@loopover/engine").GovernorCapUsage} capUsage
* @param {number} nowMs
*/
export function renderGovernorMetrics(rateLimitState, capUsage, nowMs) {
const rateLimitRows = collectRateLimitRows(rateLimitState.buckets, nowMs);
const capRows = collectCapUsageRows(capUsage);

const lines = [
`# HELP ${GOVERNOR_RATE_LIMIT_REMAINING_RATIO} ${escapeMetricsHelpText(
"Remaining headroom in the governor's current write-rate-limit window, as a fraction of the configured limit (1 = empty bucket, 0 = exhausted). Evaluated against DEFAULT_WRITE_RATE_LIMIT_POLICIES.",
)}`,
`# TYPE ${GOVERNOR_RATE_LIMIT_REMAINING_RATIO} gauge`,
];
for (const row of rateLimitRows) {
const repoLabel = row.scope === "per_repo" ? `,repo="${escapeLabelValue(row.repoFullName)}"` : "";
lines.push(
`${GOVERNOR_RATE_LIMIT_REMAINING_RATIO}{scope="${row.scope}",action_class="${escapeLabelValue(row.actionClass)}"${repoLabel}} ${row.ratio}`,
);
}

lines.push(
`# HELP ${GOVERNOR_CAP_USAGE_RATIO} ${escapeMetricsHelpText(
"The governor's persisted cumulative cap usage as a fraction of DEFAULT_AMS_POLICY_SPEC.capLimits (1 = ceiling reached). dimension is one of budget|turns|elapsed_ms.",
)}`,
);
lines.push(`# TYPE ${GOVERNOR_CAP_USAGE_RATIO} gauge`);
for (const row of capRows) {
lines.push(`${GOVERNOR_CAP_USAGE_RATIO}{dimension="${row.dimension}"} ${row.ratio}`);
}

return `${lines.join("\n")}\n`;
}

async function withGovernorState(options, run) {
const ownsGovernorState = options.openGovernorState === undefined;
const governorState = (options.openGovernorState ?? openGovernorState)();
try {
return run(governorState);
} finally {
if (ownsGovernorState) governorState.close();
}
}

export async function runGovernorMetrics(args, options = {}) {
if (args.length > 0) {
return reportCliFailure(argsWantJson(args), GOVERNOR_METRICS_USAGE);
}

try {
return await withGovernorState(options, (governorState) => {
const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
const rateLimitState = governorState.loadRateLimitState();
const capUsage = governorState.loadCapUsage();
console.log(renderGovernorMetrics(rateLimitState, capUsage, nowMs).trimEnd());
return 0;
});
} catch (error) {
return reportCliFailure(argsWantJson(args), describeCliError(error));
}
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"expected-engine.version"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check bin/gittensory-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/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/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-action-mode.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-open-pr.js && node --check lib/governor-pause-cli.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.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/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-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/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/gittensory-miner.js && node --check bin/gittensory-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/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/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-action-mode.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-open-pr.js && node --check lib/governor-pause-cli.js && node --check lib/governor-run-halt.js && node --check lib/governor-state.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.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/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-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/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": "*",
Expand Down
Loading