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
44 changes: 44 additions & 0 deletions packages/gittensory-miner/lib/coding-task-spec.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { AcceptanceCriteria, FeasibilityGateResult, FeasibilityVerdict, IssueRecord, PullRequestRecord } from "@jsonbored/gittensory-engine";

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

export type CodingTaskClaimLedger = {
listClaims(filter: { repoFullName: string; status: string }): Array<{ issueNumber: number }>;
};

export type CodingTaskContext = { issues: IssueRecord[]; pullRequests: PullRequestRecord[] };

export function buildCodingTaskFeasibility(
repoFullName: string,
issue: CodingTaskIssue,
context: CodingTaskContext,
claimLedger: CodingTaskClaimLedger,
): FeasibilityGateResult;

export function buildCodingTaskAcceptanceCriteria(issue: CodingTaskIssue, feasibility: FeasibilityGateResult): AcceptanceCriteria;

export function writeAcceptanceCriteriaFile(workingDirectory: string, acceptanceCriteria: AcceptanceCriteria): { written: boolean; path: string | null };

export type CodingTaskSpecInput = {
repoFullName: string;
issue: CodingTaskIssue;
context: CodingTaskContext;
claimLedger: CodingTaskClaimLedger;
workingDirectory: string;
};

export type CodingTaskSpecResult =
| { ready: false; verdict: FeasibilityVerdict; feasibility: FeasibilityGateResult }
| {
ready: true;
verdict: FeasibilityVerdict;
feasibility: FeasibilityGateResult;
acceptanceCriteriaPath: string;
instructions: string;
title: string;
body: string | undefined;
labels: string[] | undefined;
linkedIssues: number[];
};

export function buildCodingTaskSpec(input: CodingTaskSpecInput): CodingTaskSpecResult;
167 changes: 167 additions & 0 deletions packages/gittensory-miner/lib/coding-task-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import {
ACCEPTANCE_CRITERIA_FILENAME,
buildAcceptanceCriteria,
buildCollisionReport,
buildFeasibilityVerdict,
buildPromptPacket,
feasibilityInputFromPreStartCheck,
serializeAcceptanceCriteria,
shouldWriteAcceptanceCriteria,
} from "@jsonbored/gittensory-engine";

// 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
// package. `packages/gittensory-engine/src/miner/acceptance-criteria.ts` already composes a PromptPacket +
// FeasibilityGateResult into an immutable AcceptanceCriteria document (and deliberately does NOT write it --
// "actually writing it into the attempt's worktree is the worktree primitive's job", per its own header) --
// this module is that caller: derives the four inputs from a real target issue + the already-fetched
// SelfReviewContext (#5145), then writes the file for real.
//
// issueStatus is intentionally left undefined when computing feasibility: buildIssueQualityReport (the only
// thing that could supply it) lives only in root src/signals/engine.ts and has never been extracted into
// @jsonbored/gittensory-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.

function buildTaskBrief(issue) {
const body = (issue.body ?? "").trim();
return body ? `${issue.title}\n\n${body}` : issue.title;
}

function buildConstraints(issue) {
if (!Array.isArray(issue.labels) || issue.labels.length === 0) return "";
return `Labels on this issue: ${issue.labels.join(", ")}.`;
}

function buildFeasibilityNotes(feasibility) {
return [feasibility.summary, ...feasibility.avoidReasons, ...feasibility.raiseReasons].join("\n");
}

// Only ever resolves to "claimed"/"unclaimed": the claim ledger's own ClaimStatus vocabulary
// ("active"|"released"|"expired") has no "solved" concept for FeasibilityClaimStatus's "solved" value to
// map from -- that would need real evidence a PR already resolved the issue (e.g. a merged, linked PR),
// which this function doesn't have access to. Not fabricated; genuinely undetectable from claim data alone.
function resolveClaimStatus(claimLedger, repoFullName, issueNumber) {
const claims = claimLedger.listClaims({ repoFullName, status: "active" });
return claims.some((claim) => claim.issueNumber === issueNumber) ? "claimed" : "unclaimed";
}

// The target issue's own raw cluster risk from buildCollisionReport (newly exported from
// @jsonbored/gittensory-engine's public barrel) -- "none" when the issue isn't part of any cluster at all.
// DELIBERATELY does NOT apply #5145's ">= 2 pull_request items" threshold: that gate exists specifically to
// stop inDuplicateCluster (self-review, "does MY OWN just-created submission look redundant") from firing on
// the ordinary case of one existing PR already legitimately closing the issue. Feasibility asks a different
// question -- "should I even START working on this issue" -- where an issue already having ANY open PR
// against it (buildCollisionReport's pairwise "shared linked issue" rule, which fires at "high" for exactly
// one PR) is a meaningful, real caution signal, not a false positive to filter out.
function resolveDuplicateClusterRisk(repoFullName, issues, pullRequests, issueNumber) {
const report = buildCollisionReport(repoFullName, issues, pullRequests);
const cluster = report.clusters.find((entry) => entry.items.some((item) => item.type === "issue" && item.number === issueNumber));
return cluster ? cluster.risk : "none";
}

/**
* Compute the feasibility verdict for one target issue, from real signals: whether the issue is present in
* the fetched context, its real claim status (the claim ledger), and its real duplicate-cluster risk
* (buildCollisionReport over the fetched issues/pullRequests). issueStatus is left to its documented
* "ready" default -- see this file's header for why that's honest, not fabricated.
*
* @param {string} repoFullName
* @param {{ number: number }} issue
* @param {{ issues: Array<{ number: number }>, pullRequests: unknown[] }} context
* @param {{ listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> }} claimLedger
* @returns {import("@jsonbored/gittensory-engine").FeasibilityGateResult}
*/
export function buildCodingTaskFeasibility(repoFullName, issue, context, claimLedger) {
const found = context.issues.some((candidate) => candidate.number === issue.number);
const claimStatus = resolveClaimStatus(claimLedger, repoFullName, issue.number);
const duplicateClusterRisk = resolveDuplicateClusterRisk(repoFullName, context.issues, context.pullRequests, issue.number);
const feasibilityInput = feasibilityInputFromPreStartCheck({ found, claimStatus, duplicateClusterRisk });
return buildFeasibilityVerdict(feasibilityInput);
}

/**
* Compose the immutable AcceptanceCriteria document for one target issue + its feasibility verdict.
*
* @param {{ title: string, body?: string | null, labels?: string[] }} issue
* @param {import("@jsonbored/gittensory-engine").FeasibilityGateResult} feasibility
* @returns {import("@jsonbored/gittensory-engine").AcceptanceCriteria}
*/
export function buildCodingTaskAcceptanceCriteria(issue, feasibility) {
const promptPacket = buildPromptPacket({
taskBrief: buildTaskBrief(issue),
constraints: buildConstraints(issue),
feasibilityNotes: buildFeasibilityNotes(feasibility),
retrievalContext: "",
});
return buildAcceptanceCriteria({ promptPacket, feasibility });
}

/**
* Write the acceptance-criteria document into the prepared worktree -- only when its own verdict authorizes
* it (shouldWriteAcceptanceCriteria: verdict === "go"). A raise/avoid verdict writes nothing; the caller is
* expected to abandon the attempt rather than start it, per acceptance-criteria.ts's own documented design.
*
* @param {string} workingDirectory
* @param {import("@jsonbored/gittensory-engine").AcceptanceCriteria} acceptanceCriteria
* @returns {{ written: boolean, path: string | null }}
*/
export function writeAcceptanceCriteriaFile(workingDirectory, acceptanceCriteria) {
if (!shouldWriteAcceptanceCriteria(acceptanceCriteria.verdict)) return { written: false, path: null };
const path = join(workingDirectory, ACCEPTANCE_CRITERIA_FILENAME);
writeFileSync(path, serializeAcceptanceCriteria(acceptanceCriteria), "utf8");
return { written: true, path };
}

/**
* 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).
*/
function buildInstructions(issue, acceptanceCriteriaPath) {
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.`,
].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.
*
* @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,
* }} input
* @returns {import("./coding-task-spec.js").CodingTaskSpecResult}
*/
export function buildCodingTaskSpec(input) {
const feasibility = buildCodingTaskFeasibility(input.repoFullName, input.issue, input.context, input.claimLedger);
const acceptanceCriteria = buildCodingTaskAcceptanceCriteria(input.issue, feasibility);
const writeResult = writeAcceptanceCriteriaFile(input.workingDirectory, acceptanceCriteria);

if (!writeResult.written) {
return { ready: false, verdict: feasibility.verdict, feasibility };
}

return {
ready: true,
verdict: feasibility.verdict,
feasibility,
acceptanceCriteriaPath: writeResult.path,
instructions: buildInstructions(input.issue, writeResult.path),
title: input.issue.title,
body: input.issue.body ?? undefined,
labels: input.issue.labels,
linkedIssues: [input.issue.number],
};
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"expected-engine.version"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/attempt-cli.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.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/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-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/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 lib/attempt-cli.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.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-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/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
Loading