-
Notifications
You must be signed in to change notification settings - Fork 0
drive: cloud run fb9528bb #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,71 +1,98 @@ | ||
| # NEXT — WP-GATE2-POLLER: Implement HN poller to make hn-monitor actually monitor | ||
| # NEXT — WP-GATE3-BACKLOG-PICKER: First honest step toward Software Garden | ||
|
|
||
| **Target gate:** Gate 2 (per ops/TARGET.md — this run is pinned to gate 2 only) | ||
| **Target gate:** Gate 3 (per ops/TARGET.md — this run is pinned to gate 3 only) | ||
|
|
||
| **Work package:** WP-GATE2-POLLER — Implement deterministic HN poller | ||
| **Work package:** WP-GATE3-BACKLOG-PICKER — Build a flow that reads ops/BACKLOG.md and emits a structured work package | ||
|
|
||
| ## Objective | ||
|
|
||
| Implement a deterministic poller that fetches `https://hacker-news.firebaseio.com/v0/topstories.json`, takes the first few story IDs, and submits each through `Engine::submit_event` so hn-monitor wakes on real HN data with dedupe. | ||
| Gate 3 per RFC-0001 §3 is "a relayflow can power a factory → Software Garden." The done-when is a labeled issue flowing to a reviewed PR end-to-end. ops/TARGET.md directs: "Do the SMALLEST honest first step, not the whole thing. Good candidate: a flow that reads ops/BACKLOG.md, picks one entry by a deterministic rule, and emits a structured work package." | ||
|
|
||
| **Context from ops/TARGET.md:** PR #15 landed `testdata/hn-monitor.flow.yaml` and `kernel/relayflowd/tests/hn_monitor_integration.rs`, but the triggering event comes from test code, not Hacker News. Gate 2 is AMBER. **The previous run wrote a work package and no code — do not repeat that. This is a CODE task: write the missing poller.** | ||
| **This is that smallest step**: a flow file that demonstrates gate-3 machinery (flows that build and improve other flows) without attempting the full discover→implement→review→merge DAG. | ||
|
|
||
| **Current state:** The `Engine::submit_event` path exists and works (kernel/relayflowd/src/engine/wake.rs:19). The hn-monitor flow exists and the integration test proves event → wake → park works. **The only missing piece is the poller that fetches real HN data and calls submit_event.** | ||
| ## Current state | ||
|
|
||
| - Gates 1 (GREEN) and 2 (AMBER, in progress) have working primitives on main | ||
| - Gate 3 is RED (not started) per ops/SCOREBOARD.md | ||
| - ops/BACKLOG.md exists with structured entries | ||
| - No gate-3 flows exist yet | ||
|
|
||
| ## Files in scope | ||
|
|
||
| **New file to create:** | ||
| - `kernel/relayflowd/src/engine/hn_poller.rs` — poller implementation | ||
| **New files to create:** | ||
| - `testdata/backlog-picker.flow.yaml` — the flow spec that reads ops/BACKLOG.md and selects one entry | ||
| - `testdata/backlog-picker.spec.canonical.json` — canonical compiled spec (via `flows check`) | ||
| - `sdk/tests/backlog-picker.test.ts` — test proving deterministic selection given the same input | ||
|
|
||
| **Files to modify:** | ||
| - `kernel/relayflowd/src/engine.rs` — add `mod hn_poller;` and `pub use hn_poller::HnPoller;` | ||
| - `kernel/relayflowd/src/lib.rs` — re-export HnPoller if needed | ||
| - New test file or extend existing test to prove offline operation with recorded payload | ||
| - None required for the minimal step | ||
|
|
||
| ## Definition of done (all three required per ops/TARGET.md) | ||
|
|
||
| 1. **A new committed source file** implementing the poller exists at `kernel/relayflowd/src/engine/hn_poller.rs` with: | ||
| - Fetch `https://hacker-news.firebaseio.com/v0/topstories.json` (returns `Vec<u64>` story IDs) | ||
| - Take first N IDs (configurable, default 5) | ||
| - For each ID, construct an `Event` matching hn-monitor's trigger pattern: | ||
| - `event_type: "hn.story_posted"` | ||
| - `payload` with at least `{"id": <story_id>, "type": "story"}` (matches pattern in hn-monitor.flow.yaml:9) | ||
| - Submit via `Engine::submit_event` with the hn-monitor spec | ||
| - Dedupe works: same story submitted twice yields `matched: true, deduped: true` on second call | ||
| 1. **`flows check` resolves the flow** — `testdata/backlog-picker.flow.yaml` passes preflight validation with exit 0 | ||
|
|
||
| 2. **`cd kernel && sh ../ops/cargo.sh test` passes** including a test that exercises the poller offline from a recorded payload (no live network call in test). Test should verify: | ||
| - Parsing topstories JSON (`[41380628, 41378954, ...]`) | ||
| - Constructing events with correct structure | ||
| - Submitting through submit_event | ||
| - Dedupe behavior (second submit is deduped) | ||
| 2. **A test proves selection is deterministic** — `sdk/tests/backlog-picker.test.ts` demonstrates that: | ||
| - Given the same ops/BACKLOG.md content, the flow always selects the same entry | ||
| - The selection rule is deterministic and documented (e.g., "first non-done entry", "alphabetically first", or similar) | ||
| - The test verifies structured output (the work package emitted has required fields) | ||
|
|
||
| 3. **The run's diff contains real code outside ops/**: The poller must be substantive Rust code, not just documentation. | ||
| 3. **`cd sdk && npm test` green** — all SDK tests pass including the new backlog-picker test | ||
|
|
||
| ## Implementation approach | ||
|
|
||
| - **Deterministic and testable**: Use a trait or function parameter to inject the JSON source, allowing tests to supply a recorded payload instead of hitting the network | ||
| - **Minimal scope**: ONE cycle (~10 minutes). Small working poller beats large plan. | ||
| - No daemon/background loop (just a sync function that polls once) | ||
| - No full story metadata fetching (topstories only gives IDs; construct minimal events) | ||
| - No retry/backoff logic (fail fast is fine for this proof) | ||
| - **Error handling**: If fetch fails, return an error | ||
| - **Payload structure**: Match what hn_monitor_integration.rs expects (see testdata/hn-monitor.flow.yaml pattern) | ||
| The SMALLEST working implementation: | ||
|
|
||
| - **Flow structure:** | ||
| ```yaml | ||
| spec_version: "1.0" | ||
| name: backlog-picker | ||
| steps: | ||
| - name: read-backlog | ||
| type: deterministic | ||
| command: "cat ops/BACKLOG.md" | ||
| - name: select-entry | ||
| type: deterministic | ||
| command: # deterministic selection logic (e.g., first non-done entry) | ||
| - name: emit-package | ||
| type: deterministic | ||
| command: # output structured work package JSON | ||
| ``` | ||
|
|
||
| - **Deterministic rule examples:** | ||
| - First entry in the file | ||
| - First entry matching a pattern | ||
| - Alphabetically sorted first | ||
| - Line-number based | ||
|
|
||
| Choose the simplest that is defensible as "deterministic" | ||
|
|
||
| - **Output format:** Structured JSON work package with fields like: | ||
| - `title`: work package name | ||
| - `description`: what needs to be done | ||
| - `files_in_scope`: estimated file paths | ||
| - `gate`: which gate this serves | ||
|
|
||
| ## Explicitly OUT of scope | ||
|
|
||
| - Daemon/background polling infrastructure | ||
| - Full HN story metadata (individual `/v0/item/{id}.json` fetches) | ||
| - Retry/backoff for network failures | ||
| - CLI commands to invoke the poller | ||
| - Configuration files | ||
| - Changes to hn-monitor flow spec or existing tests (beyond adding the poller test) | ||
| - Any RFC or charter edits | ||
| - Gates 1, 3-9 | ||
| - LLM or agent steps (gate 1 is deterministic-only for now) | ||
| - GitHub integration (creating actual issues or PRs) | ||
| - The full discover→implement→review→merge DAG | ||
| - Issue labeling, assignment, or tracking | ||
| - Integration with the existing drive.yaml workflow | ||
| - Any changes to kernel code | ||
| - Changes to gates 1, 2, or 4-9 | ||
| - ops/FORBIDDEN_PATHS violations (no kernel/relayflowd/src/engine/hn_poller.rs) | ||
|
|
||
| ## Why this is the right work package | ||
|
|
||
| Per ops/TARGET.md: "PR #15 landed testdata/hn-monitor.flow.yaml... but the triggering event comes from a test rather than Hacker News, so gate 2 is AMBER. Write the missing piece: a deterministic poller..." | ||
| Per ops/TARGET.md: "Gate 3 per RFC-0001 is the Garden: flows that build and improve other flows. Do the SMALLEST honest first step, not the whole thing. Good candidate: a flow that reads ops/BACKLOG.md, picks one entry by a deterministic rule, and emits a structured work package." | ||
|
|
||
| Gate 2's done-when (RFC-0001 §3): "a real proactive workload runs as a relayflow." The flow exists, the wake path works, but it's not monitoring anything real yet. The poller completes the circuit. | ||
| This work package: | ||
| - Stays strictly within gate 3 scope (SOFTWARE GARDEN foundation) | ||
| - Is a CODE task as required | ||
| - Does the smallest honest first step | ||
| - Demonstrates "flows that build and improve other flows" (reading a backlog is the first step toward self-proposing work) | ||
| - Has a clear, testable definition of done | ||
| - Avoids all CONSTRAINTS from ops/TARGET.md (no server.rs, no hn-poller files) | ||
|
|
||
| **ONE cycle, about ten minutes — a small working poller beats a large plan.** | ||
| **ONE cycle, ten minutes.** | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| /** | ||
| * Gate-3 foundation: choose the next work package from ops/BACKLOG.md. | ||
| * | ||
| * The selection rule lived only inside a shell one-liner in | ||
| * testdata/backlog-picker.flow.yaml, which made the flow's central claim — | ||
| * that selection is DETERMINISTIC — impossible to assert. Review caught the | ||
| * missing test (PR #20, P1). A rule that cannot be tested is a rule nobody can | ||
| * rely on, so it lives here and the flow calls it. | ||
| * | ||
| * The rule: the first top-level bullet whose title is bold. Deliberately dull. | ||
| * A Garden that proposes its own work must be predictable before it is clever | ||
| * — if two runs over identical input can disagree, nothing downstream can | ||
| * reason about what the system decided or why. | ||
| */ | ||
|
|
||
| /** First bold top-level bullet: `- **Title** rest`. */ | ||
| const ENTRY = /^- \*\*(.+?)\*\*\s*(.*(?:\n .*)*)/m; | ||
|
|
||
| export interface BacklogEntry { | ||
| title: string; | ||
| body: string; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the selected entry, or null when the backlog holds no actionable | ||
| * one. Null is a real answer — "nothing to do" — not a failure. | ||
| */ | ||
| export function selectBacklogEntry(markdown: string): BacklogEntry | null { | ||
| const match = ENTRY.exec(markdown); | ||
| if (!match) return null; | ||
| return { title: match[1] ?? '', body: (match[2] ?? '').trim() }; | ||
| } | ||
|
|
||
| /** Render the selected entry as a work package. */ | ||
| export function renderWorkPackage(entry: BacklogEntry): string { | ||
| return [ | ||
| `# NEXT — ${entry.title}`, | ||
| '', | ||
| 'Selected from ops/BACKLOG.md by the backlog picker (gate 3).', | ||
| 'Selection rule: the first top-level bullet whose title is bold.', | ||
| '', | ||
| '## Scope', | ||
| '', | ||
| entry.body || '(the backlog entry carried no detail beyond its title)', | ||
| '', | ||
| '## Definition of done', | ||
| '', | ||
| 'Restate the entry as passing commands before building against it. An entry', | ||
| 'that cannot be turned into a command is not yet a work package.', | ||
| '', | ||
| ].join('\n'); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { renderWorkPackage, selectBacklogEntry } from '../src/backlog-picker.js'; | ||
|
|
||
| const BACKLOG = `# Backlog | ||
|
|
||
| Some preamble that is not an entry. | ||
|
|
||
| - **First actionable entry** the thing to do | ||
| with a continuation line | ||
|
|
||
| - **Second entry** should not be chosen | ||
| `; | ||
|
|
||
| describe('backlog picker', () => { | ||
| it('selects the first bold top-level bullet', () => { | ||
| const entry = selectBacklogEntry(BACKLOG); | ||
| expect(entry?.title).toBe('First actionable entry'); | ||
| expect(entry?.body).toContain('with a continuation line'); | ||
| }); | ||
|
|
||
| it('is DETERMINISTIC — identical input selects identically, every time', () => { | ||
| // This is the property the flow exists to guarantee and the one the PR | ||
| // claimed without testing. A Garden whose selection can drift gives | ||
| // nothing downstream a stable thing to reason about. | ||
| const runs = Array.from({ length: 25 }, () => selectBacklogEntry(BACKLOG)); | ||
| const first = JSON.stringify(runs[0]); | ||
| for (const run of runs) { | ||
| expect(JSON.stringify(run)).toBe(first); | ||
| } | ||
| }); | ||
|
|
||
| it('renders the same work package for the same entry', () => { | ||
| const entry = selectBacklogEntry(BACKLOG); | ||
| expect(entry).not.toBeNull(); | ||
| expect(renderWorkPackage(entry!)).toBe(renderWorkPackage(entry!)); | ||
| }); | ||
|
|
||
| it('returns null rather than guessing when nothing is actionable', () => { | ||
| expect(selectBacklogEntry('# Backlog\n\nnothing here\n')).toBeNull(); | ||
| expect(selectBacklogEntry('- plain bullet, no bold title\n')).toBeNull(); | ||
| }); | ||
|
|
||
| it('ignores bold text that is not a top-level bullet title', () => { | ||
| const md = 'Some **bold prose** in a paragraph.\n\n- **Real entry** yes\n'; | ||
| expect(selectBacklogEntry(md)?.title).toBe('Real entry'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Gate-3 foundation: turn the first actionable backlog entry into a work package. | ||
| # Selection rule: choose the first top-level bullet whose title is bold. | ||
| version: '0.1.0' | ||
| name: backlog-picker | ||
| description: Read ops/BACKLOG.md and deterministically emit its first work package. | ||
| steps: | ||
| - id: read-backlog | ||
| type: deterministic | ||
| command: "cat ops/BACKLOG.md" | ||
| - id: select-entry | ||
| type: deterministic | ||
| dependsOn: [read-backlog] | ||
| command: >- | ||
| node -e 'const fs=require("node:fs");const text=fs.readFileSync("ops/BACKLOG.md","utf8");const match=text.match(/^- \*\*(.+?)\*\*\s*(.*(?:\n .*)*)/m);if(!match)process.exit(1);process.stdout.write(match[1])' | ||
| - id: emit-package | ||
| type: deterministic | ||
| dependsOn: [select-entry] | ||
| command: >- | ||
| node -e 'const fs=require("node:fs");const text=fs.readFileSync("ops/BACKLOG.md","utf8");const match=text.match(/^- \*\*(.+?)\*\*\s*(.*(?:\n .*)*)/m);if(!match)process.exit(1);process.stdout.write(JSON.stringify({title:match[1],description:match[2].replace(/\s+/g," ").trim(),files_in_scope:["sdk/src/preflight.ts","sdk/tests/preflight.test.ts"],gate:3}))' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L3-L5 Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The emitted AGENTS.md reference: AGENTS.md:L3-L5 Useful? React with 👍 / 👎. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"description":"Read ops/BACKLOG.md and deterministically emit its first work package.","name":"backlog-picker","steps":[{"command":"cat ops/BACKLOG.md","depends_on":[],"id":"read-backlog","max_iterations":1,"retry":{"initial_backoff_ms":100,"jitter_percent":20,"max_backoff_ms":60000,"multiplier":2},"type":"deterministic","verification":{}},{"command":"node -e 'const fs=require(\"node:fs\");const text=fs.readFileSync(\"ops/BACKLOG.md\",\"utf8\");const match=text.match(/^- \\*\\*(.+?)\\*\\*\\s*(.*(?:\\n .*)*)/m);if(!match)process.exit(1);process.stdout.write(match[1])'","depends_on":["read-backlog"],"id":"select-entry","max_iterations":1,"retry":{"initial_backoff_ms":100,"jitter_percent":20,"max_backoff_ms":60000,"multiplier":2},"type":"deterministic","verification":{}},{"command":"node -e 'const fs=require(\"node:fs\");const text=fs.readFileSync(\"ops/BACKLOG.md\",\"utf8\");const match=text.match(/^- \\*\\*(.+?)\\*\\*\\s*(.*(?:\\n .*)*)/m);if(!match)process.exit(1);process.stdout.write(JSON.stringify({title:match[1],description:match[2].replace(/\\s+/g,\" \").trim(),files_in_scope:[\"sdk/src/preflight.ts\",\"sdk/tests/preflight.test.ts\"],gate:3}))'","depends_on":["select-entry"],"id":"emit-package","max_iterations":1,"retry":{"initial_backoff_ms":100,"jitter_percent":20,"max_backoff_ms":60000,"multiplier":2},"type":"deterministic","verification":{}}],"version":"0.1.0"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The diff declares
sdk/tests/backlog-picker.test.tsas required, but does not add that file or register this fixture in an existing test. Consequentlynpm testcan pass without executing the new selection regex or asserting its structured output, leaving the deterministic behavior introduced here entirely unpinned despite this definition of done.AGENTS.md reference: AGENTS.md:L19-L21
Useful? React with 👍 / 👎.