# Side-Effects Review — JobLoader per-entry resilience

**Version / slug:** `jobloader-per-entry-resilience`
**Date:** `2026-04-19`
**Author:** Dawn (autonomous, instar-bug-fix job AUT-5808-wo)
**Second-pass reviewer:** not required (LOW risk, per instar-bug-fix grounding)

## Summary of the change

`loadJobs()` in `src/scheduler/JobLoader.ts` is changed from fail-fast
(first invalid entry throws and aborts the whole load) to fail-soft at
the entry level (each invalid entry is logged and skipped; valid entries
load). Structural errors (missing file, unparseable JSON, non-array
root) still throw because those indicate nothing can be loaded at all.
`validateJob` is unchanged and still throws on invalid input — callers
that want single-entry validation (tests, future CLI validators) are
unaffected. A new test covers the skip path; all existing tests
continue to pass.

Files touched:

- `src/scheduler/JobLoader.ts` — `loadJobs()` body only.
- `tests/unit/JobLoader.test.ts` — one additional positive test.

Decision-point surface: this change has no block/allow authority
surface; it's a resilience fix on a JSON loader.

## Decision-point inventory

- `JobLoader.loadJobs()` — **modify** — was: throw on first invalid
  entry; now: log+skip invalid entries, return valid subset.
- `JobLoader.validateJob()` — **pass-through** — unchanged; still
  throws. Exported so CLI validators / unit tests can reuse it.

---

## 1. Over-block

**What legitimate inputs does this change reject that it shouldn't?**

No block/allow surface — over-block not applicable. The change does
the opposite: it *accepts* more inputs (files with some invalid
entries) than before. Valid entries that were previously rejected
(because an earlier sibling was invalid) are now accepted.

---

## 2. Under-block

**What failure modes does this still miss?**

No block/allow surface — under-block not applicable. Per-entry
validation is unchanged; `validateJob` still catches the same set of
malformed shapes it always did. The only difference is whether a catch
aborts the whole load or only the bad entry.

One adjacent concern: a skipped job is a silently-not-running job.
This is mitigated by `console.error` per skipped entry (with slug and
error) and a `console.warn` summary (`Loaded N valid job(s); skipped M
invalid entry(ies)`). Full escalation (attention-queue entry, Telegram
alert) is Proposal B from the originating feedback cluster and is
tracked as a separate improvement.

---

## 3. Level-of-abstraction fit

**Is this at the right layer?**

Yes. `JobLoader` is a pure loader/validator — it converts bytes on
disk into a typed array of job definitions. Resilience at this layer
is the correct place to isolate a malformed entry: every downstream
consumer (`JobScheduler.start`, `startServer`, the HTTP API, the
dashboard, the feedback pipeline, the attention queue, the Telegram
poller) should see the same "valid-only" view and not have to
implement its own defensive skip. Moving resilience any higher (e.g.,
wrapping `JobScheduler.start` in a try/catch) would lose the per-entry
granularity; any lower (e.g., validating inside each consumer) would
duplicate logic.

`validateJob` stays pure and throwing, so future smarter gates (a
`instar jobs validate` CLI, an editor-time schema check, a staged-diff
precommit) can reuse it as a plain classifier without re-implementing
validation.

---

## 4. Signal vs authority compliance

**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)

**Does this change hold blocking authority with brittle logic?**

- [x] No — this change has no block/allow surface.

`JobLoader` is a loader, not a gate. It doesn't decide whether a
message is allowed, whether an outbound action may fire, or whether a
user may take an action. The "skip invalid entry" path is a local
recovery, not a policy decision; the one who decides whether a skipped
entry matters operationally is the human reading `console.error` (and,
in the future, Proposal B's supervisor escalation).

---

## 5. Interactions

- **Shadowing:** no other validation runs before `loadJobs`; schedulers
  consume its return value directly. Skipped entries simply don't
  appear in the returned array; downstream code continues to see a
  clean, typed list. No shadowing risk.
- **Double-fire:** the loader runs once per `loadJobs` call. Skipped
  entries aren't re-processed anywhere else.
- **Races:** no shared mutable state. Pure function over a filesystem
  read; concurrent reads are safe.
- **Feedback loops:** none. The loader does not write back to the
  jobs file; there's no loop between "skipped entries" and "future
  loader behavior."
- **Grounding audit:** `auditGrounding(jobs)` continues to run over
  the valid subset, which is the intended behavior — ungrounded
  warnings apply only to jobs the scheduler is actually trying to run.

---

## 6. External surfaces

- **Other agents on the same machine:** none. This is a per-agent
  loader; each agent loads its own jobs file.
- **Install base:** existing agents with valid jobs files see
  identical behavior. Agents with a file that currently crashes the
  loader will boot successfully after upgrade with skipped entries
  logged to stderr.
- **External systems:** none. No Telegram/Slack/GitHub/Cloudflare
  impact.
- **Persistent state:** none. No database, ledger, or memory-file
  writes.
- **Timing / runtime:** strictly improves boot reliability under
  malformed config; no regression possible in the positive case.
- **Stdout/stderr:** new `console.error` lines per skipped entry and a
  summary `console.warn`. Agents that scrape startup logs for errors
  will see new "Skipping invalid job" lines when applicable — this is
  a feature, not a regression: today, those agents see no line at all
  because the whole process dies before logging anything about the
  jobs layer.

---

## 7. Rollback cost

Pure code change. Revert the commit, ship as next patch — no
persistent state, no data migration, no agent-state repair needed, no
user-visible regression during the rollback window. Any jobs file
that previously crashed would go back to crashing; any that loaded
cleanly would continue to load cleanly.

---

## Conclusion

Low-risk, narrowly-scoped resilience fix on a pure loader. No
block/allow authority surface, no signal-vs-authority concerns, no
external state touched. The change captures exactly Proposal A from
the originating feedback cluster (reporter electruck, 2026-04-17),
preserves `validateJob` exactly so that Proposal C (CLI validator) can
reuse it, and leaves Proposal B (supervisor escalation) as a separate
improvement. Clear to ship.

## Evidence pointers

- Build: `npm run build` passes (tsc + generate-builtin-manifest.cjs).
- Tests: `npx vitest run tests/unit/JobLoader.test.ts` — 41 passed
  (40 pre-existing + 1 new "skips invalid entries and loads valid
  ones" test).
- The new test encodes the exact shape of the failing entry from the
  reporter's incident (`execute.type: "bash"`, missing `name` +
  `priority`) and asserts that sibling valid entries still load.


## Post-publish note

Published as v0.28.64 on 2026-04-19. Release artifacts (renamed NEXT.md, regenerated builtin-manifest.json) committed together.
