Skip to content

v0.13.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 13:18
· 455 commits to main since this release
4b45bb6

Added

  • scoutctl bootstrap resolve SKILL.md|DREAMING.md|RESEARCH.md (engine/scout/scripts/bootstrap.py, engine/scout/cli.py). Once a brain file is the version you want, resolve records the plugin assembly behind its sidecar as the merge base and removes the sidecar. The next upgrade then merges only later plugin changes into the resolution. Before this, a hand-resolved conflict kept diffing against the pre-conflict base and conflicted on the same lines again. An adopted proposal that a dreaming run edited before the next upgrade was proposed again on every release. resolve refuses while <<<<<<</>>>>>>> markers remain, and it takes the same session lock as upgrade.

  • A desktop notification when Scout's Claude login expires (templates/scripts/claude-with-retry.sh.tmpl). An expired CLI login fails every scheduled run within seconds, and until now the only trace was a banner in the session log, so vaults went quiet for weeks before anyone noticed. When the wrapper classifies a failure as auth, it now also raises a local notification ("Scout can't run. Your Claude login expired. Open Terminal and run: claude auth login") through osascript on macOS or notify-send on Linux; neither needs the rejected credential. Under cron, notify-send is pointed at the user's session bus (/run/user/<uid>/bus) when DBUS_SESSION_BUS_ADDRESS is unset. It fires at most once per SCOUT_AUTH_ALERT_INTERVAL_S (default 86400; a non-numeric value falls back to the default), tracked in .scout-logs/.auth-alert-sent; a successful run removes the marker so the next expiry alerts right away, a failed notification releases it so the next run tries again, and a marker that can't be written skips the alert rather than sending one every run. Best-effort: a missing or failing notifier never changes the run's exit code. (#267, #233)

  • The plugin ships scripts/git-safe-commit.sh (templates/scripts/git-safe-commit.sh.tmpl, engine/scout/scripts/bootstrap.py) — the commit helper the dreaming brain now calls existed in only one vault, so on every other vault those commit steps would have called a missing script. It is a cat-1 template: install and upgrade deliver it, and a vault's own earlier copy is parked under .scout-state/drift/ and reported as replaced. It stages and commits only the paths it is given (git commit -- <paths>, so a file another session left staged is not swept in), refuses -A/--all/./:/ (exit 2), serialises the stage→commit step behind a cross-session mutex under .scout-cache/ (flock, or an atomic mkdir that records its holder's pid where there is no flock, as on macOS; exit 3 on timeout), clears a git lock only when it is old and no git process is live, and exits 4 when none of its paths changed. A gitignored or missing path is skipped with a warning instead of aborting the commit.

  • scoutctl kb lint — pre-commit ratchet for vault structure: over-budget files may not grow, run-diary headings and mega-lines are blocked (report mode by default; kb_lint.mode: block to enforce), plus --report and a --lossless-* split check.

  • scoutctl kb install-hook and a bootstrap stage that installs the fail-open pre-commit hook.

  • KB WRITE PROTOCOL phase: source notes → topic notes → project pages. A new install creates knowledge-base/topics/, knowledge-base/sources/ and knowledge-base/session-log/, seeded with topics.md, sources.md and a session-log.md index (install-only; never overwritten).

    Upgrade note: enable kb_lint.mode: block only after sharding a legacy session-log.md. The legacy file has no size budget, so an un-migrated vault is not blocked on every run, but new rows belong in the monthly shards.

Changed

  • action-items materialize carries items only; run narration is dropped (verbatim fallback whenever any item's status or priority would change).
  • Session-log rows go to monthly shards.

Fixed

  • A pending brain-file sidecar no longer blocks the next upgrade (engine/scout/scripts/brain_merge.py, engine/scout/scripts/bootstrap.py). A SKILL.md/DREAMING.md/RESEARCH.md.proposed-merge left from an earlier upgrade used to make every later scoutctl bootstrap upgrade refuse before any stage ran. Scripts, runners, jobs and the version stamp all stayed on the old plugin, and an unattended auto-update stalled until someone resolved the sidecar by hand. Now only that file is skipped: its live file, its snapshot and the half-resolved sidecar are left exactly as they are, and every other stage runs. The skip is reported as UpgradeResult.skipped (and skipped in the --json result Scout.app decodes), as a skipped (sidecar pending): line from the CLI, as a doctor warning, and by /scout-update, which no longer refuses on it. scoutctl bootstrap auto no longer refuses on it either. A pending parser.py sidecar an older engine left still blocks.
  • A plugin change to a brain file reaches a vault that never edited it. When the live file still matched its snapshot, any change to the plugin's assembly went to a sidecar instead of being applied, so every release left a sidecar on a vault that had done nothing. That rule guarded the M3 incident, where migrate-legacy seeds the snapshot from the live file and a match proves nothing. The guard is now provenance. .scout-state/last-assembled/provenance.json records the hash of each snapshot the engine writes, and migrate-legacy marks its snapshots as seeded. The live file is replaced or merged only over a snapshot the plugin is known to have written. A vault from before this release falls back, once, to the header every bootstrap assembly starts with, but the header alone never licenses a fast-forward: a hand-grown brain can start with it too, so where live still matches such a snapshot (the M3 signature) the change is proposed, once, and adopting it converges. Anything else gets the plugin's version as a proposal in the sidecar. This also closes a second path to the M3 loss: a 3-way merge against a seeded snapshot could come out "clean" and delete the legacy brain. A proposal adopted verbatim (mv X.md.proposed-merge X.md) converges on the next upgrade; anything else converges through bootstrap resolve. When the header fallback vouches for a merge into live, the file from before the merge is parked as .scout-state/drift/X.md.vault (reported as a backup and a doctor note; scoutctl bootstrap drift --resolve X.md dismisses it). Provenance is written after each file, so an upgrade that fails part-way keeps the records it made. A provenance.json that can't be parsed fails closed: it is set aside as .corrupt rather than overwritten, and every brain file is proposed, never merged or replaced, until it is recorded again. A live brain file that still has <<<<<<</>>>>>>> lines (a conflict sidecar moved into place unfinished) is held: nothing is merged into it, its snapshot doesn't advance, and it is reported as conflict_markers (held (conflict markers): from the CLI) and as a doctor warning. The doctor suggests mv-ing a sidecar into place only when it has conflict markers; a marker-free proposal is the plugin's whole version, and moving it would replace the vault's brain. scoutctl phases backport is unchanged, because the snapshot still advances only once the live file has absorbed the assembly. Spec: docs/superpowers/specs/2026-10-02-brain-sidecars-never-block-upgrade-design.md.
  • The retry wrapper classifies each attempt on its own output (templates/scripts/claude-with-retry.sh.tmpl). It matched the last 50 lines of the whole run log, which also hold the pre-session hooks' output and every earlier attempt, and it checked the attempt count before auth. So with SCOUT_RETRY_DISABLE=1, or when a retry died on auth after a transient first attempt, the run logged "Retry exhausted" with no re-authentication banner, and bootstrap doctor's auth check stayed green; and hook output that quoted another service's 403 Forbidden could pass for a rejected Claude login. The wrapper now records where each attempt's output starts, classifies only that, and checks auth before the attempt count.
  • One definition of a rejected Claude login (templates/scripts/claude-with-retry.sh.tmpl, templates/scripts/run-outcome.sh.tmpl, engine/scout/scripts/bootstrap_doctor.py). The wrapper, run-outcome.sh and the doctor each had their own auth patterns, unanchored (and case-insensitive in run-outcome.sh), so text that merely quoted an auth error counted, and the doctor missed the CLI's Failed to authenticate: OAuth session expired string from #233. All three now use the CLI's own phrasing at the start of a line (bootstrap_doctor.CLI_AUTH_FAILURE_ERE, with a test keeping the shell copies equal), and the banner, the desktop alert, the Telegram hint and the doctor all name the same fix: claude auth login.
  • The dreaming brain no longer instructs its own commit steps to run bare git add -A (phases/modes/feedback-processing.md, phases/modes/kb-deep-work.md, phases/modes/wishlist.md) — all four dreaming commit steps (Phase-1 feedback, applied-proposal, Phase-2 KB deep work, Phase-3 wishlist) told the agent to stage the entire working tree. Scheduled lanes share one working tree and overlap by design, so -A stages a concurrently-running session's half-written files into this run's commit. That has now happened six times in the vault; on 2026-10-02 an evening-consolidation's -A swallowed a dreaming run's in-progress KB section mid-rename, committing a duplicate section heading under the consolidation's message — the first recurrence to produce a visible artifact defect rather than only a mangled provenance trail. Each step now calls the shipped scripts/git-safe-commit.sh with explicit paths; it refuses -A (exit 2), serialises the stage→commit critical section behind a cross-session mutex, and clears a git lock only when it is provably stale (age + no live git process). The applied-proposal step additionally now stages the same four paths the vault brain already named, which the engine fragment had diverged from. All three fragments are mode: [dreaming], so this changes DREAMING.md only — SKILL.md and RESEARCH.md are untouched and still carry the same defect.
  • PR state is fetched only for sessions that are not archived (engine/scout/sessions/index.py) — an archived session is done whatever its PR says, so its PRs no longer spend the 25-fetch budget; they keep their cached state or read unknown. Never-fetched refs are fetched first, so on a machine where 96 PRs on archived sessions had never been fetched, those took about four builds' worth of fetches ahead of the open PRs on live sessions. A PR linked to a live session and an archived one is still fetched. Spec: scout-app docs/superpowers/specs/2026-09-08-agent-sessions-design.md §10.7.