---
phase: 260521-oyi
plan: 01
subsystem: live-agent
tags: [live-start, pulse-period, default, sentinel-0, opt-in, changelog]
dependency_graph:
  requires: []
  provides:
    - "default $LIVE start = no scheduled pulse (sentinel 0)"
    - "opt-in pulse cadence via --period <seconds>"
  affects:
    - src/live/start.rs (run + live_start_result)
    - src/live/wrapper/mod.rs (poll_psyche argv shape)
    - src/live/wrapper/claude.rs (init banner + agents-JSON substitution)
tech_stack:
  added: []
  patterns: ["sentinel-value (0 = disabled) over Option<u64> threading"]
key_files:
  created:
    - .planning/quick/260521-oyi-update-live-start-to-have-default-period/260521-oyi-SUMMARY.md
  modified:
    - src/live/start.rs
    - src/live/wrapper/mod.rs
    - src/live/wrapper/claude.rs
    - tests/cli_parse.rs
    - plugin/spt/skills/live/SKILL.md
    - plugin/spt/skills/revive/SKILL.md
    - CHANGELOG.md
decisions:
  - "Sentinel 0 (not Option<u64> end-to-end) for 'no pulse' — minimal type churn, 0 was previously unreachable, repurposing is safe"
  - "build_agents_json replaces the FULL segment `{{period}} seconds` so psyche.md needs no text change (single atomic substitution per period mode)"
  - "CHANGELOG entry scoped per quick-260520-uc2: user-facing prose in `### Changed`; internal mechanics in `### BTS` sibling section"
  - "Skipped DEPLOY.ps1 / plugin.json version bump per orchestrator constraint — user runs deploy separately"
  - "Stale unit test live::wrapper::tests::poll_psyche_argv_contains_once_flag left in place: it asserts against a self-contained local literal of the OLD argv shape; still passes (its assertions are consistent within itself) and still guards the --once invariant. Updating its literal to match Vec<&str> would be scope creep beyond the Task 1 atomicity boundary"
metrics:
  duration: 12min
  completed: 2026-05-21
  tasks_completed: 2
  files_modified: 7
---

# Quick 260521-oyi: `$LIVE start --period` default → 0 (no pulse) Summary

`$LIVE start <id>` (and `$LIVE revive <id>`) now default to **no scheduled Psyche pulse** instead of a 1200-second (20-minute) cadence. Pulses are opt-in via `--period <seconds>`; the wrapper still wakes on real events (messages, alarms, communes, file-drops, INIT_SIGNOFF, echo-commune cadence). Internal representation: `period: u64` with `0` as the no-pulse sentinel; the wrapper omits `--pulse-interval` from the inner-poll argv entirely when `period == 0` (NEVER passes literal `0`, which would tight-loop `PULSE_TRIGGER`).

## Work Completed

### Task 1: Core behavior change (commit `be87fdb`)

- `src/live/start.rs:182` (`run`) and `src/live/start.rs:566` (`live_start_result`):
  - Default flipped: `period.unwrap_or(1200)` → `period.unwrap_or(0)`.
  - Guard relaxed: `if period < 60` → `if period > 0 && period < 60` (allows 0, still rejects 1..=59).
  - Error wording: `"Minimum pulse period is 60 seconds"` → `"Minimum pulse period is 60 seconds (or 0 to disable)"`.
- `src/live/wrapper/mod.rs:953-973` (`poll_psyche`):
  - Switched fixed `[&str; 7]` to `Vec<&str>` so the `--pulse-interval` flag can be conditionally omitted.
  - When `period == 0`, the argv reads `["poll", <psyche_id>, "listen", "--psyche", "--once"]` (no pulse flag).
  - When `period > 0`, the argv reads `["poll", <psyche_id>, "listen", "--psyche", "--pulse-interval", <period>, "--once"]` (identical to prior behavior).
  - `period_str` declared at outer scope before the `if period > 0` block so its lifetime covers later borrows in cfg(unix) `poll_cmd.args(&args)` and cfg(windows) `spawn_capture_no_inherit(&self.exe, &args, &envs)`.
- `src/live/wrapper/claude.rs:15-31` (`build_agents_json`):
  - Replaced `{{period}}` → `&period.to_string()` substitution with whole-segment `{{period}} seconds` → phrase substitution. When `period == 0`, the psyche.md identity-block line reads `Pulse period: disabled (no scheduled pulses; event-driven wake only)`. When `period > 0`, the line reads `Pulse period: <N> seconds` (verbatim prior behavior).
- `src/live/wrapper/claude.rs:37-44` (`init_session` banner):
  - Conditional pulse-phrase: `"no scheduled pulses"` when `period == 0`; `"Pulse period: <N>s"` otherwise.

`psyche.md` itself was NOT edited — substitution is whole-segment at template-build time. Verified at start of Task 1 that line 6 reads exactly `Pulse period: {{period}} seconds`.

### Task 2: Regression tests + docs + CHANGELOG (commit `5ec99c6`)

- `tests/cli_parse.rs`: two new regression guards immediately after `parse_live_start_period`:
  - `live_start_default_period_becomes_zero_sentinel`: structural guard `assert_eq!(None::<u64>.unwrap_or(0), 0)` mirroring `start::run:182`. Pure-clap parse path (`period: None`) is unchanged; the conversion happens in `unwrap_or(0)`.
  - `live_start_period_60_threads_through`: boundary value (the minimum allowed explicit period) parses to `Some(60)`. Locks that explicit `--period` still flows through clap unchanged.
- `plugin/spt/skills/live/SKILL.md` (after the `$LIVE start <id> [--period <seconds>]` example): callout explaining bare start = no pulses; `--period <seconds>` is opt-in (minimum 60); `--period 0` is an explicit opt-out.
- `plugin/spt/skills/revive/SKILL.md`: same callout for revive (same default).
- `CHANGELOG.md`: prepended `## [1.10.26] - 2026-05-21` with:
  - `### Changed`: user-facing prose only (UX-language rule from quick-260520-uc2). Explains the behavior change, lists the wake events that still fire, and gives the migration recipe (`--period 1200` to restore prior cadence).
  - `### BTS`: sentinel-0 mechanics, argv-omission rule, whole-segment substitution shape, regression-test pointers.

## Orchestrator Caveats — Verification Results

The orchestrator surfaced 4 soft spots. All four were inspected at start of Task 1; none required edits:

1. **`psyche.md` line 6 verbatim.** Confirmed exact text `Pulse period: {{period}} seconds` (with single space before "seconds"). The `.replace("{{period}} seconds", &period_phrase)` call in `build_agents_json` matches the whole segment; no adjustment needed.

2. **Echo-fire override path (`select_pulse_period`).** Confirmed at `wrapper/mod.rs:883-885`: `self.next_pulse_override.take().unwrap_or(self.period)`. When `self.period == 0` AND `next_pulse_override == Some(90)`, the function returns 90 (override wins; sentinel does NOT short-circuit). One-shot micro-pulses still fire correctly with the new default. No edit needed.

3. **`WrapperState::new` in `lifecycle.rs:17-44`.** Confirmed no independent `if period < 60` guard. The constructor accepts the period verbatim, storing it on the struct. Wrapper-handoff re-spawn with sentinel 0 works unmodified. No edit needed.

4. **`perform_wrapper_handoff` argv re-emit at `wrapper/mod.rs:1099-1107`.** Confirmed `let period_str = self.period.to_string();` — with sentinel 0 this emits literal `"0"` in argv. The receiving wrapper's `WrapperState::new` accepts this (it takes `period: u64` with no guard); `start::run` is the only `< 60` enforcer, and the wrapper subprocess does not call `start::run`. Path is sound. No edit needed.

All caveats resolved without changes; no additional commits required.

## Deviations from Plan

**Auto-fixed Issues:** None. The plan's pre-flight analysis was accurate; all four soft spots were correctly diagnosed as no-edit-needed.

**Scope decisions:**

- **CHANGELOG language scoped via quick-260520-uc2 split.** The plan's literal CHANGELOG draft mixed UX prose with internal mechanics (sentinel-0, argv omission, tight-loop hazard). Per the just-landed `### BTS` bucket discipline (quick-260520-uc2), I split the entry into `### Changed` (user-facing) and `### BTS` (behind-the-scenes). The user-facing surface for the version-change Stop hook now correctly omits the internals — matching the project's new CHANGELOG convention.

- **Plugin version bump NOT included.** Orchestrator constraint explicitly said `docs/DEPLOY.ps1` is out of scope and the user runs deploy separately. The CHANGELOG header `[1.10.26]` is the version-of-record stub; `plugin/spt/.claude-plugin/plugin.json` stays at 1.10.25 until the user runs `DEPLOY.ps1 -Bump patch`. (Project precedent from quick-260521-myj batched the bump as a separate `chore:` commit during the quick task — this task follows the stricter orchestrator constraint instead.)

## Test Results

```
$ cargo build --release
   Compiling owl v1.10.25 (...)
   Finished `release` profile [optimized] target(s) in 7.66s
   (3 pre-existing dead-code warnings unrelated to this change)
```

```
$ cargo test --test cli_parse -- --test-threads=1
test result: ok. 44 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

```
$ cargo test --test cli_parse -- live_start
running 4 tests
test live_start_default_period_becomes_zero_sentinel ... ok
test parse_live_start_period ... ok
test parse_live_start ... ok
test live_start_period_60_threads_through ... ok

test result: ok. 4 passed; 0 failed
```

## Deferred Issues

- **`cargo test --lib` parallel flakes** (~31 failures in modules `common::tracked`, `live::context`, `live::fork`, `owl::doctor`, `live::wrapper::claude::tests::emit_inwrapper_boot_*`, etc.) are **pre-existing parallel-test race conditions** unrelated to this change — temp-dir / `SPT_HOME` / git-template-clone races. Single-threaded run (`--test-threads=1`) collapses to one git-timeout flake (`common::tracked::tests::seal_and_rotate_creates_seal_commit_and_truncates: GitFailed("git timed out")`), which is environmental. STATE.md decisions log already records "process-global env race" and "module-local ENV_LOCK don't coordinate across modules" as a known regression class. Out of scope for this quick task.
- **`live::wrapper::tests::poll_psyche_argv_contains_once_flag`** (mod.rs:1447) uses a self-contained local literal mirroring the OLD `[&str; 7]` shape with period=1200. It still passes (asserts what its own literal contains) and still guards the `--once` invariant. Updating its literal to match the new `Vec<&str>` shape would be scope creep beyond Task 1's atomicity boundary; flagging here for future cleanup.

## Files Touched

| File                                                  | Task | Change                                                                                         |
| ----------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------- |
| `src/live/start.rs`                                   | 1    | `unwrap_or(0)` + relaxed guard `period > 0 && period < 60` at both `run` and `live_start_result` sites |
| `src/live/wrapper/mod.rs`                             | 1    | `poll_psyche` argv: `[&str; 7]` → `Vec<&str>`; conditional `--pulse-interval` push             |
| `src/live/wrapper/claude.rs`                          | 1    | `build_agents_json` whole-segment substitution; `init_session` conditional pulse-phrase banner |
| `tests/cli_parse.rs`                                  | 2    | +2 regression guards (`live_start_default_period_becomes_zero_sentinel`, `live_start_period_60_threads_through`) |
| `plugin/spt/skills/live/SKILL.md`                     | 2    | Opt-in callout after the start example                                                         |
| `plugin/spt/skills/revive/SKILL.md`                   | 2    | Opt-in callout after the revive example                                                        |
| `CHANGELOG.md`                                        | 2    | `[1.10.26] - 2026-05-21` entry (Changed + BTS sections)                                        |

## Deploy Guidance

Run `powershell -ExecutionPolicy Bypass -File docs/DEPLOY.ps1 -Bump patch` to:
1. Bump `plugin/spt/.claude-plugin/plugin.json` from 1.10.25 → 1.10.26.
2. Bump `Cargo.toml` version (and sync `Cargo.lock`).
3. Rebuild `owl.exe` with the new psyche.md embedded.
4. Sync to marketplace clone and re-install plugin.

The CHANGELOG entry is already in place under the `[1.10.26]` header — DEPLOY.ps1's `-Bump patch` matches that header.

After deploy, smoke-test (NOT in this plan; user-driven):
- `$LIVE start <fresh-id>` (no `--period`) — starts a wrapper that never pulses; messaging it still wakes Psyche normally; init banner reads `no scheduled pulses`.
- `$LIVE start <id> --period 60` — pulses every 60s as before.
- `$LIVE start <id> --period 30` — rejected with `Minimum pulse period is 60 seconds (or 0 to disable)`.
- `$LIVE start <id> --period 0` — accepted; behaves like bare start.

## Self-Check: PASSED

- `.planning/quick/260521-oyi-update-live-start-to-have-default-period/260521-oyi-SUMMARY.md` — created (this file).
- Commit `be87fdb` (Task 1) — `git log --oneline -3` confirms.
- Commit `5ec99c6` (Task 2) — `git log --oneline -3` confirms.
- All 7 modified files visible in `git show --stat be87fdb` + `git show --stat 5ec99c6`.
- `cargo build --release` — green.
- `cargo test --test cli_parse` — 44/44 passed including 2 new regression guards.
