Skip to content

feat(bg): B-0442.1 — missed-substrate detector skeleton (completes 3-skeleton mechanization suite)#3008

Merged
AceHack merged 3 commits into
mainfrom
otto-b0442-1-missed-substrate-detector-skeleton-slice-1-impl-2026-05-13
May 13, 2026
Merged

feat(bg): B-0442.1 — missed-substrate detector skeleton (completes 3-skeleton mechanization suite)#3008
AceHack merged 3 commits into
mainfrom
otto-b0442-1-missed-substrate-detector-skeleton-slice-1-impl-2026-05-13

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented May 13, 2026

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:

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

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:

  • Bounded single-iter mode + unbounded daemon mode (no result accumulation)
  • `parsePositiveMinutes` validation on `--poll-min`

Test results

```
bun test
3 pass
0 fail
7 expect() calls
```

Composes with

🤖 Generated with Claude Code

…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>
Copilot AI review requested due to automatic review settings May 13, 2026 17:41
@AceHack AceHack enabled auto-merge (squash) May 13, 2026 17:41
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread tools/bg/missed-substrate-detector.ts
@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 13, 2026

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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.ts with a no-op pollOnce() and a polling loop supporting --once and --poll-min.
  • Adds tools/bg/missed-substrate-detector.test.ts covering default config, pollOnce(), and runDetector({ 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.

Comment thread tools/bg/missed-substrate-detector.ts Outdated
Comment thread tools/bg/missed-substrate-detector.ts Outdated
Comment thread tools/bg/missed-substrate-detector.ts Outdated
Comment thread tools/bg/missed-substrate-detector.test.ts
…-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>
@AceHack AceHack merged commit 4395f68 into main May 13, 2026
24 of 25 checks passed
@AceHack AceHack deleted the otto-b0442-1-missed-substrate-detector-skeleton-slice-1-impl-2026-05-13 branch May 13, 2026 17:53
AceHack added a commit that referenced this pull request May 13, 2026
…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>
AceHack added a commit that referenced this pull request May 13, 2026
…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>
AceHack added a commit that referenced this pull request May 13, 2026
…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>
AceHack added a commit that referenced this pull request May 13, 2026
…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>
AceHack added a commit that referenced this pull request May 13, 2026
…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>
AceHack added a commit that referenced this pull request May 13, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants