Skip to content

fix(scheduler): make scheduled skills actually run (CLI v2.3.3 + Plugin v2.4.12)#178

Merged
kengio merged 2 commits into
mainfrom
fix/scheduler-headless
May 13, 2026
Merged

fix(scheduler): make scheduled skills actually run (CLI v2.3.3 + Plugin v2.4.12)#178
kengio merged 2 commits into
mainfrom
fix/scheduler-headless

Conversation

@kengio
Copy link
Copy Markdown
Collaborator

@kengio kengio commented May 13, 2026

Summary

The scheduler was structurally broken in v2.3.2: plists emitted by onebrain register-schedule invoked onebrain --vault X --skill /name --headless, but neither onebrain nor claude CLI implements those flags. Every scheduled run silently exited 78 (EX_CONFIG) with empty stdout/stderr; the .err.md log never wrote, so /doctor's 3-strike threshold never tripped.

Reproduction (pre-fix): launchctl start com.onebrain.dailyLastExitStatus = 19968 (real exit 78); manual onebrain --vault X --skill /daily --headlesserror: unknown option '--vault'.

Fix — Option (a)

New hidden onebrain run-skill --vault X --skill /name [--arg k=v ...] subcommand shells out to claude -p "/onebrain:skill args" --add-dir <vault> with cwd=<vault>. Plist generator rewritten in launchd.ts; register-schedule resolves command-mode binary names to absolute paths via /usr/bin/which (launchd's restricted PATH doesn't include Homebrew/Bun/~/.local/bin).

See CHANGELOG.md v2.3.3 entry for the full bullet list.

Reviewer feedback addressed (3 parallel rounds before opening PR)

  • R1 correctness (8 findings): one-shot plist path divergence fixed; .ts argv[1] fallback added; empty-key/empty-skill defensive throws; unused env vars removed; relative-path resolution now vault-rooted
  • R2 coverage (8 findings): direct unit tests for resolveCommandBinary (4 branches incl. relative); ls regex loosened for Nix-style FHS variation; CLAUDE_BIN env-override path now exercised
  • R3 silent-failure (5 findings): existsSync guard on absolute-path branch; POSIX 128+signal exit code mapping; CLAUDE_BIN typo warning; entry.command no longer mutated; /doctor stale-plist check filed as follow-up

Test plan

  • bun test src/commands/run-skill.test.ts src/lib/scheduler/launchd.test.ts src/commands/register-schedule.test.ts — 55/55 pass (23 new)
  • bunx tsc --noEmit — clean
  • bunx biome check (changed files) — clean
  • bun run build — produces dist/onebrain 0.50 MB
  • ./dist/onebrain register-schedule --vault <real-vault> --dry-run — emits valid onebrain run-skill ... plists with no --headless references
  • After merge: re-run onebrain register-schedule on existing vaults to regenerate plists; manually launchctl start com.onebrain.daily and confirm LastExitStatus = 0

Breaking change

Plists generated by pre-v2.3.3 CLI silently fail every fire — users must re-run onebrain register-schedule after upgrading. A /doctor check that detects stale-shape plists is filed as a follow-up.

Bumps

  • CLI: 2.3.2 → 2.3.3 (TypeScript source changes)
  • Plugin: 2.4.11 → 2.4.12 (INSTRUCTIONS.md "Headless invocation" rewrite)

🤖 Generated with Claude Code

kengio added 2 commits May 13, 2026 11:42
The scheduler in v2.3.2 was structurally broken: plists emitted by
`onebrain register-schedule` invoked `onebrain --vault X --skill /name
--headless`, but neither `onebrain` nor `claude` CLI implemented those
flags. Every scheduled run silently exited 78 (EX_CONFIG) with empty
stdout/stderr files; the `.err.md` log mechanism never wrote, so
`/doctor`'s 3-strike threshold never tripped.

Fix (Option a per design discussion):

- New hidden `onebrain run-skill --vault X --skill /name [--arg k=v ...]`
  subcommand that shells out to `claude -p "/onebrain:skill args"
  --add-dir <vault>` with `cwd=<vault>`. Resolves `claude` via
  CLAUDE_BIN env -> known install prefixes -> PATH so launchd's
  restricted PATH doesn't break the spawn.

- Plist generator rewritten in `src/lib/scheduler/launchd.ts`: emits
  `onebrain run-skill ...` for both recurring and one-shot skill
  modes. `labelForEntry` uses basename for command mode so labels
  stay stable across bare/absolute command forms.

- `register-schedule` resolves command-mode binary names to absolute
  paths via `/usr/bin/which` (launchd's PATH excludes Homebrew/Bun/
  ~/.local/bin). Absolute paths now also existsSync-checked;
  relative paths resolve against the vault root, not process.cwd().
  No longer mutates caller-supplied entries.

- `register-schedule --test` rewired through `runSkillCommand`.

- POSIX-conventional signal exit codes (128 + signal number).
  CLAUDE_BIN typos surface as warnings. `--arg` rejects empty keys.
  buildPrompt throws on empty skill names. One-shot plist's
  self-delete path derives from the same `label` as `launchctl
  bootout` so they can never drift.

- Plugin INSTRUCTIONS.md "Headless invocation" section rewritten to
  describe the real contract (replaces the previous fiction that
  no binary implemented).

- 23 new unit tests with explicit `not.toContain('--headless')`
  regression sentinels covering buildPrompt, resolveCommandBinary
  (4 branches incl. relative-path resolution), the spawn surface,
  signal/error/exit-code propagation, and CLAUDE_BIN typo + override.

Bumps: CLI 2.3.2 -> 2.3.3, Plugin 2.4.11 -> 2.4.12.

Plists generated by pre-v2.3.3 CLI keep failing silently after
upgrade; users must re-run `onebrain register-schedule`. A
`/doctor` check for stale-shape plists is filed as a follow-up.

3-round parallel sub-agent review (correctness/coverage/silent-
failure) flagged 14 issues across all rounds; all critical/high
items addressed in this PR, 2 deferred items filed in the OneBrain
CLI tracker.
The .ts/.js rejection in resolveSkillCliPath broke CI: when `bun test`
runs on Ubuntu without a global `onebrain` install, the resolver falls
back to `which onebrain`, which throws. The 9 affected tests use
--dry-run and don't actually need a runnable binary path.

The reviewer's underlying concern (dev runs `bun run src/index.ts
register-schedule` from source and installs a .ts plist) is real but
rare; the /doctor stale-plist follow-up already covers detection at
the right layer (installed plists, not register time).

Restores the original `process.argv[1] ?? 'onebrain'` behavior and
expands the deferred /doctor task to also flag plists with .ts/.js
ProgramArguments[0].
@kengio kengio merged commit 24f5816 into main May 13, 2026
1 check passed
@kengio kengio deleted the fix/scheduler-headless branch May 13, 2026 04:46
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.

1 participant