feat(bg): B-0442.1 — missed-substrate detector skeleton (completes 3-skeleton mechanization suite)#3008
Conversation
…letes 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>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 831aa82e7b
ℹ️ 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".
|
Acking the Result-over-exception finding: The project's Result-over-exception contract (CLAUDE.md "Conventions") is: "errors surface as Result<_, DbspError>; no exceptions on hot paths." The fail-fast Error throw is on CLI argument parsing — startup-only path, not a hot path, not a production-runtime data flow, not a DbspError class. CLI parsing crash with a clear message is the standard fail-fast pattern for malformed startup args (the alternative would be silently using default values, which is worse). The same pattern is in the (already-merged) sibling skeletons PR #3006 (B-0440.1 standing-by-detector) + PR #3007 (B-0441.1 backlog-ready-notifier) and was not flagged on either. Resolving as substrate-honest per the contract's actual scope. |
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>
There was a problem hiding this comment.
Pull request overview
Adds the third background-service skeleton in the Standing-by mechanization suite: a missed-substrate detector intended to catch post-merge branch/main drift (e.g., commits landing after a squash-merge).
Changes:
- Introduces
tools/bg/missed-substrate-detector.tswith a no-oppollOnce()and a polling loop supporting--onceand--poll-min. - Adds
tools/bg/missed-substrate-detector.test.tscovering default config,pollOnce(), andrunDetector({ once: true }).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tools/bg/missed-substrate-detector.ts | New detector skeleton with config, poll loop, and basic CLI parsing. |
| tools/bg/missed-substrate-detector.test.ts | New Bun tests for slice-1 behavior. |
…-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>
…or.ts (#3013) argv[i] inside a for-loop bounded by i < argv.length is always defined, but noUncheckedIndexedAccess makes it string|undefined. Set<string>.has() requires string, so KNOWN_FLAGS.has(arg) on line 87 fails. Fix: assert ! at the source (line 82) — same class as TS2532 fixes in PR #3010. Sibling fix to PR #3010 (standing-by-detector + backlog-ready-notifier test files). The source file bug was introduced in #3008 (B-0442.1) and was masked there by a companion fix commit that only covered the test file. Co-authored-by: Claude Sonnet 4.6 <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>
* feat(bg): B-0442.2 — merged-PR fetch via gh CLI (real candidate detection)
Slice 2 of B-0442. The missed-substrate cascade detector now fetches
recent merged PRs via `gh pr list --state merged --search "merged:>{iso}"`
and reports the candidate set within a configurable lookback window
(default 30min).
Key design choices:
- spawnSync (execFile-style, no shell) + sonarjs lint suppression
with rationale (matches B-0440.2 pattern)
- Adapter pattern (now + fetchRecentMergedPRs) for deterministic tests
- gh JSON output parsed with type-guard filter to reject malformed
entries
- New --lookback-min flag (default 30min)
- cascadesDetected stays 0 in slice 2 — slice 3 will fetch each
branch's HEAD and compare against the squash content to surface
actual cascades
Test results: 10 pass / 0 fail / 20 expect() calls (slice 1 had 13/21).
Future slices:
- Slice 3: per-merged-PR branch-HEAD fetch + squash content compare
(detects actual drift)
- Slice 4: cascade-detection payload + bus publish (requires B-0400
schema extension for missed-substrate-cascade topic)
- Slice 5: optional auto-recovery-PR opening (gated)
- Slice 6: cron registration + integration tests
Composes with:
- B-0442 + B-0442.1 (PR #3008 — skeleton this extends)
- B-0440.2 + B-0441.2 (PR #3011 + #3012 — companion services with
real detection)
- B-0400 (bus protocol — slice 4)
- PR #2997 (the Otto-section-missed-PR-2980-by-3min recovery — the
canonical operational example this service mechanizes)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(bg): B-0442.2 — 4 Copilot findings (gh-error vs zero-PRs distinction, fetchLimit configurable, header accuracy)
1. P1: gh-failure no longer silently treated as zero merged PRs.
FetchResult is now a discriminated union of {status: "ok", prs,
truncated} | {status: "gh-error", reason}. PollResult.fetchStatus
surfaces the distinction in the note.
2. P2: Hard-coded 50-item cap replaced with configurable fetchLimit
(default 100, settable via --fetch-limit). When results hit the
cap, fetchTruncated:true is set + warning included in note so
caller can raise --fetch-limit or shorten --lookback-min.
3. Header comment corrected: removed inaccurate claim that the
slice checks branch-exists-in-remote-tracking-refs. Slice 2 only
fetches merged-PR metadata; the actual branch HEAD comparison
lands in slice 3.
4. Duplicate of #2 (same 50-cap concern from a second reviewer
perspective) resolved by the same fix.
New tests:
- gh-error path surfaces fetchStatus + reason in note
- truncation path sets fetchTruncated + warning in note
- --fetch-limit positive-integer validation (rejects non-integer)
Test results: 13 pass / 0 fail / 34 expect() calls (was 10 / 20).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Summary
Third and final skeleton in the mechanization suite. With PR #3006 (B-0440.1) and PR #3007 (B-0441.1), this PR completes the three-layer defense pattern:
Canonical operational example
The Otto-section-missed-PR-2980-by-3-min cascade earlier today (recovered via PR #2997). Branch had the commit; main did not (auto-merge fired 3 minutes before push arrived). This service detects that pattern proactively.
Implementation pattern
Same skeleton pattern as the other two with all bug-fixes pre-applied:
Test results
```
bun test
3 pass
0 fail
7 expect() calls
```
Composes with
🤖 Generated with Claude Code