Skip to content

Commit 3b6036c

Browse files
authored
fix(triage): accept note-only evidence; raise research max-turns to 200 (#97)
1 parent 71f83a6 commit 3b6036c

4 files changed

Lines changed: 74 additions & 19 deletions

File tree

.github/workflows/research.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ on:
1010
workflow_dispatch:
1111
inputs:
1212
focus_area:
13-
description: "Optional focus area for this manual run. Leave blank for a randomly chosen area. Constrained free-text: lowercase letters, digits, and hyphens only; must start with a letter; 1–32 characters. See the predefined list in research.md §12 for the conventional values."
13+
description: "Optional focus area for this manual run. Leave blank for a
14+
randomly chosen area. Constrained free-text: lowercase letters,
15+
digits, and hyphens only; must start with a letter; 1–32 characters.
16+
See the predefined list in research.md §12 for the conventional
17+
values."
1418
required: false
1519
type: string
1620
default: ""
@@ -112,7 +116,7 @@ jobs:
112116
allowed_bots: "*"
113117
claude_args: |
114118
--model opus
115-
--max-turns 80
119+
--max-turns 200
116120
--allowedTools "WebSearch,WebFetch,Read,Glob,Grep,Bash(gh issue create:*),Bash(gh issue list:*),Bash(gh label create:*),Bash(git log:*),Bash(cat:*),Bash(date:*),Bash(mmdc:*),Bash(awk:*),Bash(grep:*)"
117121
--disallowedTools ""
118122
prompt: |
@@ -157,7 +161,7 @@ jobs:
157161
158162
## Strict Workflow — Follow In Order
159163
160-
You have a LIMITED turn budget (~80 turns). Do NOT read every file.
164+
You have a LIMITED turn budget (~200 turns). Do NOT read every file.
161165
162166
### Step 1 — Architecture overview (2-3 turns max)
163167

docs/use/workflows/triage.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ The agent classifies the issue (bug, feature, refactor, docs, unclear). For bugs
2929

3030
## Outputs
3131

32-
| Field | Type | Notes |
33-
| ----------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
34-
| `state.valid` | boolean | Verdict. |
35-
| `state.confidence` | float `[0, 1]` | Agent's self-assessment. |
36-
| `state.summary` | string | Verdict rationale; uncapped. Embedded into the failed-cascade reason when `valid = false`. |
37-
| `state.recommendedNext` | `'plan'` \| `'stop'` ||
38-
| `state.evidence` | `Array<{file, line?, note?}>` | |
39-
| `state.reproduction` | `{attempted, reproduced, details}` | `attempted=false` for non-bug class. `reproduced=null` is allowed only after the harness ladder is walked AND an invariant test is ruled out. |
40-
| `state.report` | markdown | The full `TRIAGE.md`. Embedded verbatim in the tracking comment. |
32+
| Field | Type | Notes |
33+
| ----------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34+
| `state.valid` | boolean | Verdict. |
35+
| `state.confidence` | float `[0, 1]` | Agent's self-assessment. |
36+
| `state.summary` | string | Verdict rationale; uncapped. Embedded into the failed-cascade reason when `valid = false`. |
37+
| `state.recommendedNext` | `'plan'` \| `'stop'` | |
38+
| `state.evidence` | `Array<{file?, line?, note?}>` | Each entry must have at least one of `file` or `note`. Prefer `file`+`line` citations; `note`-only is for cross-cutting evidence (e.g. a negative grep result that has no single-file pointer). |
39+
| `state.reproduction` | `{attempted, reproduced, details}` | `attempted=false` for non-bug class. `reproduced=null` is allowed only after the harness ladder is walked AND an invariant test is ruled out. |
40+
| `state.report` | markdown | The full `TRIAGE.md`. Embedded verbatim in the tracking comment. |
4141

4242
## Stop conditions
4343

src/workflows/handlers/triage.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,15 @@ const verdictSchema = z
7575
recommendedNext: z.enum(["plan", "stop"]),
7676
evidence: z
7777
.array(
78-
z.object({
79-
file: z.string().min(1),
80-
line: z.number().int().nonnegative().optional(),
81-
note: z.string().min(1).optional(),
82-
}),
78+
z
79+
.object({
80+
file: z.string().min(1).optional(),
81+
line: z.number().int().nonnegative().optional(),
82+
note: z.string().min(1).optional(),
83+
})
84+
.refine((e) => e.file !== undefined || e.note !== undefined, {
85+
message: "evidence entry must have at least one of `file` or `note`",
86+
}),
8387
)
8488
.default([]),
8589
reproduction: reproductionSchema,
@@ -337,9 +341,10 @@ function buildTriagePrompt(input: {
337341
` "summary": "<as long as needed to faithfully convey the verdict>",`,
338342
` "recommendedNext": "plan" | "stop",`,
339343
` "evidence": [`,
340-
` { "file": "<path>", "line": <int|omit>, "note": "<short>" },`,
344+
` { "file": "<path|omit>", "line": <int|omit>, "note": "<short|omit>" },`,
341345
` ...`,
342346
` ],`,
347+
` (every evidence entry MUST have at least one of \`file\` or \`note\`. Prefer \`file\`+\`line\` citations; use \`note\`-only for cross-cutting observations like negative grep results.)`,
343348
` "reproduction": {`,
344349
` "attempted": true | false,`,
345350
` "reproduced": true | false | null,`,
@@ -348,7 +353,7 @@ function buildTriagePrompt(input: {
348353
` }`,
349354
``,
350355
`Rules:`,
351-
`- Be ruthless about evidence. A claim without a file:line citation is a guess.`,
356+
`- Be ruthless about evidence. Prefer \`file:line\` citations; a claim with neither a citation nor a concrete cross-cutting observation (e.g. a negative grep result) is a guess.`,
352357
`- For bug issues, a verdict without an honest reproduction attempt is a failure of your job.`,
353358
`- It is OK to report \`reproduced=null\` if the bug genuinely can't be reproduced in this environment — but you MUST explain WHY honestly. Never lie about reproduction status.`,
354359
`- Do NOT modify any source files in the repo (writing temporary scripts under /tmp is fine; running tests is fine; do not stage or commit anything).`,

test/workflows/handlers/triage.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,52 @@ describe("triage handler (SDK-driven)", () => {
254254
}
255255
});
256256

257+
it("accepts evidence entries with note but no file (cross-cutting observations)", async () => {
258+
triageVerdict = JSON.stringify({
259+
valid: true,
260+
confidence: 0.9,
261+
summary: "Reproduced — non-constant-time bearer comparison.",
262+
recommendedNext: "plan",
263+
evidence: [
264+
{ file: "src/orchestrator/ws-server.ts", line: 52, note: "plain !== check" },
265+
{ note: "grep timingSafeEqual src/ returns 0 hits" },
266+
],
267+
reproduction: {
268+
attempted: true,
269+
reproduced: true,
270+
details: "Code inspection confirms structural defect at cited line.",
271+
},
272+
});
273+
const ctx = buildCtx();
274+
275+
const result = await triageHandler(ctx);
276+
277+
expect(result.status).toBe("succeeded");
278+
if (result.status === "succeeded") {
279+
const state = result.state as { evidence: unknown[] };
280+
expect(state.evidence).toHaveLength(2);
281+
}
282+
});
283+
284+
it("rejects evidence entries that have neither file nor note", async () => {
285+
triageVerdict = JSON.stringify({
286+
valid: true,
287+
confidence: 0.9,
288+
summary: "x",
289+
recommendedNext: "plan",
290+
evidence: [{ line: 10 }],
291+
reproduction: { attempted: false, reproduced: null, details: "skipped" },
292+
});
293+
const ctx = buildCtx();
294+
295+
const result = await triageHandler(ctx);
296+
297+
expect(result.status).toBe("failed");
298+
if (result.status === "failed") {
299+
expect(result.reason).toContain("TRIAGE_VERDICT.json failed validation");
300+
}
301+
});
302+
257303
it("fails when target is a PR rather than an issue", async () => {
258304
const ctx = buildCtx();
259305
const prCtx: WorkflowRunContext = { ...ctx, target: { ...ctx.target, type: "pr" } };

0 commit comments

Comments
 (0)