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
115 changes: 66 additions & 49 deletions ops/NEXT.md
Original file line number Diff line number Diff line change
@@ -1,73 +1,90 @@
# NEXT — Gate 3: Sharpen backlog-picker actionability
# NEXT — Gate 3: Refuse backlog entries with unterminated backticks

Selected from ops/TARGET.md for this run (gate 3, CODE task, SDK-side).

## Target scope (quoted from ops/TARGET.md)

> Scope and definition-of-done are both derived from backticked spans. An entry
> with an ODD number of backticks makes those spans wrong: the parser pairs the
> opening backtick with whatever backtick appears next, so text that was never
> meant to be code becomes scope, and real content is swallowed.
>
> Since #42 widened what counts as scope — symbols and commands, not only paths —
> a mispaired span is now MORE likely to produce a plausible-looking but wrong
> `files_in_scope`, which is worse than an obviously empty one.
>
> Add a typed refusal for it. Salvaged from closed PR #32, which proposed the
> check but wired it to nothing; two of its three proposed reasons were rejected
> on assessment (`nested_bullet` would have been a regression — the selection
> regex already skips indented bullets; `missing_body` is covered by the existing
> reasons). Only the unterminated-backtick case is real.

**Scope:** Gate 3 — Improve how the Garden decides what is WORTH working on. CODE task, SDK-side.

On main now, all merged and tested:
- `sdk/src/backlog-picker.ts` — proposes a work package from ops/BACKLOG.md; exports selectBacklogEntry / packageFromEntry / validateWorkPackage
- `sdk/src/work-package-consumer.ts` — judges one, refusing with a typed reason (missing_title / missing_scope / missing_definition_of_done / nonexistent_files)
- `testdata/backlog-picker.flow.yaml` — the flow. Its `select-entry` step now scans for the first ACTIONABLE entry, validating candidates and skipping the ones that fail, and exits nonzero with NO_ACTIONABLE_BACKLOG_ENTRY when nothing qualifies.

Do NOT re-do any of the above. Malformed-backlog handling (PR #30) and the nonexistent-files check (PR #28) are DONE and merged.

## The actual defect

Run `select-entry` against the real ops/BACKLOG.md. It prints:

SKIPPED_UNACTIONABLE=10 ...
## Objective

and then selects a dated notes blob ("Upstream issues (2026-08-27):") as the work package. Ten genuine engineering tasks were skipped in favour of a list of links.
Add a typed refusal reason for backlog entries with an odd number of backticks, preventing mispaired code spans from producing plausible-looking but wrong scope extraction.

The cause: `validateWorkPackage` decides "actionable" using only two shallow signals — does the text contain a backticked path, and does it contain a multi-word backticked phrase. A notes blob full of backticked identifiers passes both. A real task written in prose ("Refuse a path-like deterministic command word when that path does not exist") fails both.
## Files in scope

The guard is correct. The SELECTION is poor. That is what to fix.
- `sdk/src/backlog-picker.ts` — add the refusal reason type, implement the check, wire it into `validateWorkPackage` or `packageFromEntry`
- `sdk/src/index.ts` — export the new refusal reason type if it's added to the union
- `sdk/tests/backlog-picker.test.ts` — tests for the new refusal behavior (or create this file if it doesn't exist)
- `testdata/backlog-picker.flow.yaml` — if touched, MUST regenerate the canonical spec
- `testdata/backlog-picker.spec.canonical.json` — kernel consumes this; two drift tests fail if it's stale

## Objective
## Definition of done

Implement a sharper notion of actionability in `sdk/src/backlog-picker.ts` so that the backlog picker selects real engineering tasks and does NOT select notes entries.
All of the following must be verified and the literal command output quoted:

## Files in scope
1. **A typed refusal reason exists** for entries with an odd backtick count (e.g., `'unterminated_backticks'`)

- `sdk/src/backlog-picker.ts` — improve actionability detection
- `sdk/src/index.ts` — wire in new export if it is needed
- Tests for the new behavior
- `testdata/backlog-picker.flow.yaml` — ONLY if changes needed
- `testdata/backlog-picker.spec.canonical.json` — regenerate ONLY if yaml changes
2. **The refusal is WIRED IN**, not merely exported. PR #32 was closed largely for exporting a checker nothing called. Show the flow refusing such an entry.

## Definition of done
3. **ACTIONABLE count maintained at ~22 of 32:**

All of the following must hold:
Baseline verification BEFORE changes (currently TOTAL=32 ACTIONABLE=21):
```
node -e 'const fs=require("node:fs");
const sdk=require("./sdk/dist/backlog-picker.js");
const t=fs.readFileSync("ops/BACKLOG.md","utf8");
const e=[...t.matchAll(/^- \*\*(.+?)\*\*\s*(.*(?:\n .*)*)/gm)]
.map(m=>({title:m[1],body:m[2].replace(/\s+/g," ").trim()}));
let ok=0; for(const x of e)
if(sdk.validateWorkPackage(sdk.packageFromEntry(x)).accepted) ok++;
console.log("TOTAL="+e.length+" ACTIONABLE="+ok)'
```

1. **Improved actionability logic** in `sdk/src/backlog-picker.ts` that distinguishes real engineering tasks from notes blobs
Run AFTER changes to prove the count is still ~22 (21-22 is acceptable). A refusal that also rejects well-formed entries is a regression.

2. **Literal before/after evidence:**
- Quote the literal `select-entry` output BEFORE the change showing it selected "Upstream issues"
- Quote the literal `select-entry` output AFTER the change showing it selected a real engineering task
4. **Tests covering the new behaviour AND every existing test still passing:**
- `cd sdk && npm test` — must be green (exit 0)
- Tests must include at least one case showing that an entry with an odd backtick count is refused with the new reason

3. **Test coverage:**
- Tests covering the new behavior
- EVERY new test confirmed to FAIL against current code (quote the literal failing output)
- All existing tests still passing
5. **EVERY new test confirmed to FAIL against current code:**
Before implementing the fix, write the test and run it against the current code. Paste the literal failing output proving the test fails without the implementation.

4. **Green test suites:**
6. **Kernel tests still green:**
```
cd sdk && npm test
cd kernel && sh ../ops/cargo.sh test
```
Both must pass with output quoted.

5. **If testdata/backlog-picker.flow.yaml is modified:**
- Regenerate `testdata/backlog-picker.spec.canonical.json`
Must exit 0 with all tests passing.

6. **Final verification** — as the LAST action, run:
7. **Final git status** — as the LAST action:
```
git status --porcelain
```
And paste the output
Paste the output.

## Out of scope

- **DO NOT re-implement malformed-backlog handling** (PR #30, merged)
- **DO NOT re-implement nonexistent-files check** (PR #28, merged)
- Any work on other gates (1, 2, 4, 5, 6, 7, 8, 9)
- Any changes to the consumer logic beyond what's needed for this specific defect
- Performance optimizations unrelated to the selection problem
- Do NOT change the accept/reject thresholds in `validateWorkPackage`
- Do NOT re-tune scope extraction to raise the ACTIONABLE count — ops/TARGET.md explicitly says "Do not re-do this": the picker's actionability problem is SOLVED and merged (PR #42), ACTIONABLE is 22 of 32 (we see 21, close enough), above the target of 20
- Do NOT touch the two other proposed reasons from closed PR #32 (`nested_bullet`, `missing_body`) — those were rejected
- Any changes to RFC-0001 or charter files
- Any work on gates other than gate 3

## Notes

- ops/STATE.md reports that only PR #19 is currently open (gate-2 HN demo, green and mergeable, waiting on Khaliq). No unfinished work blocks this task.
- ops/DIRECTIVES.md is empty — no standing directives conflict with this work.
- This is a CODE task, not an assessment or research task.
- The run is pinned to gate 3 by ops/TARGET.md. Several runs execute in parallel, each pinned to a different gate. Work outside this target collides with a sibling run.
11 changes: 11 additions & 0 deletions sdk/src/backlog-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ValidatedWorkPackage {
}

export type WorkPackageValidationReason =
| 'unterminated_backticks'
| 'missing_title'
| 'missing_scope'
| 'missing_definition_of_done';
Expand Down Expand Up @@ -78,6 +79,9 @@ export function validateWorkPackage(input: unknown): WorkPackageValidation {
if (!isRecord(input) || !isNonEmptyString(input['title'])) {
return { accepted: false, reason: 'missing_title' };
}
if (hasUnterminatedBackticks(input)) {
return { accepted: false, reason: 'unterminated_backticks' };
}
if (!isNonEmptyStringArray(input['files_in_scope'])) {
return { accepted: false, reason: 'missing_scope' };
}
Expand All @@ -87,6 +91,13 @@ export function validateWorkPackage(input: unknown): WorkPackageValidation {
return { accepted: true, work: input as unknown as ValidatedWorkPackage };
}

function hasUnterminatedBackticks(input: Record<string, unknown>): boolean {
const source = [input['title'], input['description']]
.filter((value): value is string => typeof value === 'string')
.join(' ');
return (source.match(/`/g)?.length ?? 0) % 2 === 1;
Comment on lines +95 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check backtick balance in each field independently

When both title and description contain an odd number of backticks, joining them makes the combined count even, so this check accepts the package if its extracted scope and definition of done are otherwise valid. For example, an unterminated code span in the title plus a description containing a valid path span and another unmatched backtick bypasses the new refusal, preserving the malformed-input scenario this change is intended to close. Validate the fields separately rather than allowing delimiters from one field to balance delimiters in another.

Useful? React with 👍 / 👎.

}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
Expand Down
12 changes: 12 additions & 0 deletions sdk/tests/backlog-picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ describe('backlog picker', () => {
});

describe('work package validation', () => {
it('refuses an entry with unterminated backticks using a typed reason', async () => {
const work = packageFromEntry({
title: 'Fix malformed scope extraction',
body: 'Update `sdk/src/backlog-picker.ts so scope cannot be mispaired.',
});

expect(await validate(work)).toEqual({
accepted: false,
reason: 'unterminated_backticks',
});
});

it('uses a referenced code symbol as evidence of repository scope', async () => {
const work = packageFromEntry({
title: 'Refuse an entry with unterminated backticks',
Expand Down