Skip to content

feat: persistent + runtime-toggleable enabled state (v1.3.0)#7

Open
royosherove wants to merge 4 commits into
mainfrom
feat/runtime-toggle
Open

feat: persistent + runtime-toggleable enabled state (v1.3.0)#7
royosherove wants to merge 4 commits into
mainfrom
feat/runtime-toggle

Conversation

@royosherove
Copy link
Copy Markdown
Member

Problem

The Alt+R / /review in-TUI toggle was in-memory only \u2014 every session_start reset it to true. External tools (e.g. roundhouse gateway) couldn't ask pi-hard-no to turn review off without restarting the session.

Solution

Add enabled: boolean to AutoReviewSettings (default true, persisted in .hardno/settings.json). Two effects:

  1. In-TUI toggle now persists \u2014 Alt+R writes the new state to disk atomically, so it survives session restart.

  2. External runtime toggle \u2014 pi-hard-no re-reads just the enabled field at each agent_end. An external writer (like roundhouse's new /toggle-review) can flip the value by writing the settings file; the running session picks up the change on the next agent turn without restart.

File routing

Write path matches read path (local > global). New resolveWritePath(cwd):

  • If cwd/.hardno/settings.json exists \u2192 write there (local wins).
  • Else \u2192 write ~/.pi/.hardno/settings.json.

Prevents the "toggle appears to revert" footgun where writing global would get masked by a local file with no enabled key on next session_start.

Safety

  • Atomic write \u2014 tmp + rename, orphan cleanup on failure
  • Race-safe \u2014 mtime check + retry (up to 3x) on concurrent writers
  • Corrupt backup \u2014 non-plain-object or unparseable bytes backed up to .corrupt-<ts>.bak before overwrite (user data never silently nuked)
  • ENOENT-only fallthrough in isEnabledFromDisk \u2014 malformed/permission errors on the more-specific file halt resolution ("local wins on silence")

Review history

3 review cycles + architect pass. Findings addressed:

  • Loop 1 (Codex): wrong write routing (global when local shadows), race conditions, malformed-data overwrite, inconsistent guards, pre-existing lint nit
  • Loop 2: clean
  • Architect: README missed the new feature \u2014 added settings table row + persistence/routing callout

Tests

  • 91 settings.test.ts (+28 from baseline)
  • 448 total green
  • New coverage: parser, isEnabledFromDisk precedence-on-silence, resolveWritePath, routing end-to-end (toggle + re-read), malformed-data backup, load-cycle integration
  • Lint + format + typecheck all clean (pre-existing no-useless-escape in dismiss.ts also fixed per Boy Scout rule)

Pairs with

roundhouse PR feat/toggle-code-review \u2014 adds /toggle-review Telegram slash command that uses this runtime mechanism.

Loki FastStart added 3 commits May 11, 2026 14:13
New settings key: `enabled: boolean` (default true). Two mechanisms:

1. **Persistence:** in-TUI toggle (Alt+R / /review) now writes the new
   state to ~/.pi/.hardno/settings.json atomically, so it survives
   session restart. Previously reset to true on each session_start.

2. **Runtime external toggle:** exposes isEnabledFromDisk() and
   writeEnabledToDisk(). The agent_end handler re-reads the `enabled`
   field from disk on each turn, so an external process (e.g.
   roundhouse's /toggle-review) can flip it by writing the file \u2014 the
   running session picks up the change without restart.

Precedence (unchanged): local .hardno/settings.json wins over global
~/.pi/.hardno/. When local exists but has no `enabled` key, does NOT
fall through to global \u2014 same-file-wins-on-silence semantics.

Atomic write: tmp + rename. No torn writes on crash.

Tests: 15 new (enabled parser + isEnabledFromDisk + writeEnabledToDisk).
435 total green. Typecheck + format clean. (Pre-existing lint error in
dismiss.ts untouched.)
F1 (Medium): writeEnabledToDisk now writes to the effective settings file
  (local if present, else global) via resolveWritePath(), matching the
  read path. Previously: in-TUI toggle wrote global but a local file
  without `enabled` masked it on next session_start \u2014 toggle appeared
  to revert. New option signature: writeEnabledToDisk(enabled, { cwd, home, targetPath }).
  Caller in index.ts now passes ctx.cwd.

F2 (Medium): race-retry in writeEnabledToDisk. Captures mtime at read,
  checks it before rename; on mismatch, re-reads and retries (up to 3
  attempts). Eliminates lost-update when roundhouse + pi-hard-no
  write concurrently to non-`enabled` fields.

F2-semantic (isEnabledFromDisk): only fall through to next dir on ENOENT.
  Malformed/permission/IO errors on the more-specific file now return
  null without falling through \u2014 preserves "local wins on silence"
  invariant even under corruption.

F3 (Low): back up original bytes to <settings>.corrupt-<ts>.bak before
  overwriting a file whose content is unparseable or non-plain-object.
  User data no longer silently nuked on malformed input.

F4 (Low): guard settings mutation consistently across both toggle paths.

F5 (Low): fix pre-existing lint error in dismiss.ts (`no-useless-escape`
  on \- in char class). Boy Scout rule.

Also: orphan tmp file cleanup on write failure; dir mkdirp inside the
retry loop so each attempt is self-contained.

Tests: 91 settings.test.ts / 448 total green. New tests:
- resolveWritePath correctness
- writeEnabledToDisk with cwd routing (local present / absent)
- end-to-end toggle writes local + read picks it up (F1 regression)
- malformed/array local does not fall through to global (F2 regression)
- corrupt-bytes backup written before overwrite (F3 regression)
- loadSettings integration: enabled persists across load cycles (F4)
Architect review flagged README missed the new user-facing feature.

- Added `enabled` row to settings.json example + settings table
- Added a callout explaining the persistence + routing + runtime re-read
  semantics so users understand why external tools (roundhouse) can
  toggle without restarting the session
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: 508d821a37

ℹ️ 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 index.ts
Comment on lines +733 to +736
if (diskEnabled !== null && diskEnabled !== orchestrator.isEnabled) {
log(`runtime toggle: disk says enabled=${diskEnabled}, updating orchestrator`);
orchestrator.setEnabled(diskEnabled);
// F4: guard like the toggleReview handler for consistency. `settings`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve manual toggle when disk persistence fails

When writeEnabledToDisk fails (for example due to read-only config files or permission errors), the toggle handler still updates orchestrator.isEnabled, but the next agent_end unconditionally re-syncs from disk and overwrites that in-memory state. In this failure mode, Alt+R//review appears to work briefly and then flips back on the next turn, which regresses the prior in-memory-only behavior exactly when persistence is unavailable.

Useful? React with 👍 / 👎.

Response to 2026-05-12 Mini Shai-Hulud wave (169 npm packages, 373
malicious versions across @TanStack, @UiPath, @squawk, @mistralai,
@TallyUI, @BeProduct, @draftlab, @draftauth, and others).

Current exposure: NONE. Lockfile pins @mistralai/mistralai@2.2.1 with
clean pre-attack integrity hash; compromised versions 2.2.2-2.2.4 were
published 2026-05-11 22:45-22:53 UTC and are never resolved because
npm ci respects the lockfile.

This .npmrc closes the policy gap: repo was missing the org-wide
min-release-age=7 quarantine that blocks any package published <7d ago
from installing. Protects against future rogue publishes inside our
existing semver ranges.
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