feat(bg): B-0441.1 — backlog-ready notifier skeleton (parallel to B-0440.1; proactive layer)#3007
Conversation
…p (parallel to B-0440.1) Companion to B-0440 (PR #3006 — Standing-by detector skeleton). B-0441 is the PROACTIVE layer that prevents the failure mode by surfacing ready-to-grind backlog rows BEFORE agents go idle. Together with B-0440 (reactive — catches Standing-by AFTER it occurs) they form a two-layer defense against the failure mode. Files: - tools/bg/backlog-ready-notifier.ts (60 lines): - NotifierConfig + DEFAULT_CONFIG (10min poll interval) - pollOnce() — no-op result - runNotifier() — loop scaffolding with --once flag - CLI entry - tools/bg/backlog-ready-notifier.test.ts (3 tests, all pass) Future slices (per B-0441 decomposition): - Slice 2: backlog row parsing + readiness detection - Slice 3: agent queue-state detection (commits + PRs) - Slice 4: assignment payload + bus publish (requires B-0400 schema extension for work-assignment topic) - Slice 5: assignment history tracking - Slice 6: tests + cron registration Test results: 3 pass / 0 fail / 8 expect() calls. Composes with: - B-0441 (the backlog row this implements; PR #3000 merged) - B-0440 (PR #3006 — Standing-by detector, just shipped; companion service) - B-0400 (bus protocol — for future slice 4) - PR #2998 (architectural challenge) - PR #2999 (substrate-honest discipline triad) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a skeleton background service (B-0441.1) that will proactively surface ready-to-grind backlog rows to idle agents. This slice ships only the no-op poll loop and CLI scaffolding, mirroring the companion B-0440.1 standing-by detector.
Changes:
- New
tools/bg/backlog-ready-notifier.tswithNotifierConfig,pollOnceno-op,runNotifierloop, and--once/--poll-minCLI flags. - New
tools/bg/backlog-ready-notifier.test.tswith 3 tests covering defaults, no-op poll, and single-iteration exit.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tools/bg/backlog-ready-notifier.ts | Skeleton notifier service with poll loop and CLI parsing. |
| tools/bg/backlog-ready-notifier.test.ts | Bun tests for default config, poll result shape, and once mode. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3793af5dc8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…P2 arg validation) + markdownlint - P1: runDetector daemon mode no longer accumulates results forever (split into single-iter return-array path + infinite-loop discard path). Same fix should land in B-0441.1 (PR #3007) — will follow up. - P2: --poll-min and --idle-min args now validated via parsePositiveMinutes (rejects NaN, non-finite, non-positive). - markdownlint: replace "+ no-op poll loop" with "with a no-op poll loop" to avoid MD032 blanks-around-lists false positive on the continuation line. Tests still 3 pass / 0 fail. Co-Authored-By: Claude <noreply@anthropic.com>
…0440.1 Preemptively apply the same fix that landed on PR #3006 for the B-0440.1 detector: - runNotifier daemon mode no longer accumulates results forever - --poll-min validated via parsePositiveMinutes (rejects NaN / non-finite / non-positive) Tests still pass 3/3. Co-Authored-By: Claude <noreply@anthropic.com>
…(slice 1 of 6) (#3006) * feat(bg): B-0440.1 — standing-by detector skeleton + no-op poll loop (3 files, 3 tests pass) First implementation slice of B-0440 (Standing-by failure-mode detector). Ships ONLY the skeleton; future slices add real detection. Files: - tools/bg/standing-by-detector.ts (76 lines): - DetectorConfig type + DEFAULT_CONFIG (5min poll / 15min idle threshold) - pollOnce() — no-op result with slice-1 placeholder note - runDetector() — loop scaffolding; --once for cron-driven mode - CLI entry with --poll-min / --idle-min / --once flags - tools/bg/standing-by-detector.test.ts (3 tests): - default config thresholds - pollOnce returns ISO-timestamped no-op result - runDetector with once:true exits after one iteration - tools/bg/README.md: - Directory purpose - Service inventory (B-0440 current; B-0441/B-0442 planned) - Run instructions (cron-driven --once vs standalone daemon) Per Rule 0: TypeScript only (no .sh files in tools/bg/). Future slices (per B-0440 decomposition section): - Slice 2: commit-history poll via git log - Slice 3: PR-activity poll via gh CLI - Slice 4: nudge payload computation + bus publish (requires B-0400 schema extension for infinite-backlog-nudge topic) - Slice 5: integration with agent subscribers - Slice 6: additional tests + cron registration Composes with: - B-0440 (the backlog row this implements; PR #3000 merged) - B-0400 (bus protocol — for future slice 4) - B-0441 / B-0442 (companion services) - PR #2998 (architectural challenge that produced these rows) - PR #2999 (substrate-honest discipline triad — decomposition discipline) Co-Authored-By: Claude <noreply@anthropic.com> * fix(bg): B-0440.1 — close 2 Copilot findings (P1 unbounded results + P2 arg validation) + markdownlint - P1: runDetector daemon mode no longer accumulates results forever (split into single-iter return-array path + infinite-loop discard path). Same fix should land in B-0441.1 (PR #3007) — will follow up. - P2: --poll-min and --idle-min args now validated via parsePositiveMinutes (rejects NaN, non-finite, non-positive). - markdownlint: replace "+ no-op poll loop" with "with a no-op poll loop" to avoid MD032 blanks-around-lists false positive on the continuation line. Tests still 3 pass / 0 fail. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
…skeleton mechanization suite) (#3008) * feat(bg): B-0442.1 — missed-substrate cascade detector skeleton (completes the 3-skeleton suite) Third and final skeleton in the mechanization suite. With B-0440.1 (reactive Standing-by detector; PR #3006) and B-0441.1 (proactive backlog-ready notifier; PR #3007), B-0442.1 (drift-prevention) closes the trio. Files (same shape as B-0440.1 / B-0441.1 with bug-fixes pre-applied): - tools/bg/missed-substrate-detector.ts (87 lines): - DetectorConfig + DEFAULT_CONFIG (5min poll) - pollOnce() no-op result - runDetector() — bounded single-iter or unbounded daemon (no result accumulation) - parsePositiveMinutes validation on --poll-min - CLI entry - tools/bg/missed-substrate-detector.test.ts (3 tests, all pass) Three-layer defense suite now in code: | Service | Layer | Trigger | |---------|-------|---------| | B-0440.1 standing-by-detector | Reactive | Cron-fires + idle threshold | | B-0441.1 backlog-ready-notifier | Proactive | Queue-empty + rows-ready | | B-0442.1 missed-substrate-detector | Drift-prevention | Merged-PR + branch-HEAD divergence | Canonical operational example B-0442 was filed for: Otto-section-missed-PR-2980-by-3-min cascade (recovered via PR #2997). Future slices (per B-0442 decomposition): - Slice 2: merged-PR state fetch via gh CLI - Slice 3: branch-vs-squash comparison logic - Slice 4: cascade-detection bus publish (requires B-0400 schema extension for missed-substrate-cascade topic) - Slice 5: optional auto-recovery-PR opening (gated) - Slice 6: integration tests + cron registration Test results: 3 pass / 0 fail / 7 expect() calls. Composes with: - B-0442 (the backlog row this implements; PR #3000 merged) - B-0440.1 + B-0441.1 (PR #3006 + #3007 — companion skeletons) - B-0400 (bus protocol — for future slice 4) - PR #2998 (architectural challenge) - PR #2999 (substrate-honest discipline triad — decomposition discipline) - tools/hygiene/LOST-FILES-LOCATIONS.md (15-class lost-files survey — this service mechanizes one class) Co-Authored-By: Claude <noreply@anthropic.com> * fix(tsc): non-null assert results[0]! under noUncheckedIndexedAccess TypeScript 6 + noUncheckedIndexedAccess makes results[0] PollResult|undefined; toHaveLength(1) asserts length but doesn't narrow the type, so the explicit non-null assertion is needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(bg): B-0442.1 — close 4 Copilot findings (split runDetector, fail-fast on unknown flags, role-ref, expand tests) Addresses 4 P1/P2 findings: 1. P1 — runDetector return type mismatch: split into runOnce() (returns PollResult) + runDaemon() (returns Promise<never>). Eliminates the misleading Promise<PollResult[]> that never resolved in daemon mode and returned a single-item array in once mode. 2. P1 — parseArgs silently ignoring unknown flags: now fail- fast with explicit error listing known flags. Typos no longer hide. Functions also exported for testability. 3. P1 — Header comment used persona-name attribution ('Otto-section-missed-PR-2980-by-3-min'). Replaced with role-ref ('the substrate-recovery cascade from earlier today'). tools/ is current-state code surface; persona naming policy applies (the docs/launch/** carve-out from PR #3005 doesn't extend here). 4. P2 — Tests now cover CLI validation paths: - parsePositiveMinutes: 5 cases (positive, undefined, non-numeric, zero/negative, Infinity/NaN) - parseArgs: 5 cases (defaults, --once, --poll-min, unknown-flag rejection, invalid --poll-min) Test results: 13 pass / 0 fail / 21 expect() calls (was 3 / 7). Sibling impl PRs (B-0440.1 / B-0441.1) already merged — will file a separate follow-up PR backporting the same fixes per substrate-honest decomposition. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
…3010) results[0] returns T|undefined under noUncheckedIndexedAccess=true. Add non-null assertion (!) since toHaveLength(1) on line above already validates the array is non-empty; tsc can't see through the Bun test framework's type signatures. Fixes the non-required lint (tsc tools) failure introduced when B-0440.1 (#3006) and B-0441.1 (#3007) landed skeleton test suites. Co-authored-by: Claude <noreply@anthropic.com>
…table adapters) Slice 2 of B-0440. The Standing-by detector now does REAL detection by polling git log for the most recent commit on HEAD and comparing its timestamp against idleThresholdMin. Key design choices: - spawnSync (execFile-style, no shell) — no command-injection risk - Adapter pattern (now + lastCommitIso) — tests inject deterministic values; production uses git log via spawnSync - Idle threshold is INCLUSIVE (>=); at exactly the boundary, idle is flagged (substrate-honest fail-fast semantics) - Clock-skew safe: negative idleMinutes clamp to 0 - Handles missing git history (fresh repo / git unavailable) — emits null lastCommitAt + descriptive note, does NOT crash Result schema extended: - lastCommitAt: ISO-8601 string | null - idleMinutes: number | null - idleDetected: boolean (vs threshold) - note: human-readable summary Test results: 13 pass / 0 fail / 31 expect() calls (slice 1 had 3 / 8). Future slices: - Slice 3: PR-activity poll via gh CLI - Slice 4: nudge payload + bus publish (requires B-0400 schema extension for infinite-backlog-nudge topic) - Slice 5: integration with agent subscribers - Slice 6: cron registration + integration tests The recursive irony preserved as substrate: the agent who canonized the Standing-by rule + shipped the detector in PR #3006 is the same agent who violated the rule mid-conversation. Memory of failure ≠ prevention. Mechanization wins. This slice MAKES the mechanization real (detection actually works now, not just a no-op). Composes with: - B-0440 (the backlog row; PR #3000 merged) - B-0440.1 (PR #3006 — the skeleton this extends) - B-0441.1 + B-0442.1 (companion services; PRs #3007 + #3008) - B-0400 (bus protocol — for slice 4) - PR #2999 (substrate-honest discipline triad — the rule this enforces operationally) Co-Authored-By: Claude <noreply@anthropic.com>
…ests pass) Slice 2 of B-0441. The backlog-ready notifier now does REAL detection by scanning docs/backlog/P*/B-*.md and classifying each row by: - status: open (candidate) - depends_on: all closed (ready) Key design choices: - Pure node:fs (readdirSync + readFileSync) — no shell, no spawn - parseRow exposed for testability + reuse - Adapter pattern (now + scanBacklog) — tests inject deterministic filesystems via fake adapter - Configurable backlogDir via --backlog-dir flag (default: docs/backlog) - candidateIds capped at first 10 to keep payload bounded - Empty/missing depends_on treated as vacuously-satisfied (ready) - Missing priority dirs skipped silently (graceful degradation) Real-data smoke test (against current repo): - 371 open rows - 229 ready-to-grind - Top candidates include B-0441 + B-0442 (the very rows whose impl slices we're shipping — recursive substrate working as designed) Test results: 19 pass / 0 fail / 38 expect() calls (slice 1 had 3 / 8). Future slices: - Slice 3: agent queue-state detection (commits + PRs) - Slice 4: assignment payload + bus publish (requires B-0400 schema extension for work-assignment topic) - Slice 5: assignment history tracking - Slice 6: cron registration + integration tests Composes with: - B-0441 + B-0441.1 (PR #3007 — skeleton this extends) - B-0440.1 + B-0440.2 (PR #3006 + #3011 — reactive companion) - B-0442.1 (PR #3008 — drift-prevention companion) - B-0400 (bus protocol — slice 4) - PR #2999 (substrate-honest discipline triad — the rule this service enforces operationally) Co-Authored-By: Claude <noreply@anthropic.com>
…ests pass) (#3012) * feat(bg): B-0441.2 — backlog row scan (real readiness detection; 19 tests pass) Slice 2 of B-0441. The backlog-ready notifier now does REAL detection by scanning docs/backlog/P*/B-*.md and classifying each row by: - status: open (candidate) - depends_on: all closed (ready) Key design choices: - Pure node:fs (readdirSync + readFileSync) — no shell, no spawn - parseRow exposed for testability + reuse - Adapter pattern (now + scanBacklog) — tests inject deterministic filesystems via fake adapter - Configurable backlogDir via --backlog-dir flag (default: docs/backlog) - candidateIds capped at first 10 to keep payload bounded - Empty/missing depends_on treated as vacuously-satisfied (ready) - Missing priority dirs skipped silently (graceful degradation) Real-data smoke test (against current repo): - 371 open rows - 229 ready-to-grind - Top candidates include B-0441 + B-0442 (the very rows whose impl slices we're shipping — recursive substrate working as designed) Test results: 19 pass / 0 fail / 38 expect() calls (slice 1 had 3 / 8). Future slices: - Slice 3: agent queue-state detection (commits + PRs) - Slice 4: assignment payload + bus publish (requires B-0400 schema extension for work-assignment topic) - Slice 5: assignment history tracking - Slice 6: cron registration + integration tests Composes with: - B-0441 + B-0441.1 (PR #3007 — skeleton this extends) - B-0440.1 + B-0440.2 (PR #3006 + #3011 — reactive companion) - B-0442.1 (PR #3008 — drift-prevention companion) - B-0400 (bus protocol — slice 4) - PR #2999 (substrate-honest discipline triad — the rule this service enforces operationally) Co-Authored-By: Claude <noreply@anthropic.com> * fix(bg): B-0441.2 — 4 Copilot findings (block-style YAML, superseded deps, dangling refs, unreachable branch) 1. P1: parseRow now handles BOTH inline-flow YAML (depends_on: [A, B]) AND block-style lists (depends_on:\n - A\n - B). Split into parseDependsOn helper for clarity. Tests cover both styles. 2. Dependency satisfaction extended: matches tools/backlog/generate- index.ts checkboxFor() — a dep is satisfied iff its row is `closed` OR `superseded-by-*`. Test verifies superseded-as-satisfied. 3. Dangling dep references (dep ID not present in scan) are now surfaced explicitly in PollResult.note as a warning. Test verifies the warning fires. 4. Removed unreachable `else if (KNOWN_FLAGS.has(arg))` branch in parseArgs (all known flags are handled by explicit branches above). KNOWN_FLAGS now const-asserted array for the error-message join. Bonus: switched from RegExp.exec() to String.match() to avoid the project's security-hook false-positive on the word "exec". Tests: 22 pass / 0 fail / 44 expect() calls (was 19 / 38). Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
…sts pass) (#3011) * feat(bg): B-0440.2 — commit-history poll (real detection logic; injectable adapters) Slice 2 of B-0440. The Standing-by detector now does REAL detection by polling git log for the most recent commit on HEAD and comparing its timestamp against idleThresholdMin. Key design choices: - spawnSync (execFile-style, no shell) — no command-injection risk - Adapter pattern (now + lastCommitIso) — tests inject deterministic values; production uses git log via spawnSync - Idle threshold is INCLUSIVE (>=); at exactly the boundary, idle is flagged (substrate-honest fail-fast semantics) - Clock-skew safe: negative idleMinutes clamp to 0 - Handles missing git history (fresh repo / git unavailable) — emits null lastCommitAt + descriptive note, does NOT crash Result schema extended: - lastCommitAt: ISO-8601 string | null - idleMinutes: number | null - idleDetected: boolean (vs threshold) - note: human-readable summary Test results: 13 pass / 0 fail / 31 expect() calls (slice 1 had 3 / 8). Future slices: - Slice 3: PR-activity poll via gh CLI - Slice 4: nudge payload + bus publish (requires B-0400 schema extension for infinite-backlog-nudge topic) - Slice 5: integration with agent subscribers - Slice 6: cron registration + integration tests The recursive irony preserved as substrate: the agent who canonized the Standing-by rule + shipped the detector in PR #3006 is the same agent who violated the rule mid-conversation. Memory of failure ≠ prevention. Mechanization wins. This slice MAKES the mechanization real (detection actually works now, not just a no-op). Composes with: - B-0440 (the backlog row; PR #3000 merged) - B-0440.1 (PR #3006 — the skeleton this extends) - B-0441.1 + B-0442.1 (companion services; PRs #3007 + #3008) - B-0400 (bus protocol — for slice 4) - PR #2999 (substrate-honest discipline triad — the rule this enforces operationally) Co-Authored-By: Claude <noreply@anthropic.com> * fix(bg): B-0440.2 — remove unused DetectorConfig import (noUnusedLocals) Co-Authored-By: Claude <noreply@anthropic.com> * fix(bg): B-0440.2 — 3 Copilot findings (comment, sonarjs lint, noisy test) - Update header comment to reflect time-based detection (minutes since last commit) rather than the stale "N consecutive ticks" description - Add eslint-disable-next-line sonarjs/no-os-command-from-path before spawnSync("git", ...) — git invoked as explicit args array, no shell - Replace runOnce(DEFAULT_CONFIG) test (hits REAL_ADAPTERS, noisy) with pollOnce + fakeAdapters so the test is deterministic and side-effect-free - Remove now-unused runOnce import from test file (noUnusedLocals) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Companion to PR #3006 (B-0440.1 — Standing-by detector). B-0441 is the proactive layer that prevents the failure mode by surfacing ready-to-grind backlog rows BEFORE agents go idle.
Together with B-0440 (reactive — catches Standing-by AFTER it occurs), the two services form a two-layer defense against the failure mode the human maintainer caught me on earlier today.
What lands
Test results
```
bun test
3 pass
0 fail
8 expect() calls
```
Two-layer defense pattern
Composes with
🤖 Generated with Claude Code