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
81 changes: 51 additions & 30 deletions ops/NEXT.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,70 @@
# NEXT — Gate 3: Build the work package consumer
# NEXT — Gate 3: Work package validator

**Scope:** Gate 3 — close the Garden's loop. CODE task, SDK-side. The picker EMITS a work package (sdk/src/backlog-picker.ts + testdata/backlog-picker.flow.yaml, merged, four tested properties) and NOTHING consumes it — that is the missing half. Build the consumer: an SDK entrypoint taking an emitted package and turning it into something runnable, validating it has a title, a non-empty scope and a definition of done, and REFUSING with a typed reason when it does not, because a package that cannot be verified must not become work. NOTE: two previous attempts (ee5c9b3e, 06c0d6ab) did this correctly and their files were LOST before delivery by a platform fault — the build sandbox's .git points at a directory that does not exist, so writes cannot be captured. You are not duplicating live work.
**Pinned to gate 3** (ops/TARGET.md)

## Scope

> Close the gap between the Garden's two halves. CODE task, SDK-side.
>
> On main: `sdk/src/backlog-picker.ts` proposes a work package from
> ops/BACKLOG.md (four tested properties).
>
> NOT on main: the consumer that judges a package. It exists in open PR #23
> (`sdk/src/work-package-consumer.ts`, seven tests) and has not been merged. An
> earlier version of this brief claimed it was on main; a run took that at face
> value, found only one half of the pair, and correctly escalated rather than
> inventing the other. It was right and the brief was wrong.
>
> Build the consumer's counterpart that CAN be built against main today: a
> validator for what the picker emits, living beside the picker, that returns
> either an accepted package or a typed refusal naming what is missing (no title,
> empty scope, no definition of done). Do not import from PR #23 — it is not
> merged, and a run must build against main, not against an open branch.
>
> That is the Garden's smallest complete loop — propose, judge, accept-or-refuse
> — and nothing exercises it end to end today.

## Objective

Build the SDK entrypoint that takes an emitted work package and turns it into something runnable. The consumer must validate that the package has:
- A title (non-empty string)
- A non-empty scope
- A definition of done
Build a validator function for work packages that lives in `sdk/src/backlog-picker.ts`
(beside the picker, not importing from the unmerged PR #23). The validator must:

When any of these is missing or invalid, the consumer REFUSES with a typed reason. A package that cannot be verified must not become work.
1. Accept a work package with title, scope, and definition of done
2. Refuse with typed reasons when:
- No title (or empty title)
- No scope (or empty scope)
- No definition of done (or empty definition of done)

## Files in scope

- `sdk/src/` (new consumer code)
- `sdk/tests/` (new consumer tests)
- NO changes to `kernel/` (PR #19 is open)
- NO changes to `sdk/src/demo-hn-monitor.ts` (PR #19 is open)
- `sdk/src/backlog-picker.ts` — add validator function
- `sdk/src/index.ts` — export the validator
- `sdk/tests/backlog-picker.test.ts` or new test file — tests for validation

## Definition of done

1. **SDK code exists** that consumes an emitted work package
2. **Validation tests exist** for:
- Missing title → typed refusal
- Empty title → typed refusal
- Missing scope → typed refusal
- Empty scope → typed refusal
- Missing definition of done → typed refusal
- Empty definition of done → typed refusal
- Valid package → accepted
3. **Every new test is confirmed to FAIL against current code** with literal output pasted
4. **SDK test suite passes:**
**All of these commands must pass and be quoted with literal output:**

1. A new exported validator function exists in sdk/src/backlog-picker.ts
2. The function is exported from sdk/src/index.ts
3. Tests exist covering:
- A backlog yielding an acceptable package
- A backlog yielding a package the validator refuses
- An empty backlog
4. **Every new test CONFIRMED TO FAIL without the fix**, with failing output quoted
5. Command that must pass:
```
cd sdk && npm test
cd /project/workflows/runs/06163551-8818-4d54-939f-5363183ca903/sdk && npm test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the required test command workspace-relative

When this work package is executed or replayed in any fresh sandbox, the hard-coded directory from run 06163551-... will not exist, so the command that the definition of done says must pass fails at cd without running the SDK tests. Use a repository-relative command such as cd sdk && npm test so verification works in the actual checkout regardless of the run-specific mount path.

Useful? React with 👍 / 👎.

```
Paste the literal output showing all tests pass, 0 failed
5. **Final verification** — as the LAST action, run:
Quote the literal output showing test counts.
6. **Final action** — run and quote:
```
git status --porcelain
```
And paste the output to make lost writes visible immediately

## Out of scope

- Kernel changes (different PR)
- Integration with hn-monitor (different PR)
- Any changes to the backlog picker itself (already merged in PRs #20, #21, #22)
- Changes to flow execution or scheduling
- Do NOT touch kernel/
- Do NOT touch sdk/src/demo-hn-monitor.ts
- Do NOT touch anything under ops/
- Do NOT import from sdk/src/work-package-consumer.ts (PR #23, not merged)
43 changes: 43 additions & 0 deletions sdk/src/backlog-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ export interface BacklogEntry {
body: string;
}

export interface ValidatedWorkPackage {
title: string;
files_in_scope: string[];
definition_of_done: string[];
description?: string;
gate?: number | null;
}

export type WorkPackageValidationReason =
| 'missing_title'
| 'missing_scope'
| 'missing_definition_of_done';

export type WorkPackageValidation =
| { accepted: true; work: ValidatedWorkPackage }
| { accepted: false; reason: WorkPackageValidationReason };

/**
* Returns the selected entry, or null when the backlog holds no actionable
* one. Null is a real answer — "nothing to do" — not a failure.
Expand Down Expand Up @@ -50,3 +67,29 @@ export function renderWorkPackage(entry: BacklogEntry): string {
'',
].join('\n');
}

/** Accept a complete emitted package, or name the first missing requirement. */
export function validateWorkPackage(input: unknown): WorkPackageValidation {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reuse the existing work-package consumer

In this commit's parent, sdk/src/work-package-consumer.ts already defines and exports consumeWorkPackage with the same input checks, result union, refusal reasons, and accepted package shape. Adding validateWorkPackage creates a second public implementation of the same boundary, so callers can choose different APIs and future validation changes can silently diverge; remove this duplicate and use or re-export the existing consumer instead.

AGENTS.md reference: AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

if (!isRecord(input) || !isNonEmptyString(input['title'])) {
return { accepted: false, reason: 'missing_title' };
}
if (!isNonEmptyStringArray(input['files_in_scope'])) {
return { accepted: false, reason: 'missing_scope' };
}
if (!isNonEmptyStringArray(input['definition_of_done'])) {
return { accepted: false, reason: 'missing_definition_of_done' };
}
return { accepted: true, work: input as unknown as ValidatedWorkPackage };
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}

function isNonEmptyStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString);
}
7 changes: 7 additions & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ export { JOURNAL_WRITE_FAILED, PROTOCOL_VERSION } from './protocol.js';

export { JournalClient, type JournalClientOptions } from './journal-client.js';

export {
validateWorkPackage,
type ValidatedWorkPackage,
type WorkPackageValidation,
type WorkPackageValidationReason,
} from './backlog-picker.js';

export {
consumeWorkPackage,
type EmittedWorkPackage,
Expand Down
65 changes: 65 additions & 0 deletions sdk/tests/backlog-picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Some preamble that is not an entry.
- **Second entry** should not be chosen
`;

async function validate(input: unknown) {
const picker = (await import('../src/backlog-picker.js')) as Record<string, unknown>;
return (picker['validateWorkPackage'] as (value: unknown) => unknown)(input);
}

describe('backlog picker', () => {
it('selects the first bold top-level bullet', () => {
const entry = selectBacklogEntry(BACKLOG);
Expand Down Expand Up @@ -77,3 +82,63 @@ describe('backlog picker', () => {
}
});
});

describe('work package validation', () => {
it('accepts a package yielded by an actionable backlog', async () => {
const entry = selectBacklogEntry(
'# Backlog\n\n- **Validate packages** edit `sdk/src/backlog-picker.ts`; run `npm test`\n',
);

expect(
await validate({
title: entry?.title,
files_in_scope: ['sdk/src/backlog-picker.ts'],
definition_of_done: ['npm test'],
}),
).toEqual({
accepted: true,
work: {
title: 'Validate packages',
files_in_scope: ['sdk/src/backlog-picker.ts'],
definition_of_done: ['npm test'],
},
});
});

it('refuses a package yielded by an unverifiable backlog', async () => {
const entry = selectBacklogEntry('# Backlog\n\n- **Vague package** improve the SDK\n');

expect(
await validate({ files_in_scope: ['sdk/src/'], definition_of_done: ['npm test'] }),
).toEqual({ accepted: false, reason: 'missing_title' });
expect(
await validate({
title: ' ',
files_in_scope: ['sdk/src/'],
definition_of_done: ['npm test'],
}),
).toEqual({ accepted: false, reason: 'missing_title' });
expect(
await validate({ title: entry?.title, definition_of_done: ['npm test'] }),
).toEqual({ accepted: false, reason: 'missing_scope' });
expect(
await validate({ title: entry?.title, files_in_scope: [], definition_of_done: [] }),
).toEqual({ accepted: false, reason: 'missing_scope' });
expect(
await validate({ title: entry?.title, files_in_scope: ['sdk/src/'] }),
).toEqual({ accepted: false, reason: 'missing_definition_of_done' });
expect(
await validate({
title: entry?.title,
files_in_scope: ['sdk/src/'],
definition_of_done: [],
}),
).toEqual({ accepted: false, reason: 'missing_definition_of_done' });
});

it('refuses an empty backlog with a typed reason', async () => {
const entry = selectBacklogEntry('# Backlog\n');

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