Skip to content

Commit 199a995

Browse files
authored
feat(miner): build the coding-task-spec (title/instructions/acceptanceCriteriaPath) (#5132) (#5239)
The second gap discovered alongside #5132's CLI wiring: IterateLoopInput's 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, but deliberately does not write it -- "that's the worktree primitive's job" per its own header. This module is that caller. buildCodingTaskFeasibility computes a real FeasibilityGateResult from signals the miner already has: whether the issue is present in the fetched context, its real claim status (the claim ledger), and its real duplicate-cluster risk (buildCollisionReport). issueStatus is left to feasibilityInputFromPreStartCheck's own documented "ready" default when quality data is unavailable -- the same honest-default precedent #5145 established for SelfReviewContext's own bounties/issueQuality gap. buildCodingTaskAcceptanceCriteria + writeAcceptanceCriteriaFile compose and write the real file -- only for a go verdict, per the module's own design (raise/avoid means the attempt should not start). Deliberately duplicate-cluster risk here does NOT apply #5145's ">= 2 pull_request items" threshold: that gate exists to stop self-review inDuplicateCluster from false-flagging one legitimately-closing PR. Feasibility asks a different question -- should this attempt even start -- where any existing PR already working the issue is a real caution signal, documented inline.
1 parent 8cb5e74 commit 199a995

4 files changed

Lines changed: 446 additions & 1 deletion

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { AcceptanceCriteria, FeasibilityGateResult, FeasibilityVerdict, IssueRecord, PullRequestRecord } from "@jsonbored/gittensory-engine";
2+
3+
export type CodingTaskIssue = { number: number; title: string; body?: string | null | undefined; labels?: string[] | undefined };
4+
5+
export type CodingTaskClaimLedger = {
6+
listClaims(filter: { repoFullName: string; status: string }): Array<{ issueNumber: number }>;
7+
};
8+
9+
export type CodingTaskContext = { issues: IssueRecord[]; pullRequests: PullRequestRecord[] };
10+
11+
export function buildCodingTaskFeasibility(
12+
repoFullName: string,
13+
issue: CodingTaskIssue,
14+
context: CodingTaskContext,
15+
claimLedger: CodingTaskClaimLedger,
16+
): FeasibilityGateResult;
17+
18+
export function buildCodingTaskAcceptanceCriteria(issue: CodingTaskIssue, feasibility: FeasibilityGateResult): AcceptanceCriteria;
19+
20+
export function writeAcceptanceCriteriaFile(workingDirectory: string, acceptanceCriteria: AcceptanceCriteria): { written: boolean; path: string | null };
21+
22+
export type CodingTaskSpecInput = {
23+
repoFullName: string;
24+
issue: CodingTaskIssue;
25+
context: CodingTaskContext;
26+
claimLedger: CodingTaskClaimLedger;
27+
workingDirectory: string;
28+
};
29+
30+
export type CodingTaskSpecResult =
31+
| { ready: false; verdict: FeasibilityVerdict; feasibility: FeasibilityGateResult }
32+
| {
33+
ready: true;
34+
verdict: FeasibilityVerdict;
35+
feasibility: FeasibilityGateResult;
36+
acceptanceCriteriaPath: string;
37+
instructions: string;
38+
title: string;
39+
body: string | undefined;
40+
labels: string[] | undefined;
41+
linkedIssues: number[];
42+
};
43+
44+
export function buildCodingTaskSpec(input: CodingTaskSpecInput): CodingTaskSpecResult;
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { writeFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import {
4+
ACCEPTANCE_CRITERIA_FILENAME,
5+
buildAcceptanceCriteria,
6+
buildCollisionReport,
7+
buildFeasibilityVerdict,
8+
buildPromptPacket,
9+
feasibilityInputFromPreStartCheck,
10+
serializeAcceptanceCriteria,
11+
shouldWriteAcceptanceCriteria,
12+
} from "@jsonbored/gittensory-engine";
13+
14+
// Coding-task-spec builder (#5132, Wave 3.5 follow-up). The second gap discovered alongside #5132's CLI
15+
// wiring: `IterateLoopInput.title`/`instructions`/`acceptanceCriteriaPath` had no builder anywhere in this
16+
// package. `packages/gittensory-engine/src/miner/acceptance-criteria.ts` already composes a PromptPacket +
17+
// FeasibilityGateResult into an immutable AcceptanceCriteria document (and deliberately does NOT write it --
18+
// "actually writing it into the attempt's worktree is the worktree primitive's job", per its own header) --
19+
// this module is that caller: derives the four inputs from a real target issue + the already-fetched
20+
// SelfReviewContext (#5145), then writes the file for real.
21+
//
22+
// issueStatus is intentionally left undefined when computing feasibility: buildIssueQualityReport (the only
23+
// thing that could supply it) lives only in root src/signals/engine.ts and has never been extracted into
24+
// @jsonbored/gittensory-engine (same gap #5145's own header documents for `issueQuality`). This is not a
25+
// fabrication -- feasibilityInputFromPreStartCheck's OWN documented default for a missing
26+
// issueQualityStatus/lifecycle is "ready", the same honest-default precedent already established.
27+
28+
function buildTaskBrief(issue) {
29+
const body = (issue.body ?? "").trim();
30+
return body ? `${issue.title}\n\n${body}` : issue.title;
31+
}
32+
33+
function buildConstraints(issue) {
34+
if (!Array.isArray(issue.labels) || issue.labels.length === 0) return "";
35+
return `Labels on this issue: ${issue.labels.join(", ")}.`;
36+
}
37+
38+
function buildFeasibilityNotes(feasibility) {
39+
return [feasibility.summary, ...feasibility.avoidReasons, ...feasibility.raiseReasons].join("\n");
40+
}
41+
42+
// Only ever resolves to "claimed"/"unclaimed": the claim ledger's own ClaimStatus vocabulary
43+
// ("active"|"released"|"expired") has no "solved" concept for FeasibilityClaimStatus's "solved" value to
44+
// map from -- that would need real evidence a PR already resolved the issue (e.g. a merged, linked PR),
45+
// which this function doesn't have access to. Not fabricated; genuinely undetectable from claim data alone.
46+
function resolveClaimStatus(claimLedger, repoFullName, issueNumber) {
47+
const claims = claimLedger.listClaims({ repoFullName, status: "active" });
48+
return claims.some((claim) => claim.issueNumber === issueNumber) ? "claimed" : "unclaimed";
49+
}
50+
51+
// The target issue's own raw cluster risk from buildCollisionReport (newly exported from
52+
// @jsonbored/gittensory-engine's public barrel) -- "none" when the issue isn't part of any cluster at all.
53+
// DELIBERATELY does NOT apply #5145's ">= 2 pull_request items" threshold: that gate exists specifically to
54+
// stop inDuplicateCluster (self-review, "does MY OWN just-created submission look redundant") from firing on
55+
// the ordinary case of one existing PR already legitimately closing the issue. Feasibility asks a different
56+
// question -- "should I even START working on this issue" -- where an issue already having ANY open PR
57+
// against it (buildCollisionReport's pairwise "shared linked issue" rule, which fires at "high" for exactly
58+
// one PR) is a meaningful, real caution signal, not a false positive to filter out.
59+
function resolveDuplicateClusterRisk(repoFullName, issues, pullRequests, issueNumber) {
60+
const report = buildCollisionReport(repoFullName, issues, pullRequests);
61+
const cluster = report.clusters.find((entry) => entry.items.some((item) => item.type === "issue" && item.number === issueNumber));
62+
return cluster ? cluster.risk : "none";
63+
}
64+
65+
/**
66+
* Compute the feasibility verdict for one target issue, from real signals: whether the issue is present in
67+
* the fetched context, its real claim status (the claim ledger), and its real duplicate-cluster risk
68+
* (buildCollisionReport over the fetched issues/pullRequests). issueStatus is left to its documented
69+
* "ready" default -- see this file's header for why that's honest, not fabricated.
70+
*
71+
* @param {string} repoFullName
72+
* @param {{ number: number }} issue
73+
* @param {{ issues: Array<{ number: number }>, pullRequests: unknown[] }} context
74+
* @param {{ listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> }} claimLedger
75+
* @returns {import("@jsonbored/gittensory-engine").FeasibilityGateResult}
76+
*/
77+
export function buildCodingTaskFeasibility(repoFullName, issue, context, claimLedger) {
78+
const found = context.issues.some((candidate) => candidate.number === issue.number);
79+
const claimStatus = resolveClaimStatus(claimLedger, repoFullName, issue.number);
80+
const duplicateClusterRisk = resolveDuplicateClusterRisk(repoFullName, context.issues, context.pullRequests, issue.number);
81+
const feasibilityInput = feasibilityInputFromPreStartCheck({ found, claimStatus, duplicateClusterRisk });
82+
return buildFeasibilityVerdict(feasibilityInput);
83+
}
84+
85+
/**
86+
* Compose the immutable AcceptanceCriteria document for one target issue + its feasibility verdict.
87+
*
88+
* @param {{ title: string, body?: string | null, labels?: string[] }} issue
89+
* @param {import("@jsonbored/gittensory-engine").FeasibilityGateResult} feasibility
90+
* @returns {import("@jsonbored/gittensory-engine").AcceptanceCriteria}
91+
*/
92+
export function buildCodingTaskAcceptanceCriteria(issue, feasibility) {
93+
const promptPacket = buildPromptPacket({
94+
taskBrief: buildTaskBrief(issue),
95+
constraints: buildConstraints(issue),
96+
feasibilityNotes: buildFeasibilityNotes(feasibility),
97+
retrievalContext: "",
98+
});
99+
return buildAcceptanceCriteria({ promptPacket, feasibility });
100+
}
101+
102+
/**
103+
* Write the acceptance-criteria document into the prepared worktree -- only when its own verdict authorizes
104+
* it (shouldWriteAcceptanceCriteria: verdict === "go"). A raise/avoid verdict writes nothing; the caller is
105+
* expected to abandon the attempt rather than start it, per acceptance-criteria.ts's own documented design.
106+
*
107+
* @param {string} workingDirectory
108+
* @param {import("@jsonbored/gittensory-engine").AcceptanceCriteria} acceptanceCriteria
109+
* @returns {{ written: boolean, path: string | null }}
110+
*/
111+
export function writeAcceptanceCriteriaFile(workingDirectory, acceptanceCriteria) {
112+
if (!shouldWriteAcceptanceCriteria(acceptanceCriteria.verdict)) return { written: false, path: null };
113+
const path = join(workingDirectory, ACCEPTANCE_CRITERIA_FILENAME);
114+
writeFileSync(path, serializeAcceptanceCriteria(acceptanceCriteria), "utf8");
115+
return { written: true, path };
116+
}
117+
118+
/**
119+
* The coding-agent driver's own prompt text (agent-sdk-driver.ts's header: "forwarded verbatim as the
120+
* prompt -- the acceptance-criteria document already lives inside the worktree", so this points to it
121+
* rather than repeating its content).
122+
*/
123+
function buildInstructions(issue, acceptanceCriteriaPath) {
124+
return [
125+
`Resolve the following GitHub issue in this repository: #${issue.number} -- ${issue.title}`,
126+
"",
127+
(issue.body ?? "").trim(),
128+
"",
129+
`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.`,
130+
].join("\n");
131+
}
132+
133+
/**
134+
* Full composition: feasibility -> acceptance criteria -> (if authorized) write the file -> instructions.
135+
* Returns `ready: false` (with the computed feasibility verdict, for the caller to report) when the
136+
* verdict is `raise`/`avoid` -- the caller should abandon the attempt rather than proceed with no real
137+
* acceptance-criteria file on disk.
138+
*
139+
* @param {{
140+
* repoFullName: string, issue: { number: number, title: string, body?: string | null, labels?: string[] },
141+
* context: { issues: Array<{ number: number }>, pullRequests: unknown[] },
142+
* claimLedger: { listClaims: (filter: { repoFullName: string, status: string }) => Array<{ issueNumber: number }> },
143+
* workingDirectory: string,
144+
* }} input
145+
* @returns {import("./coding-task-spec.js").CodingTaskSpecResult}
146+
*/
147+
export function buildCodingTaskSpec(input) {
148+
const feasibility = buildCodingTaskFeasibility(input.repoFullName, input.issue, input.context, input.claimLedger);
149+
const acceptanceCriteria = buildCodingTaskAcceptanceCriteria(input.issue, feasibility);
150+
const writeResult = writeAcceptanceCriteriaFile(input.workingDirectory, acceptanceCriteria);
151+
152+
if (!writeResult.written) {
153+
return { ready: false, verdict: feasibility.verdict, feasibility };
154+
}
155+
156+
return {
157+
ready: true,
158+
verdict: feasibility.verdict,
159+
feasibility,
160+
acceptanceCriteriaPath: writeResult.path,
161+
instructions: buildInstructions(input.issue, writeResult.path),
162+
title: input.issue.title,
163+
body: input.issue.body ?? undefined,
164+
labels: input.issue.labels,
165+
linkedIssues: [input.issue.number],
166+
};
167+
}

packages/gittensory-miner/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"expected-engine.version"
3333
],
3434
"scripts": {
35-
"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"
35+
"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"
3636
},
3737
"dependencies": {
3838
"@jsonbored/gittensory-engine": "*"

0 commit comments

Comments
 (0)