Skip to content

fix(pulse): stop setup.ts clobbering an existing PULSE.toml - #1642

Open
elhoim wants to merge 1 commit into
danielmiessler:mainfrom
elhoim:fix/pulse-setup-preserve-config
Open

fix(pulse): stop setup.ts clobbering an existing PULSE.toml#1642
elhoim wants to merge 1 commit into
danielmiessler:mainfrom
elhoim:fix/pulse-setup-preserve-config

Conversation

@elhoim

@elhoim elhoim commented Jul 26, 2026

Copy link
Copy Markdown

The problem

PULSE/setup.ts unconditionally overwrites PULSE.toml with a three-job template. There is no existence check and no backup, so running the documented bun run setup.ts on an already-configured install destroys the daemon's config of record in a single call.

What goes: [voice], [telegram], [imessage], [observability] including the server port, every [notifications.*] routing rule and threshold, [hooks] blocked_skills, [work], [bunker], [syslog], [local_intelligence], [telos], the whole [da] block (heartbeat, diary and growth schedules, cost ceiling), and every [[job]] cron entry — replaced by [worker] plus three samples.

Most personal installs are not git repositories, so this is unrecoverable.

The fix is suggested by the surrounding code: the .env write about twenty lines below already checks for existence and appends rather than clobbering. The two branches of the same function disagreed with each other.

The fix

Every config write now goes through one writeConfigPreserving helper:

  • absent → created, so first-run onboarding is unchanged
  • present → preserved untouched, and the run says so
  • --force → replaced, but only after announcing it and copying the existing file aside as <file>.backup-<timestamp>

Create-only is the right default here rather than a merge: a partial merge of a TOML the operator has hand-tuned produces a config that is neither theirs nor the template's, and silently reintroducing sample jobs into a live scheduler is its own failure. Preserve-and-tell leaves the operator in control, and --force plus a backup covers the genuine reset case.

The launch agent write is guarded the same way, for the same reason.

Tests

test/pulse/setup.test.ts, 9 cases: a fresh path is created, an existing config survives byte-identical, --force replaces it, the backup holds the original contents, and the announcement fires before anything on disk changes.

9 pass
0 fail
26 expect() calls

Separately verified against the exported helper with an independent case set, including one the unit tests do not cover: repeated --force runs keep distinct backups rather than overwriting the previous one.

✓ absent → created
✓ present → preserved
✓ preserved: real job survived
✓ preserved: template not leaked in
✓ force → overwritten
✓ force announced before the write
✓ force: backup holds the ORIGINAL
✓ repeat force keeps distinct backups — 2 backup(s)

Notes

No behaviour change for a fresh install. The only way to lose config now is to pass --force, which prints the backup path first.

setup.ts wrote its three-job worker template over PULSE.toml with no
existence check and no backup, so `bun run setup.ts` on a configured
install destroyed the daemon's config of record in one call — modules,
ports, notification routing, blocked-skills lists, the DA block and every
cron job. Most installs are not git repos, so the loss was unrecoverable.
The .env write twenty lines later already checked before touching the
file; the function contradicted itself.

All config writes now go through writeConfigPreserving: create when
absent, preserve when present, replace only under an explicit --force,
which announces the replacement and copies the file to
<file>.backup-<timestamp> first. On a preserved run the generated worker
block is printed so it can be merged by hand.

Same fix applied to the launch agent write, which also overwrote the
installed plist unconditionally and would discard local edits. The other
writes in this path (.env append, /etc/hosts append, mkcert certs) were
already guarded and are unchanged.

main() is now behind an import.meta.main guard so the config-write
helpers are importable without provisioning.
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