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
7 changes: 6 additions & 1 deletion packages/gittensory-miner/bin/gittensory-miner.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { runAttempt } from "../lib/attempt-cli.js";
import { printHelp, printVersion, runCli } from "../lib/cli.js";
import { configureLogger, extractLogOptions } from "../lib/logger.js";
import { runDenyCheck } from "../lib/deny-check.js";
import { runDiscover } from "../lib/discover-cli.js";
import { runFeasibilityCli } from "../lib/feasibility-cli.js";
Expand Down Expand Up @@ -31,7 +32,11 @@ import { resolveMinerVersion } from "../lib/version.js";
// cleanly instead of dying mid-write (#4826). Covers every subcommand below, including the local ones.
installCliSignalHandlers();

const cliArgs = process.argv.slice(2);
// Peel the global logging flags (--quiet/--verbose/--log-level) off the front of argv and configure the
// process-wide logger once (#4835), so every command below shares one level-aware logger without re-parsing
// them; the stripped `cliArgs` is what the command dispatch sees.
const { options: logOptions, rest: cliArgs } = extractLogOptions(process.argv.slice(2));
configureLogger({ ...logOptions, env: process.env });

// `status` and `doctor` are strictly local, offline commands — their contract is to make NO network calls.
// `init` stays local by default and only makes a network call when the operator explicitly passes
Expand Down
1 change: 1 addition & 0 deletions packages/gittensory-miner/docs/env-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Generated by `npm run miner:env-reference`. Do not edit manually.
| `GITTENSORY_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.js` | `""` |
| `GITTENSORY_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.js` | (none) |
| `GITTENSORY_MINER_KILL_SWITCH` | `lib/config-precedence.js` | `""` |
| `GITTENSORY_MINER_LOG_LEVEL` | `lib/logger.js` | `""` |
| `GITTENSORY_MINER_NO_UPDATE_CHECK` | `lib/update-check.js` | `""` |
| `GITTENSORY_MINER_ORB_EXPORT_DB` | `lib/orb-export.js` | `""` |
| `GITTENSORY_MINER_PLAN_STORE_DB` | `lib/plan-store.js` | `""` |
Expand Down
3 changes: 3 additions & 0 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export function printHelp(input) {
"",
"Options:",
" --no-update-check Skip the npm registry version nudge (also GITTENSORY_MINER_NO_UPDATE_CHECK=1)",
" --quiet Log only warnings and errors (also GITTENSORY_MINER_LOG_LEVEL=error)",
" --verbose Log debug-level diagnostics (also GITTENSORY_MINER_LOG_LEVEL=debug)",
" --log-level <lvl> Set the log level explicitly: silent|error|warn|info|debug",
].join("\n"),
);
}
Expand Down
58 changes: 58 additions & 0 deletions packages/gittensory-miner/lib/logger.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export type LogLevel = "silent" | "error" | "warn" | "info" | "debug";

export const LOG_LEVELS: readonly LogLevel[];
export const DEFAULT_LOG_LEVEL: LogLevel;

export function isLogLevel(value: unknown): value is LogLevel;

export function resolveLogLevel(signals?: {
level?: string | undefined;
quiet?: boolean | undefined;
verbose?: boolean | undefined;
envLevel?: string | undefined;
}): LogLevel;

export function extractLogOptions(argv: string[]): {
options: { quiet: boolean; verbose: boolean; level: string | undefined };
rest: string[];
};

export function formatFields(fields?: Record<string, unknown> | null | undefined): string;

export function formatLine(line: {
level: string;
message: string;
fields?: Record<string, unknown> | null | undefined;
pretty?: boolean | undefined;
timestamp?: string | undefined;
}): string;

export interface LoggerStreams {
stdout?: { write(chunk: string): unknown } | undefined;
stderr?: { write(chunk: string): unknown } | undefined;
}

export interface LoggerOptions {
level?: string | undefined;
quiet?: boolean | undefined;
verbose?: boolean | undefined;
pretty?: boolean | undefined;
fields?: Record<string, unknown> | undefined;
env?: Record<string, string | undefined> | undefined;
streams?: LoggerStreams | undefined;
now?: (() => string) | undefined;
}

export interface Logger {
level: LogLevel;
isLevelEnabled(level: string): boolean;
error(message: string, fields?: Record<string, unknown>): void;
warn(message: string, fields?: Record<string, unknown>): void;
info(message: string, fields?: Record<string, unknown>): void;
debug(message: string, fields?: Record<string, unknown>): void;
child(fields: Record<string, unknown>): Logger;
}

export function createLogger(options?: LoggerOptions): Logger;
export function configureLogger(options?: LoggerOptions): Logger;
export function getLogger(): Logger;
167 changes: 167 additions & 0 deletions packages/gittensory-miner/lib/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Level-aware logging abstraction for the miner CLI (#4835): every CLI file previously reached for ad hoc
// `console.log`/`console.error` with no shared level control, so an operator could neither quiet routine
// chatter nor turn on verbose diagnostics. This module is the one dependency-light logger the CLI configures
// once at startup and every command shares. It is deliberately pure/injectable — `streams`, `now`, and `env`
// are all overridable — so the branchy level/format logic is unit-testable without touching real stdio.
//
// Levels are ordered by severity; a logger at level L emits a method only when the method's severity rank is at
// or below L's rank (so `error` always survives except at `silent`, and `debug` only shows at the most verbose
// setting). `error`/`warn` go to stderr, `info`/`debug` to stdout, matching the existing convention where the
// update-check nudge writes to stderr and normal command output writes to stdout.

/** Supported log levels, least to most verbose. `silent` suppresses everything. */
export const LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];

/** The level used when nothing (flag, env var, or explicit option) selects one. */
export const DEFAULT_LOG_LEVEL = "info";

// Numeric severity rank per level (higher = more verbose). A method emits when its rank <= the active rank.
const LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };

const defaultClock = () => new Date().toISOString();

/** True when `value` names a supported log level. Non-string input is never a level (so an absent option or a
* typo'd env var falls through to the next signal instead of throwing). */
export function isLogLevel(value) {
return typeof value === "string" && Object.prototype.hasOwnProperty.call(LEVEL_RANK, value);
}

/**
* Resolve the active level from the available signals, most explicit first: an explicit `level` wins, then
* `--quiet` (→ `error`), then `--verbose` (→ `debug`), then the env-provided level, else the default. `quiet`
* beats `verbose` when both are set, so the safer/quieter choice wins a contradictory invocation. An
* unrecognized `level`/`envLevel` is ignored rather than throwing — a typo logs at the default, never crashes.
* @param {{ level?: string, quiet?: boolean, verbose?: boolean, envLevel?: string }} [signals]
* @returns {string}
*/
export function resolveLogLevel({ level, quiet = false, verbose = false, envLevel } = {}) {
if (isLogLevel(level)) return level;
if (quiet) return "error";
if (verbose) return "debug";
if (isLogLevel(envLevel)) return envLevel;
return DEFAULT_LOG_LEVEL;
}

/**
* Split the global logging flags out of a CLI argv slice, returning the parsed options plus `rest` — the argv
* with those flags (and any `--log-level` value) removed so downstream command parsing never sees them.
* Recognizes `--quiet`, `--verbose`, `--log-level <level>`, and `--log-level=<level>`. No short aliases: `-v`
* is already `--version` and `-h` is `--help` in the CLI entrypoint.
* @param {string[]} argv
* @returns {{ options: { quiet: boolean, verbose: boolean, level: string | undefined }, rest: string[] }}
*/
export function extractLogOptions(argv) {
let quiet = false;
let verbose = false;
let level;
const rest = [];
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--quiet") {
quiet = true;
continue;
}
if (arg === "--verbose") {
verbose = true;
continue;
}
if (arg === "--log-level") {
level = argv[index + 1];
index += 1;
continue;
}
if (arg.startsWith("--log-level=")) {
level = arg.slice("--log-level=".length);
continue;
}
rest.push(arg);
}
return { options: { quiet, verbose, level }, rest };
}

function formatFieldValue(value) {
// Quote a string only when it contains whitespace (so it stays one token); serialize everything else as JSON.
if (typeof value === "string") return /\s/.test(value) ? JSON.stringify(value) : value;
return JSON.stringify(value);
}

/**
* Render structured fields as a stable, sorted ` key=value` suffix (sorted so output is deterministic across
* runs). `undefined` values are dropped; an empty/absent field set yields an empty string.
* @param {Record<string, unknown> | null | undefined} fields
* @returns {string}
*/
export function formatFields(fields) {
if (!fields) return "";
const parts = [];
for (const key of Object.keys(fields).sort()) {
const value = fields[key];
if (value === undefined) continue;
parts.push(`${key}=${formatFieldValue(value)}`);
}
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
}

/**
* Format one log line. Plain mode (the default) is just `message` + any field suffix, keeping human CLI output
* identical to a bare `console.log`. Pretty mode prefixes an optional timestamp and the uppercased level tag,
* for operators who want machine-scannable diagnostics.
* @param {{ level: string, message: string, fields?: Record<string, unknown> | null, pretty?: boolean, timestamp?: string }} line
* @returns {string}
*/
export function formatLine({ level, message, fields, pretty, timestamp }) {
const suffix = formatFields(fields);
if (!pretty) return `${message}${suffix}`;
const stamp = timestamp ? `[${timestamp}] ` : "";
return `${stamp}${level.toUpperCase()} ${message}${suffix}`;
}

/**
* Build a level-aware logger. All I/O is injectable for tests: `streams` (defaults to process stdout/stderr),
* `now` (defaults to an ISO-8601 clock, only consulted in `pretty` mode), and `env` (defaults to process.env,
* read for `GITTENSORY_MINER_LOG_LEVEL`). `fields` seeds every line with contextual fields; `child(extra)`
* returns a logger that merges additional fields onto this one.
* @param {import("./logger.js").LoggerOptions} [options]
* @returns {import("./logger.js").Logger}
*/
export function createLogger(options = {}) {
const { level, quiet, verbose, pretty = false, fields: baseFields, env = process.env, streams, now } = options;
const stdout = streams?.stdout ?? process.stdout;
const stderr = streams?.stderr ?? process.stderr;
const clock = now ?? defaultClock;
const envLevel = env.GITTENSORY_MINER_LOG_LEVEL ?? "";
const activeLevel = resolveLogLevel({ level, quiet, verbose, envLevel });
const threshold = LEVEL_RANK[activeLevel];

function emit(methodLevel, stream, message, fields) {
if (LEVEL_RANK[methodLevel] > threshold) return;
const merged = baseFields || fields ? { ...baseFields, ...fields } : undefined;
const timestamp = pretty ? clock() : undefined;
stream.write(`${formatLine({ level: methodLevel, message, fields: merged, pretty, timestamp })}\n`);
}

return {
level: activeLevel,
isLevelEnabled: (methodLevel) => LEVEL_RANK[methodLevel] <= threshold,
error: (message, fields) => emit("error", stderr, message, fields),
warn: (message, fields) => emit("warn", stderr, message, fields),
info: (message, fields) => emit("info", stdout, message, fields),
debug: (message, fields) => emit("debug", stdout, message, fields),
child: (childFields) => createLogger({ ...options, fields: { ...baseFields, ...childFields } }),
};
}

// Process-wide logger. The CLI entrypoint calls `configureLogger` once from the parsed global flags/env so every
// command shares one configured instance via `getLogger`; until then this default-level instance is used.
let processLogger = createLogger();

/** Reconfigure the process-wide logger from resolved startup options and return it. */
export function configureLogger(options) {
processLogger = createLogger(options);
return processLogger;
}

/** The process-wide logger configured by `configureLogger` (a default-level logger before then). */
export function getLogger() {
return processLogger;
}
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/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/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-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/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": {
"@jsonbored/gittensory-engine": "*",
Expand Down
Loading