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
9 changes: 9 additions & 0 deletions packages/gittensory-miner/lib/miner-goal-spec.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ParsedMinerGoalSpec } from "@jsonbored/gittensory-engine";

export function resolveMinerGoalSpec(
repoPath: string,
options?: {
existsSync?: (path: string) => boolean;
readFileSync?: (path: string, encoding: "utf8") => string;
},
): ParsedMinerGoalSpec;
38 changes: 38 additions & 0 deletions packages/gittensory-miner/lib/miner-goal-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { discoverMinerGoalSpecPath, parseMinerGoalSpecContent } from "@jsonbored/gittensory-engine";

// Real local .gittensory-miner.yml resolver (#5132, Wave 3.5 follow-up). MinerGoalSpec's own discovery
// helper (discoverMinerGoalSpecPath, packages/gittensory-engine) is deliberately IO-free -- the caller
// injects the existence check. Unlike self-review-context.js/rejection-signal.js/ams-policy.js, which fetch
// their target repo's files live over raw.githubusercontent.com BEFORE any clone exists, this resolver reads
// the ALREADY-CLONED repo on disk (attempt-worktree.js's prepareAttemptWorktree runs first in the real
// attempt-cli.js flow) -- no extra network round trip needed for a file that's already sitting in the
// worktree.

/**
* Resolve the real, parsed MinerGoalSpec for an already-cloned repo at `repoPath`, trying each
* MINER_GOAL_SPEC_FILENAMES candidate in the documented discovery order. Never throws: a missing file, an
* unreadable file, or malformed content all degrade to the tolerant parser's own absent/safe-default result.
*
* `options.existsSync`/`options.readFileSync`, when injected, always receive the FULL joined path (same
* convention as `node:fs`'s own functions), not a repoPath-relative candidate.
*
* @param {string} repoPath
* @param {{ existsSync?: (path: string) => boolean, readFileSync?: (path: string, encoding: "utf8") => string }} [options]
* @returns {import("@jsonbored/gittensory-engine").ParsedMinerGoalSpec}
*/
export function resolveMinerGoalSpec(repoPath, options = {}) {
const existsImpl = options.existsSync ?? existsSync;
const readImpl = options.readFileSync ?? readFileSync;

const relativePath = discoverMinerGoalSpecPath((candidate) => existsImpl(join(repoPath, candidate)));
if (!relativePath) return parseMinerGoalSpecContent(null);

try {
const content = readImpl(join(repoPath, relativePath), "utf8");
return parseMinerGoalSpecContent(content);
} catch {
return parseMinerGoalSpecContent(null);
}
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.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-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/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/gate-verdict-poller.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-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-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.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/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-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/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-log.js && node --check lib/attempt-runner.js && node --check lib/attempt-worktree.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-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/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/gate-verdict-poller.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-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-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.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/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-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/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
77 changes: 77 additions & 0 deletions test/unit/miner-goal-spec-resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";

vi.mock("@jsonbored/gittensory-engine", async () => {
return import("../../packages/gittensory-engine/src/index");
});

import { resolveMinerGoalSpec } from "../../packages/gittensory-miner/lib/miner-goal-spec.js";

const roots: string[] = [];

afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});

function tempRepo() {
const root = mkdtempSync(join(tmpdir(), "gittensory-miner-goal-spec-"));
roots.push(root);
return root;
}

describe("resolveMinerGoalSpec (#5132)", () => {
it("returns an absent safe-default spec when no candidate file exists", () => {
const repoPath = tempRepo();
const parsed = resolveMinerGoalSpec(repoPath);
expect(parsed.present).toBe(false);
expect(parsed.spec.killSwitch).toEqual({ paused: false });
});

it("REGRESSION: reads a real .gittensory-miner.yml from the cloned repo's root", () => {
const repoPath = tempRepo();
writeFileSync(join(repoPath, ".gittensory-miner.yml"), "killSwitch:\n paused: true\n");
const parsed = resolveMinerGoalSpec(repoPath);
expect(parsed.present).toBe(true);
expect(parsed.spec.killSwitch).toEqual({ paused: true });
});

it("tries .github/gittensory-miner.yml when the root .yml is absent", () => {
const repoPath = tempRepo();
mkdirSync(join(repoPath, ".github"), { recursive: true });
writeFileSync(join(repoPath, ".github", "gittensory-miner.yml"), "killSwitch:\n paused: true\n");
const parsed = resolveMinerGoalSpec(repoPath);
expect(parsed.present).toBe(true);
expect(parsed.spec.killSwitch.paused).toBe(true);
});

it("tries the .json variants after both .yml candidates are absent", () => {
const repoPath = tempRepo();
writeFileSync(join(repoPath, ".gittensory-miner.json"), JSON.stringify({ killSwitch: { paused: true } }));
const parsed = resolveMinerGoalSpec(repoPath);
expect(parsed.present).toBe(true);
expect(parsed.spec.killSwitch.paused).toBe(true);
});

it("degrades to safe defaults on malformed content instead of throwing", () => {
const repoPath = tempRepo();
writeFileSync(join(repoPath, ".gittensory-miner.yml"), "killSwitch: [unterminated");
const parsed = resolveMinerGoalSpec(repoPath);
expect(parsed.present).toBe(false);
expect(parsed.spec.killSwitch).toEqual({ paused: false });
expect(parsed.warnings.join(" ")).toMatch(/not valid YAML/i);
});

it("degrades to safe defaults when the discovered file can't actually be read", () => {
const repoPath = tempRepo();
const parsed = resolveMinerGoalSpec(repoPath, {
existsSync: (path) => path.endsWith(".gittensory-miner.yml") && !path.includes(".github"),
readFileSync: () => {
throw new Error("EACCES: permission denied");
},
});
expect(parsed.present).toBe(false);
expect(parsed.spec.killSwitch).toEqual({ paused: false });
});
});