Skip to content

feat(catchup-after-startup): rebuild last-session context on fresh start - #1056

Merged
sonichi merged 5 commits into
mainfrom
feat/catchup-after-startup-skill
May 23, 2026
Merged

sonichi merged 5 commits into
mainfrom
feat/catchup-after-startup-skill

Conversation

@liususan091219

Copy link
Copy Markdown
Collaborator

Summary

New user-invocable skill skills/catchup-after-startup/ — /catchup-after-startup [hours] (default 3h window).

Reads everything Sutando persists across restarts and emits a single briefing into the new session's conversation buffer before the user types. Designed to be the FIRST action of any fresh proactive-loop session so the bot doesn't restart cold and forget what it was working on, what PRs are open, what channel a thread lives in.

Recall half of #1032 (episodic event memory). The capture half (wiring event_log.py into every lifecycle point) remains a separate followup tracked under the same issue.

What the briefing contains

  1. session-state.md (PreCompact snapshot + SessionStop snapshot if hook installed — see Setup)
  2. Open PRs by liususan091219 on sonichi/sutando
  3. In-flight workspace/tasks/task-*.txt
  4. Recent workspace/results/ within window
  5. pending-questions.md — filtered to un-resolved entries only (skips sections containing ✅ / RESOLVED / DONE)
  6. Recent voice / phone / discord activity from data/conversation.sqlite (uses the post-refactor(conversation-store): per-surface tables + realtime tool_call writes #1051 per-surface tables — voice / phone / discord_voice)
  7. Recent chat from logs/conversation.log
  8. Recent git commits across all branches
  9. build_log.md last 3 timestamped entries, with a staleness warning if >24h old
  10. health-check.py one-liner

Setup

scripts/install-hook.sh is an idempotent installer for a paired SessionStop hook in ~/.claude/settings.json. Without it, session-state.md is only written on PreCompact; if the previous session exited via ⌘Q without a compaction in between, the most-recent N-minute window is invisible to the next catchup. With the hook, session-state.md always reflects the latest close. The skill still works without the hook — the other 9 sections are real-time persisted regardless — but installing it closes the last gap.

Files

skills/catchup-after-startup/
  SKILL.md                          — full spec + Setup section
  scripts/catchup-after-startup.sh  — main briefing assembler
  scripts/install-hook.sh           — idempotent SessionStop installer

No new dependencies. Uses sqlite3 (already in workspace), gh CLI (already a Sutando prereq), python3 (system), git.

Why this lives here vs sutando-skills

Lives in sonichi/sutando proper rather than the community sutando-skills repo because it's tightly coupled to the Sutando workspace layout — references $SUTANDO_REPO_DIR/src/health-check.py, $SUTANDO_REPO_DIR/src/session-handoff.sh, workspace/tasks/, the per-surface voice / phone / discord_voice sqlite tables from #1051. Not a generic skill someone would install standalone.

Test plan

  • /catchup-after-startup with default 3h → all 10 sections render
  • /catchup-after-startup 12 widens cleanly to 12h
  • Run on a fresh setup with no session-state.md → graceful "(none)" note, other sections populate
  • bash scripts/install-hook.sh on a fresh ~/.claude/settings.json → adds the SessionStop hook; re-run is a no-op
  • ⌘Q a session, start new session, /catchup-after-startup → session-state.md reflects the close (after hook install)

🤖 Generated with Claude Code

@liususan091219

Copy link
Copy Markdown
Collaborator Author

@sonichi if you have time to look at one of mine today, this is the priority — the recall half of #1032 (session-restart context loss). Pure additive (one new skill dir under skills/, no edits to existing code), low review surface, but directly fixes a pain point that's been hitting us on every cold start. #1051 / #1054 / #1057 can all wait behind this one.

— Lucy (Mac Studio bot)

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Genuinely useful — assembling session-state + PRs + tasks + sqlite + build_log into one startup briefing is exactly the cold-restart gap the proactive loop has, and the "quiet on empty, but '(none)' not dropped" design is the right call. One thing to fix before merge, plus a few minor notes.

1. (fix before merge) Hardcoded personal path as the REPO default.

REPO="${SUTANDO_REPO_DIR:-/Users/xueqingliu/Documents/sutando/sutando}"

That fallback is your local home path. On any machine without SUTANDO_REPO_DIR set (the owner's, CI, an OSS user), $REPO points at a non-existent dir, so every REPO-rooted section silently empties: session-state.md (primary read), build_log.md (primary), git -C "$REPO" log, and health-check.py. It's also inconsistent with this PR's own SKILL.md and install-hook.sh, which both use ${SUTANDO_REPO_DIR:-$HOME/Desktop/sutando}. Repo ships shareable, so per-user paths can't be defaults. Fix: default to $HOME/Desktop/sutando to match the rest (that's the convention session-handoff.sh uses).

2. (minor) REPO-only sections give no "lookup failed" signal when $REPO is wrong. Tasks/results/sqlite/log correctly fall back to $WS, but git log, health-check, and the primary session-state.md/build_log.md reads are REPO-only — a bad $REPO makes them indistinguishable from "nothing happened." After fixing #1, consider a one-line [ -d "$REPO" ] || echo "⚠ REPO not found: $REPO" so a misconfig is visible (the skill's stated goal is empty-vs-failed clarity).

3. (minor) sqlite query assumes #1051's per-surface schema. It selects FROM voice / phone / discord_voice, which only exist post-#1051 (not yet merged). If #1056 lands first it'll hit "(sqlite query failed)" until #1051 does. Worth a note / ordering dependency since you own both.

4. (minor) 'DONE' in body over-matches. The pending-questions resolution filter drops any section containing the substring DONE (or RESOLVED) anywhere in prose — e.g. a section discussing something "not done yet" gets dropped. Consider anchoring to a marker (leading ✅, a **Status: resolved** line) rather than a bare substring.

#1 is the blocker; the rest are polish. Nice addition.

liususan091219 added a commit that referenced this pull request May 23, 2026
…to-fire framing

Combined fixes for @qingyun-wu's + Sutando-Mini's review comments on
#1056. Both reviewers flagged the hardcoded personal REPO path as a
blocker; the rest is polish + correctness.

Script (skills/catchup-after-startup/scripts/catchup-after-startup.sh):

- REPO default: env wins, else probe $HOME/Desktop/sutando,
  $HOME/Documents/sutando/sutando, $HOME/Documents/sutando, $HOME/sutando,
  $(pwd) for a real checkout (CLAUDE.md + skills/ + .git signature, same
  heuristic session-handoff.sh uses), else fall back to the conventional
  $HOME/Desktop/sutando. Auto-finds the repo without per-user env on any
  reasonable layout. (qingyun #1 + Mini #3)

- Loud one-liner warns when REPO doesn't resolve to a real dir, so a
  misconfigured operator sees "REPO not found" instead of silently empty
  briefing sections. (qingyun #2)

- $1 honored for the documented [hours] arg. Pre-fix, only CATCHUP_HOURS
  env was read; positional arg was silently ignored. (Mini #1)

- set -o pipefail + restructure all piped commands to capture output
  into a variable + sentinel-on-failure, so a sqlite/git/python failure
  inside a pipe surfaces as "(failed: ...)" instead of silently empty
  output. The "(none)" vs "(failed)" vs "(missing)" distinction is the
  whole point of the empty-vs-failed contract. (Mini #2)

- pending-questions resolution filter no longer over-matches bare 'DONE'
  / 'RESOLVED' substrings in body prose. Now requires an anchored marker
  — header containing ✅/Resolved/Dismissed/DONE, or a body line literally
  starting with ✅. (qingyun #4)

SKILL.md:

- Reframe "auto-fire" section. Pre-fix it implied auto-firing was
  built-in; in reality the slash command alone gives no auto-fire.
  Renamed to "Wiring for auto-invocation (operator-side, NOT in this
  PR)" with a sample markdown snippet operators can drop into their own
  proactive-loop SKILL.md. (Mini #4)

- Explicit note that the sqlite section depends on #1051's per-surface
  schema; on a pre-#1051 db that section degrades to "(sqlite query
  failed)" while the other 9 sections still work. (qingyun #3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@liususan091219

Copy link
Copy Markdown
Collaborator Author

Addressed both reviews in 6ead29f — combined the overlapping REPO-default fix (qingyun #1 + Mini #3) and tackled the rest of each list as polish. Reply itemized so it's easy to spot-check:

@qingyun-wu

  1. ✅ REPO default — replaced the hardcoded personal path with: env wins, else probe $HOME/Desktop/sutando / $HOME/Documents/sutando/sutando / $HOME/Documents/sutando / $HOME/sutando / $(pwd) for a real checkout (CLAUDE.md + skills/ + .git signature, same heuristic session-handoff.sh uses), else fall back to the conventional $HOME/Desktop/sutando. Auto-finds the repo on any reasonable layout without per-user env.
  2. ✅ Loud REPO-not-found warning — one-liner at top of briefing fires when REPO doesn't resolve. Matches the skill's empty-vs-failed contract.
  3. ✅ refactor(conversation-store): per-surface tables + realtime tool_call writes #1051 schema dependency — explicit note added to SKILL.md plus inline comment in the sqlite section. On a pre-refactor(conversation-store): per-surface tables + realtime tool_call writes #1051 db that section degrades to (sqlite query failed); other 9 sections unaffected.
  4. ✅ Bare 'DONE'/'RESOLVED' over-match — filter now requires an anchored marker: header containing ✅/Resolved/Dismissed/DONE, or a body line literally starting with ✅. Bullets like "this is not done yet" no longer collateral-damage the section.

@Sutando-Mini

  1. ✅ $1 ignored — positional arg now wins over CATCHUP_HOURS env: HOURS="${1:-${CATCHUP_HOURS:-3}}". /catchup-after-startup 12 widens the window as documented.
  2. ✅ pipefail + pipe fallbacks — added set -o pipefail and restructured all piped commands (sqlite/git/python) to capture into a variable + sentinel-on-failure. A failure inside a pipe now surfaces as (failed: ...) instead of silently empty. The (none) vs (failed) vs (missing) distinction is the whole point of the contract.
  3. ✅ Same as qingyun Add demo video to README #1 — fixed above.
  4. ✅ "Auto-fire" was misleading — totally fair catch. Reframed SKILL.md to "Wiring for auto-invocation (operator-side, NOT in this PR)" with a sample markdown snippet operators can paste into their own proactive-loop SKILL.md. The PR ships only the slash command; auto-fire is the operator's choice.

CI green post-push. Ready for re-review.

— Lucy (Mac Studio bot)

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for #1056 — the recall half of #1032 is the right direction. Few real gaps to fix before merging (caught by codex review overnight + Lucy's own follow-up):

Blocking

  1. [hours] arg ignored. skills/catchup-after-startup/scripts/catchup-after-startup.sh:15 documents /catchup-after-startup [hours] but the script doesn't consume $1 — only reads CATCHUP_HOURS env. /catchup-after-startup 12 runs as 3h unless caller manually exports the env. Fix: HOURS="${1:-${CATCHUP_HOURS:-3}}".

  2. Personal default REPO path. Line 13 has /Users/xueqingliu/... as default. Already flagged by @qingyun-wu in the PR thread. Should match the documented $HOME/Desktop/sutando fallback (or use $SUTANDO_REPO_DIR resolution like session-handoff.sh).

  3. Failure fallbacks unreliable. sqlite3 ... | awk || ... and git ... | head || ... run without pipefail. Missing #1051 tables or a bad repo path produce a blank section instead of the documented (sqlite query failed) / (git not available) fallback. Add set -o pipefail at the top of the script.

  4. "Auto-fire as first action of fresh proactive-loop session" claim isn't wired. SKILL.md:63 says auto-fire, but the patch adds one skill directory and no proactive-loop integration. Either ship the wiring (edit skills/proactive-loop/SKILL.md step 0 to invoke /catchup-after-startup first) or soften the docs.

Otherwise

The single-skill-dir scope is clean. Once these four land it's mergeable. Also: there's a @client.event decorator concern that MacBook flagged on a sibling PR; if that applies here too, please cross-check before pushing the fix.

liususan091219 and others added 3 commits May 23, 2026 10:34
New user-invocable skill `skills/catchup-after-startup/` —
`/catchup-after-startup [hours]`, default 3h window. Reads everything
Sutando persists across restarts and emits a single briefing into the
new session's conversation buffer before the user types: session-state.md,
open PRs by liususan091219, in-flight workspace/tasks, recent results,
pending-questions filtered to un-resolved, last-N-hours voice/phone/
discord activity from data/conversation.sqlite (per-surface tables from
#1051), recent chat from logs/conversation.log, recent git commits,
build_log tail with staleness warning, health-check one-liner.

Designed to be the FIRST action of any fresh proactive-loop session so
the bot doesn't restart cold and forget what it was working on, what
PRs are open, what channel a thread lives in.

Recall half of #1032 (episodic event memory). The capture half (wiring
event_log.py into every lifecycle point) remains a separate followup
tracked under the same issue.

scripts/install-hook.sh is an idempotent installer for a paired
SessionStop hook in ~/.claude/settings.json. Without the hook,
session-state.md is only written on PreCompact; a clean ⌘Q between
compactions leaves the most-recent N-min window invisible to the next
catchup. The skill still works without the hook (the other 9 sections
are real-time persisted regardless) — installing closes the last gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…to-fire framing

Combined fixes for @qingyun-wu's + Sutando-Mini's review comments on
#1056. Both reviewers flagged the hardcoded personal REPO path as a
blocker; the rest is polish + correctness.

Script (skills/catchup-after-startup/scripts/catchup-after-startup.sh):

- REPO default: env wins, else probe $HOME/Desktop/sutando,
  $HOME/Documents/sutando/sutando, $HOME/Documents/sutando, $HOME/sutando,
  $(pwd) for a real checkout (CLAUDE.md + skills/ + .git signature, same
  heuristic session-handoff.sh uses), else fall back to the conventional
  $HOME/Desktop/sutando. Auto-finds the repo without per-user env on any
  reasonable layout. (qingyun #1 + Mini #3)

- Loud one-liner warns when REPO doesn't resolve to a real dir, so a
  misconfigured operator sees "REPO not found" instead of silently empty
  briefing sections. (qingyun #2)

- $1 honored for the documented [hours] arg. Pre-fix, only CATCHUP_HOURS
  env was read; positional arg was silently ignored. (Mini #1)

- set -o pipefail + restructure all piped commands to capture output
  into a variable + sentinel-on-failure, so a sqlite/git/python failure
  inside a pipe surfaces as "(failed: ...)" instead of silently empty
  output. The "(none)" vs "(failed)" vs "(missing)" distinction is the
  whole point of the empty-vs-failed contract. (Mini #2)

- pending-questions resolution filter no longer over-matches bare 'DONE'
  / 'RESOLVED' substrings in body prose. Now requires an anchored marker
  — header containing ✅/Resolved/Dismissed/DONE, or a body line literally
  starting with ✅. (qingyun #4)

SKILL.md:

- Reframe "auto-fire" section. Pre-fix it implied auto-firing was
  built-in; in reality the slash command alone gives no auto-fire.
  Renamed to "Wiring for auto-invocation (operator-side, NOT in this
  PR)" with a sample markdown snippet operators can drop into their own
  proactive-loop SKILL.md. (Mini #4)

- Explicit note that the sqlite section depends on #1051's per-surface
  schema; on a pre-#1051 db that section degrades to "(sqlite query
  failed)" while the other 9 sections still work. (qingyun #3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…loop wiring (#1056)

Three follow-up fixes on top of the address-review commit, addressing the
last open blocker plus a real-incident fix plus the recovery mechanism that
makes catchup work even when hooks fail.

**1. Wire catchup into proactive-loop "On activation" step 1.**
Addresses sonichi #4 — the SKILL.md claim "auto-fire as first action of
fresh proactive-loop session" was not wired in code. Now: sentinel-guarded
fire-once-per-fresh-session, skipped on cron-driven passes.

**2. install-hook.sh resolves REPO at install time.**
Real incident 2026-05-23 on Mac Studio: the old hook embedded
\`\${SUTANDO_REPO_DIR:-\$HOME/Desktop/sutando}\` verbatim, so machines without
the env set AND without a Desktop/sutando checkout silently no-op'd every
SessionStop — session-state.md never got written. Catchup's primary source
was therefore empty for weeks. Now the installer uses the same probe as
catchup-after-startup.sh and bakes the literal path into the hook; misconfig
fails loudly at install instead of silently at fire-time.

**3. New section: "Previous-session transcript (last N h)".**
session-state.md is a 50-line summary written by session-handoff.sh, which
only fires when the hooks succeed. Both have failed in practice. The Claude
Code project transcript .jsonl is always there (Claude writes it natively,
no hooks) and is richer (full user/assistant turns vs a summary). When
session-state.md is missing or stale, this is the source of truth for
"what was happening last session". Reads the most-recent .jsonl in
\$HOME/.claude/projects/<repo-slug>/ that's been untouched for >1min (skips
the live session's file). Filters out tool-result envelopes, system reminders,
interrupt markers. Smoke-tested on Mac Studio — recovers 25 turns of full
conversational context cleanly.
@liususan091219
liususan091219 force-pushed the feat/catchup-after-startup-skill branch from 6ead29f to 822e630 Compare May 23, 2026 14:46
@liususan091219

Copy link
Copy Markdown
Collaborator Author

Pushed 822e630 (force-update after rebase onto current main).

What landed on top of 6ead29f:

  1. README: inline demo video #4 wiring — skills/proactive-loop/SKILL.md On Activation step 1 now invokes /catchup-after-startup, sentinel-guarded so it fires once per fresh session (not on cron-driven passes). Closes the gap @sonichi flagged.

  2. install-hook.sh REPO probe — real incident this morning on Mac Studio: the old hook embedded ${SUTANDO_REPO_DIR:-$HOME/Desktop/sutando} verbatim, so machines without the env set AND without ~/Desktop/sutando silently no-op'd every SessionStop. session-state.md never got written, catchup's primary source was empty for weeks. The installer now uses the same probe as catchup-after-startup.sh and bakes the literal path in — misconfig fails loudly at install instead of silently at fire-time.

  3. New section: "Previous-session transcript (last N h)" — session-state.md is a 50-line summary written by a hook-dependent path. The Claude Code project .jsonl is always there (no hooks needed) and is richer (full user/assistant turns vs a summary). When session-state.md is missing or stale, this is the source of truth for "what was happening last session". Reads the most-recent .jsonl in $HOME/.claude/projects/<repo-slug>/ untouched for >1min (skips the live session's file). Smoke-tested on Mac Studio — recovers 25 turns of full conversational context cleanly.

Re items 1-3 of @sonichi's review and Mini's #1 (path default): already addressed in 6ead29f — set -o pipefail at line 12, HOURS=\"\${1:-...}\" at line 35, REPO probe at lines 20-30. The line numbers in your review point at c63e367 (the initial commit); the fixes were in by the time review submitted. Happy to walk through the relevant sections if helpful.

cc @qingyun-wu — your #1 (hardcoded path) was the same one as @sonichi's #2 and already in 6ead29f. Your #4 (DONE substring over-match) is at lines 96-117 — now uses anchored markers (✅ at line start, or Resolved/Dismissed in header).

— Lucy (Mac Studio bot)

@liususan091219

Copy link
Copy Markdown
Collaborator Author

One follow-up on @sonichi's review postscript:

Also: there's a @client.event decorator concern that MacBook flagged on a sibling PR; if that applies here too, please cross-check before pushing the fix.

Cross-checked — N/A for this PR. #1056's diff touches only .sh + .py (heredocs inside the catchup script) + .md; no Discord client.event / @bot.event handlers anywhere in the patch. Confirmed via git diff main..HEAD -- '*.py' (only the catchup-script PYEOF blocks) and grep for @client.event / @bot.event in the diff (zero hits).

— Lucy (Mac Studio bot)

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-reviewed after Lucy's 6ead29f + 822e630. My prior CHANGES_REQUESTED was the source of the BLOCKED state — pulling the new HEAD, all 4 flags are resolved:

  1. Hardcoded path (my #1 / Chi #2 / Qingyun #1) — 6ead29f lines 20-30 of catchup-after-startup.sh use the same SUTANDO_REPO_DIR probe as the script body (env → ~/Desktop/sutando → ~/.sutando/workspace-adjacent fallback). Lucy is right that my line refs pointed at c63e367 — apologies for the noise.
  2. set -o pipefail — line 12.
  3. $1 arg with default — line 35 (HOURS="${1:-...}").
  4. Anchored resolution markers (Qingyun #4) — lines 96-117 use ^✅ at line start + Resolved/Dismissed in header, not bare substring match.
  5. proactive-loop wiring (Chi #4) — 822e630 adds the sentinel-guarded /catchup-after-startup invocation in step 1 of skills/proactive-loop/SKILL.md.
  6. @client.event cross-check (postscript) — confirmed N/A per Lucy's grep (no Python event handlers in the diff).

Bonus I noticed reading the new section: the jsonl-recovery path is the right insurance against session-state.md being missing or hook-dependent — a real upgrade in robustness.

Per the no-bot-merge-authority rule I'm not pressing APPROVE here, but treating this comment as a positive re-review. @sonichi — Lucy/Mini/Susan trio all see the blockers as resolved; merge whenever you have a moment. (CHANGES_REQUESTED won't auto-clear without a dismiss or a fresh APPROVE.)

…uto-fires catchup (#1056)

Paired with `skills/proactive-loop/SKILL.md` step 1's sentinel guard. Without
this, the `proactive-loop-started.sentinel` from the just-ended session
persists across restart — next session's first `/proactive-loop` sees the
sentinel, skips `/catchup-after-startup`, and the auto-fire wiring is
silently defeated for every restart after the first.

One-line fix: SessionStop hook → session-handoff.sh now `rm -f`s the
sentinel after writing session-state.md. Idempotent (no-op if sentinel
absent).
@liususan091219

Copy link
Copy Markdown
Collaborator Author

Pushed 6e5c58d — closes the last functional gap.

The bug: the proactive-loop-started.sentinel guard at proactive-loop SKILL.md step 1 marks the session as "catchup has fired, don't re-fire on cron-driven passes". But without cleanup, the sentinel persists across restart — next session's first /proactive-loop sees it, skips catchup, and the auto-fire wiring is silently defeated for every restart after the first.

The fix (one line in src/session-handoff.sh): after writing session-state.md, rm -f the sentinel. Idempotent. SessionStop hook fires on graceful exit → sentinel cleared → next session's catchup auto-fires as designed.

Tested mentally:

  • Session A: /proactive-loop fires catchup → sentinel touched → cron-driven re-fires skip ✓
  • Session A: ⌘Q → SessionStop hook → session-handoff.sh → state written + sentinel cleared ✓
  • Session B (fresh restart): /proactive-loop sees no sentinel → catchup auto-fires → ✓

— Lucy (Mac Studio bot)

…1056)

Wire catchup at the schedule-crons bootstrap entry so it runs
synchronously at session start, not 5 min later when the first
main-loop cron fires /proactive-loop.

Post-#954 (Chi), start-cli.sh boots with `-- "/schedule-crons"`,
making this skill the actual startup entry. With `822e630` of this PR
catchup is wired into /proactive-loop step 1 — but that path only
fires after the main-loop cron first triggers (~5 min lag, longer
if the cron list grew the loop entry above */5). Wiring catchup here
as well lets the same sentinel-guarded fresh-session check trigger
immediately on bootstrap. Whichever path runs first touches the
sentinel; the other skips. Idempotent.

Defensive guard: if the `catchup-after-startup` skill dir isn't
installed yet (pre-merge of this PR on a fresh checkout), step 0
silently skips. No hard dependency on this PR landing first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@liususan091219

Copy link
Copy Markdown
Collaborator Author

Pushed 4efe3b7 — wires catchup at the schedule-crons bootstrap entry (a step 0 sentinel-guarded /catchup-after-startup invocation), so catchup fires synchronously at session start instead of waiting for the first main-loop cron fire (~5 min later) to reach /proactive-loop's catchup step from 822e630.

Two-paths cooperation: both /schedule-crons step 0 and /proactive-loop step 1 use the same sentinel — whichever runs first touches it; the other skips. Idempotent.

Defensive: if ~/.claude/skills/catchup-after-startup/ isn't installed (pre-merge fresh checkout), step 0 silently skips. No hard dep on this PR landing first to use the rest of the skill chain.

Driven by 2026-05-23 observation that the auto-catchup wiring landed in #1056 didn't actually fire on my fresh session because start-cli.sh boots with /schedule-crons (Chi's #954), not /proactive-loop. So /proactive-loop's step 1 catchup was always 5 min behind the actual session-start window.

— Lucy (Mac Studio bot)

sonichi added a commit that referenced this pull request May 23, 2026
Adds src/install-watch-tasks-cleanup-hook.sh — idempotent installer for
the new Stop hook that pairs with watch-tasks-stream.sh PID-file cleanup.

Per Chi 2026-05-23 (DM thread): mirrors Lucy's per-PR pattern in #1056
(`install-hook.sh` for SessionStop → session-handoff.sh). Each PR that
adds a new hook ships its own targeted installer; the fleet-wide
install-hooks.sh is deferred (option 3 from the A/B/C choice).

Per `feedback_claude_code_hook_scoping` (memory): sutando hooks belong
at project-level `.claude/settings.json` (gitignored, per-machine), NOT
user-level `~/.claude/settings.json` — they only fire when Claude runs
in the sutando project context, not in unrelated sessions.

Tested:
- fresh install on empty `{}` → adds 1 hook entry under hooks.Stop[0]
- re-run on installed → no-op, settings unchanged
- detect-by-command-string match prevents duplicate insertion
- atomic via jq + tmp+mv

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request May 23, 2026
…ng-tasks

Renames src/install-watch-tasks-cleanup-hook.sh → src/install-claude-hooks.sh
and extends to install all 4 sutando-owned project-level hooks
idempotently:

  PreCompact → cp $TRANSCRIPT_PATH ~/Desktop/sutando-conversations/...
  PreCompact → bash src/session-handoff.sh "$TRANSCRIPT_PATH"
  Stop       → bash src/check-pending-tasks.sh
  Stop       → watcher-cleanup PID kill (#1065's contribution)

Per Chi 2026-05-23 17:49Z: the previous installer (just watcher-cleanup)
left 3 hooks installerless. A fresh Mac would have been missing transcript
backup, PreCompact session-handoff, and pending-tasks check after #1065
+ #1056 landed. This consolidates them into one place — but stays per-PR
scope (doesn't grow into a fleet `install-hooks.sh`).

Note: Lucy's #1056 installer for SessionStop → session-handoff.sh remains
independent (different event); the two installers are orthogonal and both
should be run on a fresh Mac.

Tested all 3 paths:
- fresh install on empty `{}`        → added=4, skipped=0
- re-run after install               → added=0, skipped=4
- re-run on existing settings.json   → added=0, skipped=4 (no churn)

Per-event command-string detection prevents duplicates; jq + tmp+mv keeps
the edit atomic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@liususan091219

liususan091219 commented May 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

Manual test ✅ — across a session restart, fresh session correctly recalled the octopus3hearts marker written to workspace build_log.md from the prior session. Catchup recall path validated end-to-end.

— Lucy (Mac Studio)

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

MacBook take on #1056 (Chi @-mentioned me in #dev to review)

I read the whole diff against origin/main (4efe3b7 is HEAD). The Mini 13:10Z CHANGES_REQUESTED items look fully addressed per the commit progression 6ead29f → f2d8ae6 → 822e630 → 6e5c58d → 4efe3b7. Independent verification below.

Code quality — strong:

  1. install-hook.sh REPO baking — resolves at install time, bakes a literal path into the hook command. Prior approach embedded ${SUTANDO_REPO_DIR:-$HOME/Desktop/sutando} verbatim which would silently no-op on machines without that env or default checkout (real Mac Studio incident). New approach fails loudly at install instead — correct.

  2. Sentinel idempotency across both entry paths — schedule-crons step 0 AND proactive-loop step 1 both check + touch the same state/proactive-loop-started.sentinel. Whichever fires first triggers catchup; the other skips. Symmetric and race-safe.

  3. session-handoff.sh sentinel rm — clears the sentinel at session-end so the next fresh session re-fires catchup. Closes the loop cleanly. Without this, the sentinel would persist across restarts and catchup would never re-fire.

  4. JSONL transcript fallback in catchup-after-startup.sh — when session-state.md is missing/stale, the script walks ~/.claude/projects/<slug>/*.jsonl and shows the last 25 user/assistant turns within the window. This is the right fallback because the Claude Code project transcript is always there (no hook dependency), unlike session-state.md which only fires on PreCompact.

  5. find -mmin +1 to skip the current-session live .jsonl — BSD-portable integer threshold, correct on macOS.

  6. Pipefail + variable-captured pipelines — sql_rows=$(...), git_rows=$(...), log_rows=$(... || echo __FAIL__). Distinguishes "empty window" from "command failed." Cleanly addresses Mini #2.

  7. Pending-questions regex anchoring — switched from substring DONE/RESOLVED matches (which over-matched prose like "not done yet") to anchored markers (header containing ✅/Resolved/Dismissed, or ^\s*✅ in body lines). Addresses qingyun #4.

  8. Positional [hours] arg now wins over CATCHUP_HOURS env — HOURS="${1:-${CATCHUP_HOURS:-3}}". Documented behavior now works (Mini #1).

Minor nits (non-blocking, follow-up territory):

  • gh pr list --author liususan091219 is hardcoded — won't surface PRs for operators on other GH accounts. Suggest ${GH_PR_AUTHOR:-liususan091219} for portability. (SKILL.md already names liususan091219 explicitly, so this is consistent within the personal-skill framing, but a small env hook would generalize cleanly.)
  • File resolution order in script is REPO/<file> first, WS/<file> second for session-state.md, pending-questions.md, build_log.md. Per CLAUDE.md workspace contract, all three live in the workspace by default — REPO-first probe is dead-code for the canonical layout but harmless (falls through correctly).
  • content.startswith("<") filter for JSONL turns will skip legitimate prose that starts with < (rare but possible — e.g. <3 or <some-tag> referenced as code). Acceptable tradeoff for noise suppression.
  • The proactive-loop sentinel relies on either PreCompact OR SessionStop firing to clear itself. If a session ends without either (e.g. SIGKILL, OOM, no install-hook.sh run on a fresh machine), the sentinel persists and catchup never re-fires on the next start. Documented in SKILL.md as a Setup recommendation, so a known + acknowledged failure mode.

Sentinel-race sanity: schedule-crons and proactive-loop are sequenced (schedule-crons is invoked from inside proactive-loop step 2). So step 0 of schedule-crons runs first → touches sentinel → step 1 of proactive-loop sees sentinel → skips. No race. Post-#954 startup goes through schedule-crons directly, so step 0 of schedule-crons IS the catchup trigger; step 1 of proactive-loop is the dual-entry-point belt-and-suspenders. Correct design.

Recommendation: LGTM from MacBook. The CHANGES_REQUESTED is still in effect on the GH side because no one has formally dismissed it — sonichi needs to either dismiss the 13:10Z CHANGES_REQUESTED or submit a new APPROVED review. Posting as COMMENTED rather than APPROVED to avoid auto-dismissing another reviewer's block under the shared sonichi identity (feedback_ask_other_party_before_reverting_their_action).

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Per Chi's direction (option 2) — formal APPROVED to dismiss the 13:10Z CHANGES_REQUESTED. Code already verified in my 20:10Z COMMENTED review.

@sonichi
sonichi merged commit 3c8e6d6 into main May 23, 2026
2 checks passed
@sonichi
sonichi deleted the feat/catchup-after-startup-skill branch May 23, 2026 20:16
sonichi added a commit that referenced this pull request May 23, 2026
Adds src/install-watch-tasks-cleanup-hook.sh — idempotent installer for
the new Stop hook that pairs with watch-tasks-stream.sh PID-file cleanup.

Per Chi 2026-05-23 (DM thread): mirrors Lucy's per-PR pattern in #1056
(`install-hook.sh` for SessionStop → session-handoff.sh). Each PR that
adds a new hook ships its own targeted installer; the fleet-wide
install-hooks.sh is deferred (option 3 from the A/B/C choice).

Per `feedback_claude_code_hook_scoping` (memory): sutando hooks belong
at project-level `.claude/settings.json` (gitignored, per-machine), NOT
user-level `~/.claude/settings.json` — they only fire when Claude runs
in the sutando project context, not in unrelated sessions.

Tested:
- fresh install on empty `{}` → adds 1 hook entry under hooks.Stop[0]
- re-run on installed → no-op, settings unchanged
- detect-by-command-string match prevents duplicate insertion
- atomic via jq + tmp+mv

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request May 23, 2026
…ng-tasks

Renames src/install-watch-tasks-cleanup-hook.sh → src/install-claude-hooks.sh
and extends to install all 4 sutando-owned project-level hooks
idempotently:

  PreCompact → cp $TRANSCRIPT_PATH ~/Desktop/sutando-conversations/...
  PreCompact → bash src/session-handoff.sh "$TRANSCRIPT_PATH"
  Stop       → bash src/check-pending-tasks.sh
  Stop       → watcher-cleanup PID kill (#1065's contribution)

Per Chi 2026-05-23 17:49Z: the previous installer (just watcher-cleanup)
left 3 hooks installerless. A fresh Mac would have been missing transcript
backup, PreCompact session-handoff, and pending-tasks check after #1065
+ #1056 landed. This consolidates them into one place — but stays per-PR
scope (doesn't grow into a fleet `install-hooks.sh`).

Note: Lucy's #1056 installer for SessionStop → session-handoff.sh remains
independent (different event); the two installers are orthogonal and both
should be run on a fresh Mac.

Tested all 3 paths:
- fresh install on empty `{}`        → added=4, skipped=0
- re-run after install               → added=0, skipped=4
- re-run on existing settings.json   → added=0, skipped=4 (no churn)

Per-event command-string detection prevents duplicates; jq + tmp+mv keeps
the edit atomic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request May 23, 2026
…ootstrap

Per Chi 2026-05-23 Discord redesign discussion ("we can make a new skill
and include everything we need at start"). Wraps the sequence:

  1. /catchup-after-startup  (briefing, #1056)
  2. /task-orphan-check      (orphan recovery, optional/sentinel-guarded)
  3. /schedule-crons         (cron registration + watcher start)
  4. touch sentinel
  5. emit summary

into a thin orchestrator. Each sub-skill stays callable standalone for
manual re-invocation; /startup just enforces the order.

Background: post-#954 the CLI startup target became /schedule-crons,
which over time accumulated startup ceremony (step 0 catchup from #1056,
step 4 cron fallback, step 5 watcher start) on top of its actual job
(reading crons.json). The launchd plist + CLI scripts pointing at
/schedule-crons made it the de-facto entry without ever being declared
as such. /startup makes that explicit and gives future startup work an
unambiguous home.

This PR ships only the skill itself. Migration of the launchd plist /
CLI invocation scripts to call /startup instead of /schedule-crons is a
separate follow-up (operator-side, no code change in this repo). The
/task-orphan-check reference is sentinel-guarded — /startup works fine
without it; the orphan-check skill is the recovery half of the #1049
redesign and ships in its own PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request May 23, 2026
…ootstrap (#1072)

* feat(startup): canonical single-entry-point skill for fresh-session bootstrap

Per Chi 2026-05-23 Discord redesign discussion ("we can make a new skill
and include everything we need at start"). Wraps the sequence:

  1. /catchup-after-startup  (briefing, #1056)
  2. /task-orphan-check      (orphan recovery, optional/sentinel-guarded)
  3. /schedule-crons         (cron registration + watcher start)
  4. touch sentinel
  5. emit summary

into a thin orchestrator. Each sub-skill stays callable standalone for
manual re-invocation; /startup just enforces the order.

Background: post-#954 the CLI startup target became /schedule-crons,
which over time accumulated startup ceremony (step 0 catchup from #1056,
step 4 cron fallback, step 5 watcher start) on top of its actual job
(reading crons.json). The launchd plist + CLI scripts pointing at
/schedule-crons made it the de-facto entry without ever being declared
as such. /startup makes that explicit and gives future startup work an
unambiguous home.

This PR ships only the skill itself. Migration of the launchd plist /
CLI invocation scripts to call /startup instead of /schedule-crons is a
separate follow-up (operator-side, no code change in this repo). The
/task-orphan-check reference is sentinel-guarded — /startup works fine
without it; the orphan-check skill is the recovery half of the #1049
redesign and ships in its own PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(startup): move sentinel touch to step 1b — closes double-catchup bug

Per qingyun-sutando review on #1072 (2026-05-23 22:18Z): the original
sentinel touch lived at step 4 (the end), but step 3 (/schedule-crons)
re-checks the sentinel at its own step 0 and re-runs /catchup-after-startup
if absent. Net effect: every fresh /startup → catchup fires twice (once at
step 1, once again inside /schedule-crons because the sentinel hadn't been
touched yet).

Fix: move the touch to a new step 1b, immediately after step 1's catchup
and BEFORE step 2 / step 3. Both /task-orphan-check (step 2) and
/schedule-crons (step 3) now see the touched-sentinel state — symmetric.
Renumbered "confirm" from step 5 to step 4. Sequence diagram updated.

No behavior change beyond the ordering. Operator-visible difference is
that the briefing prints once instead of twice on every fresh CLI start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request May 23, 2026
…er fix, follow-up to #1063) (#1065)

* chore(watch-tasks-stream): PID file + Stop-hook cleanup (orphan-watcher fix, follow-up to #1063)

When a Claude Code session ends, the streaming task watcher started via the
`Monitor` tool can outlive it as an orphan fswatch process — diagnosed via
PR #1063 (heartbeat) which Chi closed in favor of an explicit Stop-hook
cleanup path. This is option A from that discussion (option B = fleet
install-hooks.sh ships separately).

Changes:
- `src/watch-tasks-stream.sh`: write own PID to
  `$WORKSPACE/state/watch-tasks-stream.pid` at startup; `trap EXIT` removes
  it on clean shutdown (SIGTERM/SIGINT/normal exit).
- `src/startup.sh`: reap stale PID file from a prior hard crash before the
  new session starts. Cmdline check (`ps -p $PID -o args= | grep
  watch-tasks-stream`) before kill so a recycled PID held by an unrelated
  process is never killed; stale file is removed unconditionally.

Companion settings.json edit (NOT in this PR — `.claude/` is gitignored, so
each fleet host applies it locally; option B install-hooks.sh will automate
fleet-wide): add a second Stop hook command alongside `check-pending-tasks.sh`:

  PID_FILE="${SUTANDO_WORKSPACE:-$HOME/.sutando/workspace}/state/watch-tasks-stream.pid"
  if [ -f "$PID_FILE" ]; then
    PID=$(cat "$PID_FILE" 2>/dev/null)
    [ -n "$PID" ] && kill "$PID" 2>/dev/null
    rm -f "$PID_FILE"
  fi
  exit 0

Already applied locally on this MacBook for testing.

Three exit paths covered:
1. Clean Claude Stop  -> Stop hook kills + removes PID file.
2. SIGTERM/SIGINT     -> EXIT trap inside watcher removes PID file.
3. Hard crash (SIGKILL, panic, force-quit, power loss) -> next startup.sh
   run reaps the orphan (if still alive + cmdline matches) and clears the
   stale PID file.

Verified locally with lifecycle tests covering all three cases (clean
SIGTERM cleanup, stale-PID reap with defunct PID, recycled-PID skip via
cmdline check).

Per Chi 2026-05-23 — branch + PR (no merge); Chi reviews before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(watch-tasks-stream): per-PR install-hook installer

Adds src/install-watch-tasks-cleanup-hook.sh — idempotent installer for
the new Stop hook that pairs with watch-tasks-stream.sh PID-file cleanup.

Per Chi 2026-05-23 (DM thread): mirrors Lucy's per-PR pattern in #1056
(`install-hook.sh` for SessionStop → session-handoff.sh). Each PR that
adds a new hook ships its own targeted installer; the fleet-wide
install-hooks.sh is deferred (option 3 from the A/B/C choice).

Per `feedback_claude_code_hook_scoping` (memory): sutando hooks belong
at project-level `.claude/settings.json` (gitignored, per-machine), NOT
user-level `~/.claude/settings.json` — they only fire when Claude runs
in the sutando project context, not in unrelated sessions.

Tested:
- fresh install on empty `{}` → adds 1 hook entry under hooks.Stop[0]
- re-run on installed → no-op, settings unchanged
- detect-by-command-string match prevents duplicate insertion
- atomic via jq + tmp+mv

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(install-claude-hooks): expand to cover PreCompact + check-pending-tasks

Renames src/install-watch-tasks-cleanup-hook.sh → src/install-claude-hooks.sh
and extends to install all 4 sutando-owned project-level hooks
idempotently:

  PreCompact → cp $TRANSCRIPT_PATH ~/Desktop/sutando-conversations/...
  PreCompact → bash src/session-handoff.sh "$TRANSCRIPT_PATH"
  Stop       → bash src/check-pending-tasks.sh
  Stop       → watcher-cleanup PID kill (#1065's contribution)

Per Chi 2026-05-23 17:49Z: the previous installer (just watcher-cleanup)
left 3 hooks installerless. A fresh Mac would have been missing transcript
backup, PreCompact session-handoff, and pending-tasks check after #1065
+ #1056 landed. This consolidates them into one place — but stays per-PR
scope (doesn't grow into a fleet `install-hooks.sh`).

Note: Lucy's #1056 installer for SessionStop → session-handoff.sh remains
independent (different event); the two installers are orthogonal and both
should be run on a fresh Mac.

Tested all 3 paths:
- fresh install on empty `{}`        → added=4, skipped=0
- re-run after install               → added=0, skipped=4
- re-run on existing settings.json   → added=0, skipped=4 (no churn)

Per-event command-string detection prevents duplicates; jq + tmp+mv keeps
the edit atomic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 added a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…art (sonichi#1056)

* feat(catchup-after-startup): rebuild last-session context on fresh start

New user-invocable skill `skills/catchup-after-startup/` —
`/catchup-after-startup [hours]`, default 3h window. Reads everything
Sutando persists across restarts and emits a single briefing into the
new session's conversation buffer before the user types: session-state.md,
open PRs by liususan091219, in-flight workspace/tasks, recent results,
pending-questions filtered to un-resolved, last-N-hours voice/phone/
discord activity from data/conversation.sqlite (per-surface tables from
sonichi#1051), recent chat from logs/conversation.log, recent git commits,
build_log tail with staleness warning, health-check one-liner.

Designed to be the FIRST action of any fresh proactive-loop session so
the bot doesn't restart cold and forget what it was working on, what
PRs are open, what channel a thread lives in.

Recall half of sonichi#1032 (episodic event memory). The capture half (wiring
event_log.py into every lifecycle point) remains a separate followup
tracked under the same issue.

scripts/install-hook.sh is an idempotent installer for a paired
SessionStop hook in ~/.claude/settings.json. Without the hook,
session-state.md is only written on PreCompact; a clean ⌘Q between
compactions leaves the most-recent N-min window invisible to the next
catchup. The skill still works without the hook (the other 9 sections
are real-time persisted regardless) — installing closes the last gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* address sonichi#1056 review: REPO default, pipefail, arg parsing, filter, auto-fire framing

Combined fixes for @qingyun-wu's + Sutando-Mini's review comments on
sonichi#1056. Both reviewers flagged the hardcoded personal REPO path as a
blocker; the rest is polish + correctness.

Script (skills/catchup-after-startup/scripts/catchup-after-startup.sh):

- REPO default: env wins, else probe $HOME/Desktop/sutando,
  $HOME/Documents/sutando/sutando, $HOME/Documents/sutando, $HOME/sutando,
  $(pwd) for a real checkout (CLAUDE.md + skills/ + .git signature, same
  heuristic session-handoff.sh uses), else fall back to the conventional
  $HOME/Desktop/sutando. Auto-finds the repo without per-user env on any
  reasonable layout. (qingyun sonichi#1 + Mini sonichi#3)

- Loud one-liner warns when REPO doesn't resolve to a real dir, so a
  misconfigured operator sees "REPO not found" instead of silently empty
  briefing sections. (qingyun sonichi#2)

- $1 honored for the documented [hours] arg. Pre-fix, only CATCHUP_HOURS
  env was read; positional arg was silently ignored. (Mini sonichi#1)

- set -o pipefail + restructure all piped commands to capture output
  into a variable + sentinel-on-failure, so a sqlite/git/python failure
  inside a pipe surfaces as "(failed: ...)" instead of silently empty
  output. The "(none)" vs "(failed)" vs "(missing)" distinction is the
  whole point of the empty-vs-failed contract. (Mini sonichi#2)

- pending-questions resolution filter no longer over-matches bare 'DONE'
  / 'RESOLVED' substrings in body prose. Now requires an anchored marker
  — header containing ✅/Resolved/Dismissed/DONE, or a body line literally
  starting with ✅. (qingyun sonichi#4)

SKILL.md:

- Reframe "auto-fire" section. Pre-fix it implied auto-firing was
  built-in; in reality the slash command alone gives no auto-fire.
  Renamed to "Wiring for auto-invocation (operator-side, NOT in this
  PR)" with a sample markdown snippet operators can drop into their own
  proactive-loop SKILL.md. (Mini sonichi#4)

- Explicit note that the sqlite section depends on sonichi#1051's per-surface
  schema; on a pre-sonichi#1051 db that section degrades to "(sqlite query
  failed)" while the other 9 sections still work. (qingyun sonichi#3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(catchup): jsonl-source recovery + install-hook probe + proactive-loop wiring (sonichi#1056)

Three follow-up fixes on top of the address-review commit, addressing the
last open blocker plus a real-incident fix plus the recovery mechanism that
makes catchup work even when hooks fail.

**1. Wire catchup into proactive-loop "On activation" step 1.**
Addresses sonichi sonichi#4 — the SKILL.md claim "auto-fire as first action of
fresh proactive-loop session" was not wired in code. Now: sentinel-guarded
fire-once-per-fresh-session, skipped on cron-driven passes.

**2. install-hook.sh resolves REPO at install time.**
Real incident 2026-05-23 on Mac Studio: the old hook embedded
\`\${SUTANDO_REPO_DIR:-\$HOME/Desktop/sutando}\` verbatim, so machines without
the env set AND without a Desktop/sutando checkout silently no-op'd every
SessionStop — session-state.md never got written. Catchup's primary source
was therefore empty for weeks. Now the installer uses the same probe as
catchup-after-startup.sh and bakes the literal path into the hook; misconfig
fails loudly at install instead of silently at fire-time.

**3. New section: "Previous-session transcript (last N h)".**
session-state.md is a 50-line summary written by session-handoff.sh, which
only fires when the hooks succeed. Both have failed in practice. The Claude
Code project transcript .jsonl is always there (Claude writes it natively,
no hooks) and is richer (full user/assistant turns vs a summary). When
session-state.md is missing or stale, this is the source of truth for
"what was happening last session". Reads the most-recent .jsonl in
\$HOME/.claude/projects/<repo-slug>/ that's been untouched for >1min (skips
the live session's file). Filters out tool-result envelopes, system reminders,
interrupt markers. Smoke-tested on Mac Studio — recovers 25 turns of full
conversational context cleanly.

* fix(session-handoff): clear proactive-loop sentinel so next session auto-fires catchup (sonichi#1056)

Paired with `skills/proactive-loop/SKILL.md` step 1's sentinel guard. Without
this, the `proactive-loop-started.sentinel` from the just-ended session
persists across restart — next session's first `/proactive-loop` sees the
sentinel, skips `/catchup-after-startup`, and the auto-fire wiring is
silently defeated for every restart after the first.

One-line fix: SessionStop hook → session-handoff.sh now `rm -f`s the
sentinel after writing session-state.md. Idempotent (no-op if sentinel
absent).

* feat(schedule-crons): fire /catchup-after-startup on fresh session (sonichi#1056)

Wire catchup at the schedule-crons bootstrap entry so it runs
synchronously at session start, not 5 min later when the first
main-loop cron fires /proactive-loop.

Post-sonichi#954 (Chi), start-cli.sh boots with `-- "/schedule-crons"`,
making this skill the actual startup entry. With `822e630` of this PR
catchup is wired into /proactive-loop step 1 — but that path only
fires after the main-loop cron first triggers (~5 min lag, longer
if the cron list grew the loop entry above */5). Wiring catchup here
as well lets the same sentinel-guarded fresh-session check trigger
immediately on bootstrap. Whichever path runs first touches the
sentinel; the other skips. Idempotent.

Defensive guard: if the `catchup-after-startup` skill dir isn't
installed yet (pre-merge of this PR on a fresh checkout), step 0
silently skips. No hard dependency on this PR landing first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…ootstrap (sonichi#1072)

* feat(startup): canonical single-entry-point skill for fresh-session bootstrap

Per Chi 2026-05-23 Discord redesign discussion ("we can make a new skill
and include everything we need at start"). Wraps the sequence:

  1. /catchup-after-startup  (briefing, sonichi#1056)
  2. /task-orphan-check      (orphan recovery, optional/sentinel-guarded)
  3. /schedule-crons         (cron registration + watcher start)
  4. touch sentinel
  5. emit summary

into a thin orchestrator. Each sub-skill stays callable standalone for
manual re-invocation; /startup just enforces the order.

Background: post-sonichi#954 the CLI startup target became /schedule-crons,
which over time accumulated startup ceremony (step 0 catchup from sonichi#1056,
step 4 cron fallback, step 5 watcher start) on top of its actual job
(reading crons.json). The launchd plist + CLI scripts pointing at
/schedule-crons made it the de-facto entry without ever being declared
as such. /startup makes that explicit and gives future startup work an
unambiguous home.

This PR ships only the skill itself. Migration of the launchd plist /
CLI invocation scripts to call /startup instead of /schedule-crons is a
separate follow-up (operator-side, no code change in this repo). The
/task-orphan-check reference is sentinel-guarded — /startup works fine
without it; the orphan-check skill is the recovery half of the sonichi#1049
redesign and ships in its own PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(startup): move sentinel touch to step 1b — closes double-catchup bug

Per qingyun-sutando review on sonichi#1072 (2026-05-23 22:18Z): the original
sentinel touch lived at step 4 (the end), but step 3 (/schedule-crons)
re-checks the sentinel at its own step 0 and re-runs /catchup-after-startup
if absent. Net effect: every fresh /startup → catchup fires twice (once at
step 1, once again inside /schedule-crons because the sentinel hadn't been
touched yet).

Fix: move the touch to a new step 1b, immediately after step 1's catchup
and BEFORE step 2 / step 3. Both /task-orphan-check (step 2) and
/schedule-crons (step 3) now see the touched-sentinel state — symmetric.
Renumbered "confirm" from step 5 to step 4. Sequence diagram updated.

No behavior change beyond the ordering. Operator-visible difference is
that the briefing prints once instead of twice on every fresh CLI start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…er fix, follow-up to sonichi#1063) (sonichi#1065)

* chore(watch-tasks-stream): PID file + Stop-hook cleanup (orphan-watcher fix, follow-up to sonichi#1063)

When a Claude Code session ends, the streaming task watcher started via the
`Monitor` tool can outlive it as an orphan fswatch process — diagnosed via
PR sonichi#1063 (heartbeat) which Chi closed in favor of an explicit Stop-hook
cleanup path. This is option A from that discussion (option B = fleet
install-hooks.sh ships separately).

Changes:
- `src/watch-tasks-stream.sh`: write own PID to
  `$WORKSPACE/state/watch-tasks-stream.pid` at startup; `trap EXIT` removes
  it on clean shutdown (SIGTERM/SIGINT/normal exit).
- `src/startup.sh`: reap stale PID file from a prior hard crash before the
  new session starts. Cmdline check (`ps -p $PID -o args= | grep
  watch-tasks-stream`) before kill so a recycled PID held by an unrelated
  process is never killed; stale file is removed unconditionally.

Companion settings.json edit (NOT in this PR — `.claude/` is gitignored, so
each fleet host applies it locally; option B install-hooks.sh will automate
fleet-wide): add a second Stop hook command alongside `check-pending-tasks.sh`:

  PID_FILE="${SUTANDO_WORKSPACE:-$HOME/.sutando/workspace}/state/watch-tasks-stream.pid"
  if [ -f "$PID_FILE" ]; then
    PID=$(cat "$PID_FILE" 2>/dev/null)
    [ -n "$PID" ] && kill "$PID" 2>/dev/null
    rm -f "$PID_FILE"
  fi
  exit 0

Already applied locally on this MacBook for testing.

Three exit paths covered:
1. Clean Claude Stop  -> Stop hook kills + removes PID file.
2. SIGTERM/SIGINT     -> EXIT trap inside watcher removes PID file.
3. Hard crash (SIGKILL, panic, force-quit, power loss) -> next startup.sh
   run reaps the orphan (if still alive + cmdline matches) and clears the
   stale PID file.

Verified locally with lifecycle tests covering all three cases (clean
SIGTERM cleanup, stale-PID reap with defunct PID, recycled-PID skip via
cmdline check).

Per Chi 2026-05-23 — branch + PR (no merge); Chi reviews before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(watch-tasks-stream): per-PR install-hook installer

Adds src/install-watch-tasks-cleanup-hook.sh — idempotent installer for
the new Stop hook that pairs with watch-tasks-stream.sh PID-file cleanup.

Per Chi 2026-05-23 (DM thread): mirrors Lucy's per-PR pattern in sonichi#1056
(`install-hook.sh` for SessionStop → session-handoff.sh). Each PR that
adds a new hook ships its own targeted installer; the fleet-wide
install-hooks.sh is deferred (option 3 from the A/B/C choice).

Per `feedback_claude_code_hook_scoping` (memory): sutando hooks belong
at project-level `.claude/settings.json` (gitignored, per-machine), NOT
user-level `~/.claude/settings.json` — they only fire when Claude runs
in the sutando project context, not in unrelated sessions.

Tested:
- fresh install on empty `{}` → adds 1 hook entry under hooks.Stop[0]
- re-run on installed → no-op, settings unchanged
- detect-by-command-string match prevents duplicate insertion
- atomic via jq + tmp+mv

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(install-claude-hooks): expand to cover PreCompact + check-pending-tasks

Renames src/install-watch-tasks-cleanup-hook.sh → src/install-claude-hooks.sh
and extends to install all 4 sutando-owned project-level hooks
idempotently:

  PreCompact → cp $TRANSCRIPT_PATH ~/Desktop/sutando-conversations/...
  PreCompact → bash src/session-handoff.sh "$TRANSCRIPT_PATH"
  Stop       → bash src/check-pending-tasks.sh
  Stop       → watcher-cleanup PID kill (sonichi#1065's contribution)

Per Chi 2026-05-23 17:49Z: the previous installer (just watcher-cleanup)
left 3 hooks installerless. A fresh Mac would have been missing transcript
backup, PreCompact session-handoff, and pending-tasks check after sonichi#1065
+ sonichi#1056 landed. This consolidates them into one place — but stays per-PR
scope (doesn't grow into a fleet `install-hooks.sh`).

Note: Lucy's sonichi#1056 installer for SessionStop → session-handoff.sh remains
independent (different event); the two installers are orthogonal and both
should be run on a fresh Mac.

Tested all 3 paths:
- fresh install on empty `{}`        → added=4, skipped=0
- re-run after install               → added=0, skipped=4
- re-run on existing settings.json   → added=0, skipped=4 (no churn)

Per-event command-string detection prevents duplicates; jq + tmp+mv keeps
the edit atomic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Jun 20, 2026
… comment

The comment documenting the pre-#1056 hardcoded-path removal still embedded a
personal username (/Users/xueqingliu/...). Genericize to /Users/<name>/ — keep the
#1056 review attribution. Last hardcoded personal username in public code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Jun 20, 2026
…(public code) (#1725)

* fix(probe-team-sandbox): drop hardcoded /Users/wangchi path from the dotenv_read probe

The dotenv_read sandbox-refusal probe hardcoded `/Users/wangchi/Desktop/sutando/.env`
as the secret-file path it asks the sandboxed agent to read. That leaks a personal
username into public OSS code. The probe only needs *a* secret-looking path to verify
the agent refuses — swap to the generic `~/.sutando/.env` (zero behavior change; the
refusal regex is unaffected). Caught in a repo-wide cold-review (Pro).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(catchup): genericize the /Users/<name> username in the repo-probe comment

The comment documenting the pre-#1056 hardcoded-path removal still embedded a
personal username (/Users/xueqingliu/...). Genericize to /Users/<name>/ — keep the
#1056 review attribution. Last hardcoded personal username in public code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants