Skip to content
Closed
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
4 changes: 3 additions & 1 deletion packages/gittensory-miner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ package manager, and the build / test / lint / format commands — for Node (npm
(pip/poetry/pipenv/uv), Rust, Go, Maven, and Gradle. It is pure (injectable `existsSync` / `readFileSync`), never
throws, and per its acceptance criteria **fails closed** — a repo with no recognized manifest returns
`{ detected: false, reason }` and a command that can't be inferred without guessing stays `null`, rather than being
assumed. Detection only; wiring the description into the attempt prompt is the follow-up ([#4786](https://github.com/JSONbored/gittensory/issues/4786)). (#4785)
assumed. The attempt path consumes it: `buildCodingTaskSpec` appends the real stack summary (and any
confidently-inferred build/test/lint/format commands) to the coding-agent instructions so validation uses
the target repo's own tooling rather than assuming LoopOver/gittensory CI ([#4786](https://github.com/JSONbored/gittensory/issues/4786)). (#4785)

The package also includes an append-only governor decision ledger: `initGovernorLedger` / `appendGovernorEvent`
persist structured allow/deny/throttle/kill-switch outcomes in local SQLite for contributor audit. Insert-only —
Expand Down
3 changes: 3 additions & 0 deletions packages/gittensory-miner/lib/coding-task-spec.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AcceptanceCriteria, FeasibilityGateResult, FeasibilityVerdict, IssueRecord, PullRequestRecord } from "@loopover/engine";
import type { RepoStackResult } from "./stack-detection.js";

export type CodingTaskIssue = { number: number; title: string; body?: string | null | undefined; labels?: string[] | undefined };

Expand All @@ -25,6 +26,8 @@ export type CodingTaskSpecInput = {
context: CodingTaskContext;
claimLedger: CodingTaskClaimLedger;
workingDirectory: string;
/** Injectable stack detector (#4786); omitted falls back to stack-detection.js's real `detectRepoStack`. */
detectRepoStack?: (repoPath: string) => RepoStackResult;
};

export type CodingTaskSpecResult =
Expand Down
73 changes: 66 additions & 7 deletions packages/gittensory-miner/lib/coding-task-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
serializeAcceptanceCriteria,
shouldWriteAcceptanceCriteria,
} from "@loopover/engine";
import { detectRepoStack, renderStackSummary } from "./stack-detection.js";

// Coding-task-spec builder (#5132, Wave 3.5 follow-up). The second gap discovered alongside #5132's CLI
// wiring: `IterateLoopInput.title`/`instructions`/`acceptanceCriteriaPath` had no builder anywhere in this
Expand All @@ -24,6 +25,12 @@ import {
// @loopover/engine (same gap #5145's own header documents for `issueQuality`). This is not a
// fabrication -- feasibilityInputFromPreStartCheck's OWN documented default for a missing
// issueQualityStatus/lifecycle is "ready", the same honest-default precedent already established.
//
// Target-repo stack detection (#4786 / #4785 follow-up): `detectRepoStack` already returned a structured
// language/package-manager/command description, but nothing in the attempt path consumed it -- instructions
// were issue text + an acceptance-criteria path only. This module now appends that real stack summary (and
// any confidently-inferred validation commands) to the coding-agent prompt so the agent validates against
// THIS repository's tooling rather than assuming LoopOver/gittensory CI, Codecov, or `npm run test:ci`.

function buildTaskBrief(issue) {
const body = (issue.body ?? "").trim();
Expand Down Expand Up @@ -131,32 +138,78 @@ export function writeAcceptanceCriteriaFile(workingDirectory, acceptanceCriteria
return { written: true, path };
}

/**
* Prompt guidance derived from a real `detectRepoStack` result (#4786). Lists only commands the detector
* confidently inferred -- a `null` command stays omitted rather than guessed -- and always tells the agent
* not to assume LoopOver/gittensory's own CI/coverage conventions.
*
* @param {import("./stack-detection.js").RepoStackResult} stack
* @returns {string}
*/
function buildValidationGuidance(stack) {
const lines = [
`Detected target-repo stack: ${renderStackSummary(stack)}`,
"",
"Validate your change with THIS repository's own build/test/lint tooling from the stack summary above.",
"Do not assume LoopOver/gittensory CI conventions, Codecov patch coverage, or `npm run test:ci` unless those commands appear in the detected stack.",
];
if (stack?.detected === true) {
const commands = [
stack.testCommand ? `- test: \`${stack.testCommand}\`` : null,
stack.lintCommand ? `- lint: \`${stack.lintCommand}\`` : null,
stack.buildCommand ? `- build: \`${stack.buildCommand}\`` : null,
stack.formatCommand ? `- format: \`${stack.formatCommand}\`` : null,
].filter((entry) => entry !== null);
if (commands.length > 0) {
lines.push("", "Run these commands before finishing:", ...commands);
} else {
lines.push(
"",
"No build/test/lint/format commands were confidently inferred — discover and use this repo's own tooling rather than guessing.",
);
}
}
return lines.join("\n");
}

/**
* The coding-agent driver's own prompt text (agent-sdk-driver.ts's header: "forwarded verbatim as the
* prompt -- the acceptance-criteria document already lives inside the worktree", so this points to it
* rather than repeating its content).
* rather than repeating its content). Also carries the target repo's detected stack + validation commands
* (#4786) so the agent does not default to gittensory-specific CI assumptions.
*
* @param {{ number: number, title: string, body?: string | null }} issue
* @param {string} acceptanceCriteriaPath
* @param {import("./stack-detection.js").RepoStackResult} stack
*/
function buildInstructions(issue, acceptanceCriteriaPath) {
function buildInstructions(issue, acceptanceCriteriaPath, stack) {
return [
`Resolve the following GitHub issue in this repository: #${issue.number} -- ${issue.title}`,
"",
(issue.body ?? "").trim(),
"",
`A structured acceptance-criteria document describing what "done" means for this attempt is at ${acceptanceCriteriaPath} -- read it and ensure your change satisfies every criterion before finishing.`,
"",
buildValidationGuidance(stack),
].join("\n");
}

/**
* Full composition: feasibility -> acceptance criteria -> (if authorized) write the file -> instructions.
* Returns `ready: false` (with the computed feasibility verdict, for the caller to report) when the
* verdict is `raise`/`avoid` -- the caller should abandon the attempt rather than proceed with no real
* acceptance-criteria file on disk.
* Full composition: feasibility -> acceptance criteria -> (if authorized) write the file -> detect the
* target-repo stack (#4786) -> instructions. Returns `ready: false` (with the computed feasibility verdict,
* for the caller to report) when the verdict is `raise`/`avoid` -- the caller should abandon the attempt
* rather than proceed with no real acceptance-criteria file on disk.
*
* `detectRepoStack` is injectable so tests can assert both the detected and fail-closed undiscovered stack
* branches without depending on real filesystem probes; omitted falls back to stack-detection.js's real
* `detectRepoStack` (the production default).
*
* @param {{
* repoFullName: string, issue: { number: number, title: string, body?: string | null, labels?: string[] },
* context: { issues: Array<{ number: number }>, pullRequests: unknown[] },
* claimLedger: { listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> },
* workingDirectory: string,
* detectRepoStack?: (repoPath: string) => import("./stack-detection.js").RepoStackResult,
* }} input
* @returns {import("./coding-task-spec.js").CodingTaskSpecResult}
*/
Expand All @@ -169,12 +222,18 @@ export function buildCodingTaskSpec(input) {
return { ready: false, verdict: feasibility.verdict, feasibility };
}

// Real target-repo stack (#4786): detected from the prepared worktree's own manifests, not guessed from
// gittensory conventions. Fail-closed `{ detected: false }` results still reach the prompt (via
// renderStackSummary) so the agent is told detection failed rather than silently defaulting to npm/Codecov.
const detect = input.detectRepoStack ?? detectRepoStack;
const stack = detect(input.workingDirectory);

return {
ready: true,
verdict: feasibility.verdict,
feasibility,
acceptanceCriteriaPath: writeResult.path,
instructions: buildInstructions(input.issue, writeResult.path),
instructions: buildInstructions(input.issue, writeResult.path, stack),
title: input.issue.title,
body: input.issue.body ?? undefined,
labels: input.issue.labels,
Expand Down
148 changes: 148 additions & 0 deletions test/unit/miner-coding-task-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,152 @@ describe("buildCodingTaskSpec (#5132)", () => {
if (!result.ready) throw new Error("expected ready");
expect(result.body).toBeUndefined();
});

it("REGRESSION (#4786): embeds a detected Node stack's real test/build/lint commands in the coding-agent instructions", () => {
const dir = tempDir();
writeFileSync(
join(dir, "package.json"),
JSON.stringify({
name: "widgets",
scripts: { test: "vitest run", build: "tsc -b", lint: "eslint .", format: "prettier --write ." },
}),
);
const target = issue();
const result = buildCodingTaskSpec({
repoFullName: "acme/widgets",
issue: target,
context: { issues: [target], pullRequests: [] },
claimLedger: claimLedger(),
workingDirectory: dir,
});

expect(result.ready).toBe(true);
if (!result.ready) throw new Error("expected ready");
expect(result.instructions).toContain("Detected target-repo stack:");
expect(result.instructions).toMatch(/javascript|typescript/i);
expect(result.instructions).toContain("npm");
expect(result.instructions).toContain("Do not assume LoopOver/gittensory CI conventions");
expect(result.instructions).toContain("- test: `");
expect(result.instructions).toContain("- build: `");
expect(result.instructions).toContain("- lint: `");
expect(result.instructions).toContain("- format: `");
expect(result.instructions).not.toContain("No build/test/lint/format commands were confidently inferred");
});

it("REGRESSION (#4786): a fail-closed undetected stack still reaches the prompt (no silent gittensory default)", () => {
const dir = tempDir();
const target = issue();
const result = buildCodingTaskSpec({
repoFullName: "acme/widgets",
issue: target,
context: { issues: [target], pullRequests: [] },
claimLedger: claimLedger(),
workingDirectory: dir,
});

expect(result.ready).toBe(true);
if (!result.ready) throw new Error("expected ready");
expect(result.instructions).toContain("Detected target-repo stack: stack not detected:");
expect(result.instructions).toContain("Do not assume LoopOver/gittensory CI conventions");
expect(result.instructions).not.toContain("Run these commands before finishing:");
});

it("REGRESSION (#4786): a detected stack with no confidently-inferred commands tells the agent not to guess", () => {
const dir = tempDir();
const target = issue();
const result = buildCodingTaskSpec({
repoFullName: "acme/widgets",
issue: target,
context: { issues: [target], pullRequests: [] },
claimLedger: claimLedger(),
workingDirectory: dir,
detectRepoStack: () => ({
detected: true,
language: "python",
packageManager: "pip",
buildCommand: null,
testCommand: null,
lintCommand: null,
formatCommand: null,
evidence: { manifest: "requirements.txt", lockfile: null },
}),
});

expect(result.ready).toBe(true);
if (!result.ready) throw new Error("expected ready");
expect(result.instructions).toContain("python via pip");
expect(result.instructions).toContain("no validation commands detected");
expect(result.instructions).toContain("No build/test/lint/format commands were confidently inferred");
expect(result.instructions).not.toContain("Run these commands before finishing:");
});

it("REGRESSION (#4786): when input.detectRepoStack is omitted, uses the REAL stack-detection.js default against the worktree", () => {
const dir = tempDir();
writeFileSync(join(dir, "Cargo.toml"), '[package]\nname = "widgets"\nversion = "0.1.0"\n');
const target = issue();
const result = buildCodingTaskSpec({
repoFullName: "acme/widgets",
issue: target,
context: { issues: [target], pullRequests: [] },
claimLedger: claimLedger(),
workingDirectory: dir,
});

expect(result.ready).toBe(true);
if (!result.ready) throw new Error("expected ready");
expect(result.instructions).toContain("rust via cargo");
expect(result.instructions).toContain("- test: `cargo test`");
expect(result.instructions).toContain("- build: `cargo build`");
});

it("REGRESSION (#4786): includes only the non-null commands from a partial injected stack (both sides of each command ternary)", () => {
const target = issue();
const withBuildAndTest = buildCodingTaskSpec({
repoFullName: "acme/widgets",
issue: target,
context: { issues: [target], pullRequests: [] },
claimLedger: claimLedger(),
workingDirectory: tempDir(),
detectRepoStack: () => ({
detected: true,
language: "go",
packageManager: "go",
buildCommand: "go build ./...",
testCommand: "go test ./...",
lintCommand: null,
formatCommand: null,
evidence: { manifest: "go.mod", lockfile: null },
}),
});
expect(withBuildAndTest.ready).toBe(true);
if (!withBuildAndTest.ready) throw new Error("expected ready");
expect(withBuildAndTest.instructions).toContain("- test: `go test ./...`");
expect(withBuildAndTest.instructions).toContain("- build: `go build ./...`");
expect(withBuildAndTest.instructions).not.toContain("- lint:");
expect(withBuildAndTest.instructions).not.toContain("- format:");

const withLintAndFormat = buildCodingTaskSpec({
repoFullName: "acme/widgets",
issue: target,
context: { issues: [target], pullRequests: [] },
claimLedger: claimLedger(),
workingDirectory: tempDir(),
detectRepoStack: () => ({
detected: true,
language: "javascript",
packageManager: "npm",
buildCommand: null,
testCommand: null,
lintCommand: "npm run lint",
formatCommand: "npm run format",
evidence: { manifest: "package.json", lockfile: null },
}),
});
expect(withLintAndFormat.ready).toBe(true);
if (!withLintAndFormat.ready) throw new Error("expected ready");
expect(withLintAndFormat.instructions).toContain("- lint: `npm run lint`");
expect(withLintAndFormat.instructions).toContain("- format: `npm run format`");
expect(withLintAndFormat.instructions).not.toContain("- test:");
expect(withLintAndFormat.instructions).not.toContain("- build:");
});
});
Loading