Skip to content

fix(quota-tracker): narrow bare-except to ImportError - #1094

Merged
liususan091219 merged 1 commit into
sonichi:mainfrom
VasiliyRad:fix/quota-narrow-except
May 29, 2026
Merged

liususan091219 merged 1 commit into
sonichi:mainfrom
VasiliyRad:fix/quota-narrow-except

Conversation

@VasiliyRad

Copy link
Copy Markdown
Contributor

Summary

Why

The bare except Exception: masked a path bug (#1055) for 12h by swallowing ImportError and falling through to "No quota-state.json found." Narrowing to ImportError means only the expected failure mode (module not on path) is caught; any other exception from status_read_path propagates visibly in cron logs.

Test plan

  • python3 skills/quota-tracker/scripts/read-quota.py prints live 5h/7d windows (verified)
  • Remove src/ from sys.path → raises ImportError → falls through to _canonical = None (expected)

🤖 Generated with Claude Code

@CLAassistant

CLAassistant commented May 25, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@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.

LGTM — minimal correctness fix. ImportError (and its subclass ModuleNotFoundError) covers both module-not-on-path and missing-name-from-module cases at this import site. Any other failure path (e.g. a future status_read_path body that raises during evaluation) now propagates visibly instead of silently falling through to _canonical = None — that's the whole point of #1062.

Diff is one token; tsc+tests green. CLA pending — needs contributor signature before merge.

@liususan091219 liususan091219 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.

LGTM — single-token correctness fix on skills/quota-tracker/scripts/read-quota.py:25.

  1. The narrowing is the right call. Changing except Exception: → except ImportError: correctly scopes the catch to the only failure mode that should fall through to _canonical = None: the import-side bug where src/ isn't on sys.path. Anything else (status_read_path raising on a malformed payload, an OSError from a permission issue, a KeyError from workspace_default returning an unexpected shape) now surfaces visibly in cron logs instead of silently collapsing to "No quota-state.json found."
  2. The PR body's debugging-history rationale is concrete. "Masked a path bug (#1055) for 12h by swallowing ImportError" is exactly the kind of past-incident that justifies tightening. The narrower except will pay for itself the first time status_read_path raises a KeyError or TypeError from a future schema change — instead of 12h of silent fallback, it'll fail loud in the same cron logs you're already reading.
  3. Pairs naturally with #1062's intent. This is a 1-line counterpart to whatever larger structural fix #1062 will land; the bare-except narrowing is a "leaf" cleanup that's safe to ship independently and unblocks the next debugging round.

Non-blocking observation: there's still one bare except Exception: in the same file's later candidate-list loop (probably intentional — json.loads() and file-open errors expected as quota-state may be absent/partial mid-write). If that catch ever masks a real bug the same way, it'd be worth a follow-up to tighten to (OSError, json.JSONDecodeError). Not for this PR.

Cross-PR linkage: PR #1093 lives in the same "narrow what we catch / fail loud earlier" arc — both tighten error-handling that was masking real signal. Consistent direction across recent infrastructure PRs.

Tests note: The PR body's test plan is the right shape — python3 read-quota.py confirms happy path, and the unchecked box ("Remove src/ from sys.path → raises ImportError → falls through") is verifiable in a one-liner: PYTHONPATH= python3 read-quota.py. Worth running before merge if it's easy, but not blocking — the diff is literally one-token and the fallback path is unchanged.

Ready to merge.

— Lucy (Mac Studio bot)

@bassilkhilo-ag2

Copy link
Copy Markdown
Collaborator

Cold review (Bassil's Sutando node):

LGTM — single-token fix, exactly right.

except Exception: was masking FileNotFoundError (and any other exception from status_read_path) — the root of #1055. except ImportError: is the correct scope: the only expected failure mode at that site is workspace_default not being on sys.path, which raises ImportError. Any other exception (file I/O, bad JSON, etc.) now propagates visibly in cron logs instead of silently falling through to _canonical = None.

No observations. Clean diff, proper scope, closes #1062.

@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.

LGTM — textbook narrow-bare-except fix. ImportError is the only failure mode workspace_default can produce at module-import time (it's either on sys.path or it isn't); any other exception from status_read_path was previously being swallowed and silently routing through the legacy candidate-list fallback, which is exactly the failure pattern that hid #1055 for 12h. Narrowing makes those failures visible in cron logs while preserving the legitimate fallback for the "module not available" case.

Tiny, surgical, no risk surface. Owner merge call.

@bassilkhilo-ag2 bassilkhilo-ag2 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.

LGTM. Single-token fix with the right reasoning — narrowing to ImportError means only the expected failure mode (module not on path) is caught; any other exception from status_read_path propagates visibly in cron logs instead of silently falling through to 'No quota-state.json found.' Same pattern used in other skill scripts.

@liususan091219 liususan091219 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.

BLOCKING: None.

NON-BLOCKING:

  • Clean one-token narrowing. from workspace_default import status_read_path is the only ImportError path in the try; status_read_path() returns a Path (no exception expected in normal use). Bug history (#1055/#1062 — bare except swallowed a real path bug for 12h) directly justifies this change.
  • One thing worth flagging post-merge: if status_read_path() ever raises something path-related (FileNotFoundError, OSError under odd HOME setups), it now propagates instead of being swallowed. That IS the desired behavior per the PR rationale (visible-in-cron), but worth a callout in case anyone sees a new traceback in cron logs and wonders.
  • Test plan item 2 ("Remove src/ from sys.path → raises ImportError") is the right validation; leaving it unchecked is fine for a single-token PR but a quick python3 -c 'import sys; sys.path = [p for p in sys.path if "src" not in p]; ...' would close it.

LGTM on the fix itself.

@bassilkhilo-ag2 bassilkhilo-ag2 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.

LGTM. Single-line fix is correct and minimal — except ImportError is exactly the right scope. The original except Exception was masking path/logic bugs (as #1055 demonstrated). No tests needed for a one-token change to an exception clause; the PR body's manual test plan is sufficient.

@bassilkhilo-ag2 bassilkhilo-ag2 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.

Code review (Sutando bot):

PR #1094 — fix(quota-tracker): narrow bare-except to ImportError

This is the correct fix. The original except Exception: would swallow runtime errors from workspace_default (attribute errors, value errors, etc.) that should propagate — they indicate something is wrong with the workspace setup, not just that the module is missing.

except ImportError: precisely targets the "module not installed" case, which is the only legitimate reason to fall back to the quota-state.json path lookup.

Verdict: LGTM — tight, principled, exactly the right scope.

— Chi (Sutando node 2)

@bassilkhilo-ag2 bassilkhilo-ag2 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.

Correct fix — ImportError is the only exception that should be caught here (missing workspace_default module). The bare except Exception would silently mask real errors (OS errors, JSON parse failures, etc.) and make the quota system appear unavailable when the actual problem was something else entirely. LGTM.

@jgluck96 jgluck96 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Single-token narrowing of bare except Exception: to except ImportError: is the right fix — only the expected import-not-on-path failure mode should be silently swallowed. Any other exception from status_read_path should propagate.

@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.

LGTM. Same 1-line fix as per #1062 — correct narrowing. Note: I separately opened #1283 with the same fix without checking for existing PRs first; #1283 is a duplicate and should be closed in favor of this one (which came first).

@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.

LGTM — single-token fix, exactly right. The bare was masking real import failures (as it did for #1055). Closing my duplicate #1300.

@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.

LGTM. Single-token fix, exactly right. Bare except was masking real import failures (as it did for #1055). Closing my duplicate #1300.

@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.

LGTM. Single-token fix, exactly right. Bare except was masking real import failures. Closing my duplicate #1300.

@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.

LGTM — exactly the right scope. except ImportError: is what the original comment asked for. Any other exception from status_read_path is a real bug and should propagate, not be silently swallowed.

@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.

LGTM. Single-token fix: except Exception → except ImportError. Correct narrowing — only the expected module-not-on-path failure is caught; any other exception from status_read_path (e.g. PermissionError, FileNotFoundError) now propagates and surfaces instead of silently falling through to 'No quota-state.json found.'

@liususan091219

Copy link
Copy Markdown
Collaborator

@VasiliyRad — same CLA-mapping issue as #1093: this PR's commit is authored with vasiliyr@Vasiliys-MacBook-Pro.local (a Mac hostname email), which CLA-assistant can't resolve to a GitHub account. Your vasiliy@live.com is already CLA-signed and mapped (#1066 etc. merged fine on that).

Same one-shot fix as the #1093 thread:

git config user.email vasiliy@live.com
git rebase -i origin/main
# in the editor: change `pick` → `edit` on the first commit, then:
git commit --amend --reset-author --no-edit && git rebase --continue
git push --force-with-lease

CLA-assistant re-checks on the next push. ~2 min.

Once green here + #1093 + #1020, happy to babysit the merges (no rebase needed — this branch is currently 1 commit ahead of main, no conflicts).

— Lucy (Mac Studio bot)

@sonichi

sonichi commented May 29, 2026

Copy link
Copy Markdown
Owner

@cla-assistant check

…#1062)

The except Exception block on read-quota.py:25 masked a path bug for
12h by swallowing ImportError and falling through to "not found."
Narrow to ImportError so non-import failures in status_read_path
propagate visibly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@liususan091219
liususan091219 force-pushed the fix/quota-narrow-except branch from c540337 to c4c83c2 Compare May 29, 2026 20:54
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@liususan091219

Copy link
Copy Markdown
Collaborator

Took the liberty of applying the rebase + reset-author fix on your behalf since maintainerCanModify=true was set on the PR — your authorship is preserved (still Vasiliy Radostev) but with vasiliy@live.com as both author and committer, which is your CLA-mapped GitHub email. Force-pushed with --force-with-lease. CLA-Assistant should re-verify automatically; if it stays PENDING, ping me and I will trigger a recheck. — Lucy (Mac Studio bot)

@liususan091219
liususan091219 merged commit 5e1b24f into sonichi:main May 29, 2026
3 checks passed
amkunte added a commit to amkunte/sutando that referenced this pull request May 30, 2026
…ner DM (#50)

* docs: CONTRIBUTING — drop misplaced architecture section + transient items (#1341)

* docs: CONTRIBUTING — drop misplaced architecture section + transient items

Cleans up content that doesn't belong in CONTRIBUTING.md:

1. Drop the entire `## Architecture` section. It self-evidently
   pointed at README ("See README.md for the full architecture
   diagram") — architecture content belongs in README, not in the
   contributor guide.

2. Drop checklist item 5 ("Respect the V1-workspace hold list").
   Transient internal state — the hold list dissolves when the V1
   workspace contract finalizes. Public CONTRIBUTING shouldn't pin
   contributors to remember internal migration politics.

3. Drop checklist item 6 ("After update-branch, CLA-Assistant may
   not auto-rerun"). The new `.github/workflows/cla-recheck-on-push.yml`
   added in #1338 handles the recheck automatically; contributors no
   longer need to know the manual workaround.

4. "MacBook or Mac Mini" in review-process → generic "a maintainer
   (or a maintainer's bot)". The specific bot-instance hardware
   inventory is owner-internal trivia.

Checklist intro updated: "Six checks" → "Four checks".

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

* docs: CONTRIBUTING — surface Sutando-assisted duplicate search in §1

Per Susan: item 1 only listed raw \`gh\` commands. For contributors
already running a Sutando agent, the easier path is to just ask it
("are there existing PRs / issues for X?") — it runs the searches,
dedups by topic, and summarizes. Raw commands kept for users who
prefer them or don't have an agent.

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

* docs: CONTRIBUTING — add §5 'supervise your bot' (no repo abuse)

Per Susan: easy failure mode is letting an agent crank out 50–100+
PRs that duplicate each other, re-fix upstream-fixed bugs, or split
trivial cleanups across separate PRs. Documents the contributor norm:
cap in-flight PRs, read the agent's diff, don't batch repo-wide
refactors, and own what the bot ships.

Updates intro: "Four checks" → "Five checks".

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

* docs: CONTRIBUTING — drop hardcoded "5 open per author" cap

Per Susan: the specific number isn't necessarily right (context-dependent).
Kept the underlying intent — "land or close before opening more, don't let
the queue balloon faster than maintainers can review" — without a hardcoded
threshold.

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

* docs: CONTRIBUTING — concrete example for Bonus bug-report fields

Per Susan: the "Bonus (highly valued)" bullets were abstract. Adds a
real worked example using sonichi/sutando#1339 (filed today) showing
all three bonus items in practice — a POC script with output, the
regression commit identified via `git log -L`, and the exact function
+ line.

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

* docs: CONTRIBUTING — POC → validation (3 sites)

Per Susan: "validation script" is the more accurate term — these
scripts demonstrate the bug exists, not just a proof of concept.
Renamed all 3 call-sites (Bonus bullet, worked-example label,
worked-example summary line).

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

* docs: CONTRIBUTING — compress worked example to single link

Per Susan: keep the example high-level, not too specific. Replaced
the 7-line walkthrough of #1339's three bonus items with a single
"see #1339 for a recent worked example" pointer. Reader who wants
detail clicks through; reader who doesn't isn't burdened with it.

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

* docs: CONTRIBUTING — don't assume bonus bullet 2 is a regression

Per Susan: not every bug is a regression — some have been broken
since day 1. Generalized "when it regressed" → "suspected origin"
which covers both cases.

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

---------

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

* chore: relocate pointer-teacher-tracer + CONTEXT.md to skills/, document workspace migration in KNOWN_ISSUES (#1334)

* chore: delete dead code and empty repo dirs (15 audit-flagged sites)

[workspace-migration] Delete-only cleanup. 974 lines removed, 4 inserted.

Scope: the 15 dead-code sites the v3 workspace-contract audit flagged
(workspace/notes/cwd-audit-v3-2026-05-27.md), plus two top-level dirs
(`data/`, `logs/`) that strawman Part 1 calls out as repo-root state
leftovers from the pre-workspace-contract era.

Files deleted (whole-file):
  src/context-drop-inline.txt        — superseded by Sutando.app main.swift dropContext
  src/context-drop.sh                — same; legacy bash twin
  src/fix-setup.sh                   — one-shot migration helper, never called from current setup
  src/migrate-plists-to-logs-dir.sh  — one-shot plist→logs dir migration for #251
  src/migrate.sh                     — legacy bundle-migration tool, hardcoded -Desktop-sutando slug
  scripts/test-migrate-bundle.sh     — smoke test for migrate.sh, dies with it
  data/radar-topics.example.json     — stranded example, 0 references in code
  logs/.gitkeep                      — empty-dir placeholder; no code refs to <repo>/logs/

In-file dead lines removed:
  src/startup.sh L174-176            — legacy `mkdir -p tasks results data` in cwd; comment
                                       explicitly tagged "back-compat with scripts that haven't
                                       migrated yet" — no remaining callers, all watchers
                                       since #911 read from $SUTANDO_WORKSPACE.
  src/health-check.py L883-890       — `if not log_file.exists(): log_file = REPO_DIR/src/<name>.log`
                                       fallback. Comment in the file itself notes "src/ default
                                       was silently a no-op since 2026-04 when startup.sh was
                                       changed to write logs/<name>.log". Dropped the dead branch
                                       and the now-incomplete comment.
  skills/quota-tracker/scripts/credential-proxy.ts L18 — unused `readFileSync` import.

Verification:
  git grep checked for residual refs to each deleted filename across
    {src, scripts, skills, tests, *.json, *.yaml, *.yml, *.toml}
    → 0 external refs to any deleted file.
  npx tsc --noEmit clean.
  python3 -c 'ast.parse(...)' on health-check.py: parses ok.
  bash -n src/startup.sh: ok.

Strawman alignment: workspace-contract-redesign-strawman.md Part 1 lists
"to remove: logs, data, move pointer-xxx to skills". This PR ships the
first two. (pointer-teacher-tracer move can be a follow-up — separate
class of change.)

Refs v3 workspace-contract audit, dead-code subset (15 sites = 8 files,
+ 3 in-file dead lines).

* chore: move pointer-teacher-tracer/ → skills/pointer-teacher-tracer/

[workspace-migration] Per Susan 2026-05-28: strawman Part 1 lists
"To remove: logs, data, move 'pointer-xxx' to skills". Bundled
into this PR alongside the data/ + logs/ removals (already in
prior commit on this branch).

Pure move via `git mv` — preserves history. No file content
changed. 5 tracked files moved:
  pointer-teacher-tracer/.gitignore        → skills/pointer-teacher-tracer/.gitignore
  pointer-teacher-tracer/README.md          → skills/pointer-teacher-tracer/README.md
  pointer-teacher-tracer/pointer-overlay.swift → skills/pointer-teacher-tracer/pointer-overlay.swift
  pointer-teacher-tracer/resolver.py        → skills/pointer-teacher-tracer/resolver.py
  pointer-teacher-tracer/run.sh             → skills/pointer-teacher-tracer/run.sh

Compiled `pointer-overlay` binary remains gitignored via the
skill's own .gitignore (Mach-O artifact, regenerated by run.sh).

Verified zero external refs to the old path:
  git grep -E '\bpointer-teacher-tracer/' HEAD -- ':!**/*.md' \
    ':!skills/pointer-teacher-tracer/**'
  → (no output)

Refs strawman workspace-contract-redesign Part 1.

* chore: delete PERSONAL_CLAUDE.md.example (owner-internal template)

The .example file was a starter template for the gitignored
PERSONAL_CLAUDE.md (per the comment inside it). Public contributors
don't need the owner-internal work-menu template structure visible;
the gitignored PERSONAL_CLAUDE.md path is documented in the CLAUDE.md
"Personal overrides" section, which is enough.

Adds 1 more deletion to this cleanup PR.

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

* chore: move CONTEXT.md → skills/pointer-teacher-tracer/CONTEXT.md

CONTEXT.md is the Pointer Teacher domain vocabulary — it scopes
only that one skill (per its opening line). Belongs inside the
skill directory, not at the repo root.

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

* docs(KNOWN_ISSUES): add workspace-migration-in-progress entry

Per Susan: the main current known issue is that workspace contract
migration is mid-transition — some scripts still write per-user state
into <repo>/ instead of \$SUTANDO_WORKSPACE/. Users hitting a
path-resolution oddity should check repo vs workspace; the per-site
fixes are tracked in PRs #1271-#1334 (held in draft pending V1).

Existing entries (web-UI flicker, voice hallucinations, Gemini idle
timeout) kept — those are independent ongoing issues.

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

* docs(pointer-teacher-design): fix CONTEXT.md link after move

CONTEXT.md was moved to skills/pointer-teacher-tracer/ in this PR;
the cross-doc link in docs/pointer-teacher-design.md needs the
matching path update. Catch from Susan's deletion-safety audit ask.

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

* revert: keep startup.sh mkdir -p tasks results data — not dead yet

The original PR removed this line on the premise that it was "safe
no-op once everything's switched". But a deeper audit (per Susan's
ask) shows multiple consumers STILL expect repo-relative dirs:

- skills/openai-tts/scripts/synthesize.sh:26 — writes to \$REPO/results/
- skills/gemini-tts/scripts/synthesize.sh:31 — writes to \$REPO/results/
- src/task-bridge.ts:19 — TASK_DIR = join(REPO_DIR, 'tasks')
- src/session-handoff.sh:69 — reads \$REPO/tasks/*.txt
- scripts/query-conversation.sh:14 — \$REPO_DIR/data/conversation.sqlite default

The TTS scripts in particular would silently fail on fresh installs
where \$REPO/results/ doesn't exist (init.sh's create_dir_if_missing
ONLY creates under \$WORKSPACE/, never \$REPO/). Removal needs to wait
for the per-site workspace-migration PRs (#1280 covers TTS) to land.

Restoring the original line. Other deletions (whole files + health-
check.py log fallback + credential-proxy.ts unused import) are
unaffected and stay deleted.

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

* revert(health-check): restore the src/ log fallback removed in original PR

Per Susan's "no workspace-related behavior changes in #1334" rule. The
src/<name>.log fallback (when WORKSPACE/logs/<name>.log doesn't exist)
was claimed dead at 2026-04, but removing it changes log-stale detection
behavior. Restore to keep #1334 a pure dead-code / cosmetic cleanup PR;
the fallback removal can land in a focused workspace-migration PR later.

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

* revert: keep src/migrate.sh + scripts/test-migrate-bundle.sh

Per Susan: migrate.sh was last touched 2026-05-21 (commit e4cc341
"fix(migrate): non-destructive .env restore + bundle off iCloud-synced
Desktop") — that's active maintenance, not dead code. The other 5
deletions in this PR were all last-touched 3-7 weeks ago.
test-migrate-bundle.sh is migrate.sh's regression test, so it stays
alongside.

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

* revert: CONTEXT.md move + KNOWN_ISSUES.md addition + docs link tweak

Per Susan: PR #1334 should be ONLY dead-code deletions (no file moves,
no docs additions). Reverting:

- CONTEXT.md: restored at repo root (the move to
  skills/pointer-teacher-tracer/ is out of scope for a dead-code PR)
- KNOWN_ISSUES.md: removed my added "Workspace contract migration
  in progress" entry — workspace-related doc change, out of scope
- docs/pointer-teacher-design.md: link reverted to ../CONTEXT.md
  (corollary of the CONTEXT.md restore)

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

* revert: option B — restore deleted files, keep CONTEXT.md move + KNOWN_ISSUES.md entry

Per Susan's option B: PR #1334 stops doing dead-code deletions.
Restoring everything that was deleted; keeping the workspace-adjacent
doc/move work that was added in this branch.

Restored:
- src/context-drop-inline.txt (back at repo root)
- src/context-drop.sh
- src/fix-setup.sh
- src/migrate-plists-to-logs-dir.sh
- data/radar-topics.example.json
- logs/.gitkeep
- PERSONAL_CLAUDE.md.example
- skills/quota-tracker/scripts/credential-proxy.ts — readFileSync import restored

Kept (re-applied since they were reverted in the wrong-direction
commit 25a006e earlier):
- CONTEXT.md → skills/pointer-teacher-tracer/ (move)
- KNOWN_ISSUES.md: "Workspace contract migration in progress" entry
- docs/pointer-teacher-design.md: link updated to new CONTEXT.md path

PR scope is now: CONTEXT.md reorg + KNOWN_ISSUES.md doc update.
No dead-code deletions, no behavior changes.

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

---------

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

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Sutando-Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): make built-in-tools stub an explicit imperative (closes #854) (#1323)

Agents reading CLAUDE.md cold could miss the docs/built-in-tools.md
catalog when a user asks for email/calendar/etc. and answer "I don't
have that capability." The stub now reads as an explicit directive:
check that file BEFORE refusing or inventing a tool.


Stand: Sutando-Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* feat(dashboard): add Outbox card showing recent outbound messages (#1321)

* feat(dashboard): add Outbox card showing recent outbound messages (closes #932 Phase B-2)

Adds `get_outbox()` helper and an Outbox card to `render_dashboard()`.
The card calls `outbox_log.read_recent(10)` (shipped in #931) and renders
the last 10 outbound messages in reverse-chronological order with channel
icon, timestamp, recipient label, and body preview. Card is hidden when
the log is empty so the dashboard stays clean on fresh installs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): make built-in-tools stub an explicit imperative (closes #854) (#1323)

Agents reading CLAUDE.md cold could miss the docs/built-in-tools.md
catalog when a user asks for email/calendar/etc. and answer "I don't
have that capability." The stub now reads as an explicit directive:
check that file BEFORE refusing or inventing a tool.


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* feat(conversation-store): dedup migration + idempotent import (closes #941) (#1314)

* feat(conversation-store): dedup migration + idempotent import (#941 Part 1+2)

Adds scripts/dedup-conversation-store.py (dry-run by default, --commit to
execute) that removes content-identical duplicate rows from all conversation
tables (voice, phone, discord_voice, sessions, session_events, conversation).
Keeps the lowest rowid per content group.

Also adds best-effort UNIQUE INDEXes in both import scripts' ensure_schema()
and switches INSERTs to INSERT OR IGNORE so re-running the import is safe
once duplicates are cleared.

Flow:
  python3 scripts/dedup-conversation-store.py          # preview
  python3 scripts/dedup-conversation-store.py --commit  # clean dupes
  python3 scripts/import-conversation-log.py            # now idempotent
  python3 scripts/import-session-metrics.py             # now idempotent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* test(conversation-store): 12 tests for dedup-conversation-store.py (closes #941 test gap) (#1329)

Covers _count_dupes, _delete_dupes, _table_exists, and main() CLI:
- no-dupe tables return 0; N identical rows → N-1 reported/deleted
- NULL session_id normalized via COALESCE (two NULLs = same group)
- lowest rowid is kept after dedup
- dry-run (no --commit) does not modify the DB
- --commit removes dupes; second run is idempotent
- missing DB exits 0 with "nothing to do"
- absent tables are skipped, not errored


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): make built-in-tools stub an explicit imperative (closes #854) (#1323)

Agents reading CLAUDE.md cold could miss the docs/built-in-tools.md
catalog when a user asks for email/calendar/etc. and answer "I don't
have that capability." The stub now reads as an explicit directive:
check that file BEFORE refusing or inventing a tool.


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* feat(util_paths): SUTANDO_HOST_LABEL env override for stable per-machine identity (closes #871) (#1313)

* feat(util_paths): SUTANDO_HOST_LABEL env override for stable per-machine identity (closes #871)

When a user renames their Mac in System Settings, the hostname changes and
the bot starts a fresh per-machine state, losing pending-questions.md,
stand-identity.json, etc. SUTANDO_HOST_LABEL provides a stable override
that bypasses the hostname.

- Python: `os.environ.get("SUTANDO_HOST_LABEL") or socket.gethostname()`
- TypeScript: `(process.env.SUTANDO_HOST_LABEL || hostname())`
- Empty string falls back to hostname (same as unset)
- 3 new Python tests + 3 new TypeScript tests (8 total, all pass)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): make built-in-tools stub an explicit imperative (closes #854) (#1323)

Agents reading CLAUDE.md cold could miss the docs/built-in-tools.md
catalog when a user asks for email/calendar/etc. and answer "I don't
have that capability." The stub now reads as an explicit directive:
check that file BEFORE refusing or inventing a tool.


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* feat(discord-voice): meeting mode — manual + auto-idle (closes #1105) (#1311)

* feat(discord-voice): meeting mode — manual + auto-idle (closes #1105)

Adds meeting mode to discord-voice-server.ts, mirroring the meetingActive
behaviour in src/voice-agent.ts for the Discord voice surface.

**Manual mode**: polls state/voice-mode.txt every 2s (same file the
Sutando.app menu-bar + voice-agent already write). When content is
"meeting" → suppress audio output. When "active" → resume.

**Auto-idle mode**: if no user audio arrives for SUTANDO_VOICE_AUTO_MEETING_AFTER_SEC
(default 180s), flips to meeting mode automatically and writes
"meeting" to voice-mode.txt. Set to 0 to disable.

**Wake-phrase exit**: on turn.end, checks last user utterance for
phrases ("active mode", "sutando active", "lucy active", "wake up",
"stop meeting mode", or "<OWNER_NAME> active"). Match → flip to active
and write "active" to voice-mode.txt.

**What's suppressed vs kept**: only `pushAudio()` is gated. Transcription,
per-turn sqlite recording, and Gemini processing continue so the bot
captures meeting audio silently.

Per Susan (2026-05-25): "auto-meeting-mode is the load-bearing ask."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): make built-in-tools stub an explicit imperative (closes #854) (#1323)

Agents reading CLAUDE.md cold could miss the docs/built-in-tools.md
catalog when a user asks for email/calendar/etc. and answer "I don't
have that capability." The stub now reads as an explicit directive:
check that file BEFORE refusing or inventing a tool.


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(cli): enable tmux wheel-scroll in alternate-screen mode (sutando-plus#46) (#1304)

* fix(cli): enable tmux scroll in alternate-screen mode (sutando-plus#46)

Claude Code runs in alternate-screen mode and consumes mouse-wheel events
before tmux can intercept them. With `mouse on` alone, WheelUp in the
Claude Code pane is a no-op — tmux never enters copy-mode and the user
can't scroll through prior output.

Fix: override WheelUpPane to check `#{alternate_on}`. When set (Claude Code
TUI, vim, any alt-screen app), force `copy-mode -e` then replay the event so
the wheel scroll works against tmux's scrollback buffer. When not in
alternate screen, pass through normally (no behavior change for plain shell
prompts or scrollable apps like less that handle their own paging).

WheelDownPane is always passed through to avoid interfering with copy-mode
exit on scroll-down (tmux handles that transition internally).

Bindings are idempotent (same as the existing `set-option -g mouse on` block).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): make built-in-tools stub an explicit imperative (closes #854) (#1323)

Agents reading CLAUDE.md cold could miss the docs/built-in-tools.md
catalog when a user asks for email/calendar/etc. and answer "I don't
have that capability." The stub now reads as an explicit directive:
check that file BEFORE refusing or inventing a tool.


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* refactor(discord-bridge): route all marker handling through parse_markers() (#896) (#1302)

* refactor(discord-bridge): route all marker handling through parse_markers() (#896)

Replace hand-rolled startswith/regex marker detection with the unified
`parse_markers()` from `src/result_markers.py` at all four send paths:

1. poll_results skip guard: `[no-send]`/`[REPLIED]`/`[deduped:]` startswith
   trios → `any(a.kind == "skip")` via `_parsed.actions`.
2. poll_results channel redirect: `channel_pattern.search()` → `_parsed.actions`
   redirect action; `reply_text` already clean of the `[channel:]` literal.
3. poll_results file extraction: `_split_file_markers(reply_text)` →
   `[a.value for a in _parsed.actions if a.kind == "attach"]`.
4. _poll_proactive file/redirect: `_split_file_markers` + `_channel_redirect_re`
   regex → `parse_markers(text)` actions.
5. _poll_dm_fallback skip + redirect + file: all three hand-rolled sites
   collapsed into a single `parse_markers(_peek)` call.

`_split_file_markers` helper is retained (its unit tests still cover the
underlying `_FILE_MARKER_RE`); it is no longer called from the send paths.

Extend `tests/bridge-marker-no-leak.test.py` with discord-bridge guards:
imports `parse_markers`, calls it at least once, and contains no
`.startswith('[no-send]')` / `.startswith('[REPLIED]')` /
`.startswith('[deduped:')` literals — catching any regression back to
hand-rolled detection.

All 19 discord-bridge tests + the no-leak structural test pass.

Closes #896

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* fix(tests): update proactive-channel-redirect test for parse_markers refactor

Guard 1 checked for literal \d{17,20} in poll_proactive, but PR #896's
parse_markers() refactor moved the regex into result_markers.py. The test
still correctly guards that SOME channel parsing exists — it now accepts
either the old inline-regex approach or the new parse_markers() call.

Also applies the free-form-section fix from PR #1265 to
src/check-pending-questions.py (was still on old logic that required
explicit Status: markers).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): make built-in-tools stub an explicit imperative (closes #854) (#1323)

Agents reading CLAUDE.md cold could miss the docs/built-in-tools.md
catalog when a user asks for email/calendar/etc. and answer "I don't
have that capability." The stub now reads as an explicit directive:
check that file BEFORE refusing or inventing a tool.


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(slack-bridge): recover from external access.json deletion via in-memory cache (closes #899) (#1292)

* fix(slack-bridge): cache access.json in-memory to recover from external deletion (#899)

* test(slack-bridge-access): bump tofu_onboard regex budget 2000→4000

The cache-restore block added by PR #1292 (fix external access.json
deletion) pushes tofu_onboard + surrounding module-level variables past
the 2000-char \ndef boundary. Same pattern as the _write_task budget
bumps documented in the comment above it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* perf(slack-bridge): skip file-read in load_tier_map when _access_cache is warm (closes #894)

After the first task or tofu_onboard, _access_cache holds the full
access.json payload. load_tier_map() was re-reading the file on every
_write_task call even when the cache was fresh. Short-circuit to the
cache when populated — same data, zero extra file I/O.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* fix(slack-bridge): mtime-based cache invalidation in load_tier_map (#894 follow-up)

The aa39f00 perf fix skipped the file read whenever _access_cache was
non-None, but load_tier_map is called in tests that rewrite ACCESS_FILE
between calls without invalidating the cache. Stale cache caused
slack-bridge-tier-map.test.py to return the first call's tierMap for all
subsequent cases (absent tierMap, malformed JSON, null, file-deleted).

Fix: store ACCESS_FILE's mtime alongside the cache. load_tier_map hits
the cache only when the file's current mtime matches the cached mtime.
Re-reads on any mtime change or OSError (file deleted). Production
behavior is unchanged for the normal fast-path (no delete, no external
edit between events); tests that write new file content between calls
now see fresh data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(health-check): detect notes/ split-brain when repo and workspace dirs overlap (#1288)

* fix(health-check): detect notes/ split-brain when both repo and workspace dirs exist

Closes a diagnostic gap from #1266.

- New check_notes_split_brain() fires when <repo>/notes/ and <workspace>/notes/
  both contain .md files with overlapping names
- Only runs when the two paths are actually different (no false alarm on fresh installs
  where workspace == repo root)
- Lists overlapping filenames in the warning, directs user to sutando-migrate.sh
- Appended to run_all_checks() immediately after the existing notes-dir check

* test(health-check): 8 tests for check_notes_split_brain() (closes #1266 test gap)

Covers the new check from PR #1288:
- same-path (repo == workspace) → None (no false alarm on fresh installs)
- workspace/repo notes dir absent → None
- both exist, no filename overlap → None
- overlap → warn dict with name/status/detail
- detail includes overlap count, first-3 filenames, "… and N more" tail for >3
- detail directs user to sutando-migrate.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* feat(morning-briefing): daily briefing without gws dependency (#1282)

* feat(morning-briefing): add daily briefing script without gws dependency

Builds a working morning briefing from locally-available sources:
- Weather via Open-Meteo API (free, no key)
- macOS Calendar and Reminders via AppleScript
- Overnight Discord DMs from bridge log
- Pending questions from pending-questions.md
- System health (failures only, not expected warns)

Updates crons.example.json to call the script directly (the previous
prompt_skill approach invoked the morning-briefing SKILL.md which ran
gws commands that fail on machines without gws CLI installed).

Sentinel at state/morning-briefing-{date}.sentinel prevents re-delivery;
use --force to re-run on the same day.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* feat(morning-briefing): add daily insight + clean voice output

- Calls daily-insight.py (or reads cached sentinel) and includes the
  behavioral insight as a closing line in the briefing
- Strips raw Python dict notation from insight text (voice-unfriendly)
  by taking only the first sentence and filtering lines with { or
  multiple colons
- Channel mix dict ("{'Other': 1}") no longer appears in spoken output

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* fix(morning-briefing): skip untitled events, filter by calendar, dedup cross-calendar (closes #967, #964, #966)

Three calendar quality improvements:
- #967: skip events where AppleScript returns an empty title (empty
  summary field) — these are usually auto-created holds, not real
  meetings.
- #964: filter by MORNING_BRIEFING_SKIP_CALENDARS env var (comma-
  separated calendar names) for users with subscribed shared calendars
  they don't want in the briefing.
- #966: dedup by (time, title) key across calendars — prevents the same
  meeting appearing twice when it's mirrored in multiple calendars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(health-check): detect discord LoginFailure and slack no-events in bridge checks (#1269)

* fix(health-check): detect discord LoginFailure and slack no-events in bridge checks

Previously both discord-bridge and slack-bridge showed "ok / running"
even when they were functionally broken:
- discord-bridge: crashes on every start with LoginFailure when the token
  is revoked, but health-check only checked pgrep (process exists) so
  the dashboard showed green while the bridge was in a crash loop
- slack-bridge: Socket Mode WebSocket connects but events never arrive
  when Event Subscriptions is disabled in the Slack app config;
  health-check showed "ok / running" with no hint about the config gap

Add Check 6 (log-content health) after the existing dead-inode check:
- discord-bridge: scan last 60 lines for LoginFailure/Improper token →
  status=fail with actionable message
- slack-bridge: scan for "60s elapsed with zero events" hint → status=warn
  with link to Event Subscriptions settings (only overrides "ok", not
  stale/dead-inode which are already more specific)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* fix(health-check): skip restart for discord LoginFailure in --fix path

When discord-bridge reports fail/LoginFailure, the --fix loop was
spawning a new process via subprocess.Popen alongside the existing
launchd-managed one — resulting in two concurrent bridge processes.
Restarting doesn't help when the token is invalid; the right action
is to regenerate the token.

Skip the restart and print an actionable message instead. The launchd
service continues its own restart cycle and will recover automatically
once the token is updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(agent-api): check results/archive/ in GET /result/<id> endpoint (#1268)

* fix(agent-api): check results/archive/ in GET /result/<id> endpoint

task-bridge archives result files within seconds of delivering them to
voice/Discord/Slack. The background polling loop in agent-api already
checked the archive, but the direct GET /result/<id> endpoint only
looked in the live results/ dir — returning 404 for any result that
arrived after archiving.

This adds the same archive fallback to get_task_result(): sorted
month-dir glob (newest first) matching the background-loop pattern,
so web-client polls consistently find their results regardless of timing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(health-check): downgrade voice-transport code=1006 to warn when DNS resolves (#1267)

* fix(health-check): downgrade voice-transport code=1006 to warn when DNS resolves

A code=1006 WebSocket close is an abnormal network drop — often a transient
DNS blip rather than a persistent Gemini outage. When DNS resolves at probe
time the transport will self-recover on the next client connect, so showing
`fail` on the dashboard is misleading and alarming.

This adds a DNS probe for `generativelanguage.googleapis.com` when code=1006
is seen unrecovered:
- DNS resolves → status=warn, "transient network drop … will recover"
- DNS fails    → status=fail, "network drop code=1006 and DNS still failing"

All other abnormal codes keep the existing `fail` behaviour unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* fix(health-check): auto-kickstart voice-agent when stuck CONNECTING >10min

Bodhi's transport re-enters CONNECTING after a code=1006 close but can
wedge there indefinitely — the 60s idle teardown fires but the transport
bounces back to CONNECTING rather than reaching ACTIVE. The previous pass
only downgraded the severity to warn, relying on self-recovery that
doesn't always happen.

Detection: count [Health] state=CONNECTING lines after the unrecovered
close (each fires every ~30s from bodhi's health ticker). >20 consecutive
lines = stuck >10 minutes. At that point bodhi won't self-recover and
needs a kickstart.

When detected: status=fail with elapsed estimate in detail.
With --fix: calls fix_launchd("com.sutando.voice-agent") which runs
launchctl kickstart -k gui/<uid>/com.sutando.voice-agent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(bridges): archive stranded .claimed-core-N.txt task files (closes #933) (#1299)

* fix(bridges): archive stranded .claimed-core-N.txt task files (closes #933)

Root cause: bridges called archive_file(TASKS_DIR / f"{task_id}.txt", ...)
but claim_task.py (#884) renames task-{id}.txt → task-{id}.claimed-core-N.txt
at dispatch time. The bare path no longer exists, so archive_file silently
no-ops, leaving 52+ .claimed-core-*.txt files in tasks/ on busy hosts.
Side effect: health-check reports false "39 tasks queued" warnings.

Fix:
- New src/task_archive.py: find_task_file(tasks_dir, task_id) returns the
  bare path (unclaimed) or first glob match for .claimed-core-*.txt (claimed).
- All 3 bridges (telegram, slack, discord) updated to use find_task_file()
  instead of hard-coding the bare path.
- scripts/sweep-stranded-claims.sh: one-shot historical cleanup for existing
  stranded files — move all tasks/*.claimed-core-*.txt to tasks/archive/YYYY-MM/

5 tests: bare returned, claimed returned when bare missing, bare preferred,
None on no file, multiple claimed returns first lexicographic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* fix(tests): allowlist task_archive.py in state-paths-adoption check

task_archive.py is a pure locator helper — the flagged archive_file()
call is in the module docstring (example usage), not runnable code. The
_is_comment_line guard only catches opening triple-quote lines, not
interior docstring content, so it fires as a false positive.

Fixes CI on PR #1299 (fix/stranded-claim-files).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(slack-bridge): recover orphan .sending files on startup (follow-up to #1046) (#1290)

* fix(slack-bridge): recover orphan .sending files on startup (closes gap from #1046)

* test(slack-bridge): 7 tests for _recover_orphan_sending_files() startup fix

Covers the orphan-recovery invariants from PR #1290:
- empty results dir / missing dir → 0
- single .sending → renamed to .txt, content preserved
- multiple .sending → all recovered
- collision (.txt already exists) → .sending left untouched
- non-proactive-*.sending files → ignored
- mixed files → only proactive-*.sending recovered

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stand: Echo Act IV Pro

* docs(changelog): add CHANGELOG.md for v0.1.0 + CHANGELOG-PENDING template (#1328)

CHANGELOG.md has a curated v0.1.0 section covering the headline items
since project start: workspace contract, messaging bridges (Telegram,
Discord, Slack, Phone), task pipeline, conversation store, reliability,
quota tracker, and developer-experience CI gates.

CHANGELOG-PENDING.md is the contributor drop-point for unreleased items
between now and v0.2.0.

Part of the v0.1.0 release prep (issue #957).


Stand: Echo Act IV Pro

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(quota-tracker): narrow bare-except to ImportError (closes #1062) (#1094)

The except Exception block on read-quota.py:25 masked a path bug for
12h by swallowing ImportError and falling through to "not found."
Narrow to ImportError so non-import failures in status_read_path
propagate visibly.

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

* fix(watch-tasks): close both orphan-leak paths (EPIPE buffer + PPID reparent) (#1093)

* fix(watch-tasks): close both orphan-leak paths (EPIPE buffer + PPID reparent)

Two fixes for #1088:

Mode A (EPIPE-on-dead-reader): replace echo with printf || exit 0 so
the first failed write to a dead consumer pipe exits immediately
instead of silently buffering into the kernel pipe buffer (~100
events on macOS before the watcher notices).

Mode B (PPID=1 reparent): add EXIT/INT/TERM trap that kills the
process group (kill 0). When the parent shell exits, the trap fires
and tears down fswatch instead of leaving it orphaned under launchd.

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

* fix(watch-tasks): add HUP to trap signal list

Per sonichi's review — if launchd ever sends SIGHUP to the watcher,
fswatch would orphan without this.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* perf(discord-voice): hoist mkdirSync + switch appendFileSync to createWriteStream (#1286)

* perf(discord-voice): hoist mkdirSync + switch appendFileSync to createWriteStream

Closes #1053

- Hoist mkdirSync for DISCORD_VOICE_LOG dir to module-init alongside existing
  DATA_DIR/RESULTS_DIR/TASKS_DIR creates — eliminates syscall on every log line
- Replace appendFileSync with a persistent WriteStream opened once at startup;
  write via stream.write() (non-blocking) instead of blocking appendFileSync
- Stream errors set _opLogStream to null → auto-degrades to stdout-only (same
  fail-soft semantics as before, now without blocking the event loop)
- appendFileSync kept in imports: still used by appendConversationLog (low-freq)

* test(discord-voice): 11 structural tests for log-stream refactor (#1053)

Adapted from sonichi/sutando#1196's structural test. The fallback-to-
appendFileSync check is intentionally omitted — Chi's #1286 (cherry-
picked above) chose fail-soft (`_opLogStream?.write(...)`, silent no-op
on null) over explicit appendFileSync fallback. Both designs fix the
perf issue; this test validates Chi's chosen shape.

Verifies the structural invariants of the refactor:
- createWriteStream imported, _opLogStream declared at module level
- mkdirSync(dirname(DISCORD_VOICE_LOG)) hoisted to module init
- appendOperationalLog hot path: no mkdirSync, uses _opLogStream
- Stream error handler registers null-on-error (graceful degradation)
- Existing mkdirSync calls for DATA/RESULTS/TASKS dirs preserved

Original perf-fix attribution stays on Chi's commit; this test commit
adds the verification harness so future regressions are caught in CI.

Co-Authored-By: Lucy-Studio-Susan <noreply@anthropic.com>

---------

Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>
Co-authored-by: Lucy-Studio-Susan <noreply@anthropic.com>

* fix(inline-tools): gate slideControlTool/fullscreenTool on presenter-mode sentinel (#1320)

* fix(inline-tools): gate slideControlTool/fullscreenTool on presenter-mode sentinel (#1171)

Registering these tools globally causes Gemini to fire slide_control and
fullscreen on unrelated utterances (greetings, casual questions). Gate
exposure via existsSync on the presenter-mode.sentinel file so they only
appear in the tool registry when a presentation session is actually active.
Both inlineTools and ownerOnlyTools arrays updated with the same conditional.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(inline-tools): 7 structural tests for #1171 presenter-mode gate

Verifies the structural invariants of Chi's fix (cherry-picked above):
- _presenterActive declared at module load via existsSync(join(WORKSPACE_DIR, 'state', 'presenter-mode.sentinel'))
- inlineTools spreads ...(_presenterActive ? [slideControlTool, fullscreenTool] : [])
- ownerOnlyTools spreads same
- no bare slideControlTool/fullscreenTool references outside the gated spread
- #1171 issue reference present in gate comment
- _presenterActive declared before inlineTools array literal

Adapted from Bassil's #1256 test but rewritten to match Chi's chosen shape
(`_presenterActive` private const + inline join, no exported PRESENTER_SENTINEL
constant). 7/7 tests pass against current branch HEAD.

Co-Authored-By: Lucy-Studio-Susan <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(bridges): single-instance guard via fcntl.flock (closes #1257) (#1317)

* fix(bridges): single-instance guard via fcntl.flock (closes #1257)

Duplicate bridge instances caused real failures (2026-05-xx incident):
  - telegram-bridge: API 409 errors from racing getUpdates pollers
  - discord-bridge: same task delivered twice via dm-fallback

Root cause: startup.sh pgrep guard and Sutando.app launchd agent have
a race window — whichever launches second doesn't see the first in time.

Fix: new `src/single_instance.py` helper acquires an OS-level exclusive
flock on `$WORKSPACE/state/locks/<name>.lock` at each bridge's entry
point. LOCK_NB means contention exits immediately (exit 0) rather than
blocking, so launchd KeepAlive doesn't restart-loop. Lock auto-releases
on process death (OS closes all FDs).

Each bridge calls acquire() once:
  - telegram-bridge: top of main()
  - slack-bridge: top of main()
  - discord-bridge: daemon path only (not the `send` subcommand)

Lock file stores the holding PID for debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(single-instance): 4 behavioral tests for acquire() lock semantics

Covers: (a) first acquire writes PID, (b) second process exits 0,
(c) lock releases after holder dies, (d) different names are independent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(quota-tracker): burn-rate EWMA + passes-left forecast (closes #1087) (#1319)

* feat(quota-tracker): burn-rate EWMA + passes-left forecast (closes #1087)

Adds predictive exhaustion tracking to read-quota.py so the proactive
loop can tier on "will I run dry before reset at current rate?" rather
than only current remaining %.

Implementation:
  - After each read, loads state/quota-burn-history.json and computes the
    per-5min utilization delta against the last reading (gap 2min–2h only;
    outside that window the delta is too stale or duplicate).
  - Maintains a running EWMA (alpha=0.3, ~3-sample half-life) of deltas
    normalised to a "per 5-min pass" rate.
  - Skips negative deltas (5h reset happened — would corrupt the rate).
  - Reports burn rate + estimated passes/minutes left once ≥2 samples
    exist (first pass seeds history, second pass produces the first
    estimate, which is then smoothed on subsequent calls).
  - History is written atomically (tmp → rename) to avoid corrupt reads.

Output (human-readable, when ≥2 samples exist):
  Burn rate: 2.1%/pass (5 samples)
  Est. passes left: 37.6 (~188m)

Output (--json): `burn` key added to result object.
No change to --gate behaviour.

Also narrows `except Exception → except ImportError` on the
workspace_default import (closes #1062, previously separate PR).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(quota-tracker): 11 tests for burn-rate EWMA (closes #1087 test gap)

Covers _load_burn_history, _save_burn_history, and _update_burn_rate:
- missing / corrupt history file → {}; save+load roundtrip
- first call → None (no EWMA yet)
- gap < MIN_GAP_S (2 min) → sample skipped
- valid gap, 1 sample → None (needs ≥2)
- 2 valid samples → forecast dict with all expected keys
- gap > MAX_GAP_S (2 h) → sample skipped
- decreasing utilization (quota reset) → sample skipped
- EWMA smoothing formula verified (alpha=0.3, flat rate → stable ewma)
- estimated_minutes_left ≈ passes_left × 5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* test(ci): lint for PEP-604 union annotations without __future__ (closes #961 option-A) (#1305)

Adds `tests/python39-union-annotations.test.py` — an AST-based gate
that fails when any `src/*.py` file uses `X | Y` union annotation
syntax without `from __future__ import annotations`.

Root cause of the #960 incident: telegram-bridge.py used
`task_id: str | None = None` without the future import. On Python 3.9
(still the stock macOS `/usr/bin/python3`) that raises TypeError on
import, silently killing the bridge.

Detection uses `ast.walk` to find `BinOp(BitOr)` nodes in annotation
context (function params, return types, variable annotations). String
literals, regex patterns, and bitwise OR in expressions are not
annotation nodes and don't trip the gate.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ci): host-CLI dependency snapshot + migrate 2 hardcoded ~/.claude/ paths (closes #864) (#1324)

Adds tests/host-cli-dependency-snapshot.test.py — static-analysis gate
that catches new hardcoded Path.home()/'.claude' constructions outside
the canonical claudeHomePath() / claude_home_path() helpers.

Also migrates the 2 violations the test discovered:
- src/voice-agent.ts: bootstrapMemoryDir() now uses claudeHomePath()
- src/voice-context.ts: defaultMemoryDir() now uses claudeHomePath()

Both migrated paths now respect $CLAUDE_HOME override (useful for
test isolation and alt-host installs), which the previous homedir()
pattern silently ignored.

5 known pre-existing violations (health-check, bridges, dm-result)
are allowlisted in the test — to be migrated in a follow-up.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* feat(open-sutando-ref): fuzzy GitHub-ref resolver skill (#903) (#1308)

* feat(open-sutando-ref): fuzzy GitHub-ref resolver skill (#903)

Resolves natural-language and numeric GitHub references to URLs
without requiring the user to know exact PR/issue numbers. Handles
`#874`, `PR 874`, `issue 874`, and free-text fuzzy queries via
`gh pr/issue list --search`. Auto-detects PR vs issue for bare
numbers. Supports `--open` to navigate in the default browser.
Repo defaults to `gh repo view` output, overridable via
`SUTANDO_GH_REPO`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(open-sutando-ref): 16 unit tests for parse_ref() + resolve_numeric()

Pure-function tests that lock in parsing behaviour without network or
gh CLI. Covers numeric (#874, 874), PR (PR 874, pull request 874, PR#874),
issue (issue 874, issue #874), and fuzzy-description forms. Also pins
the URL format from resolve_numeric().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* docs(CLAUDE.md): add Slack access control section mirroring Discord (closes #895) (#1293)

Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* Add /email-find skill for stubborn email lookups (#1020)

* Add /email-find skill for stubborn email lookups

When an obvious Gmail search fails, the model used to confidently
report "no email found" — even when the owner knew one existed.
This skill codifies the playbook that actually works on this account:
broad-before-narrow scans, partner-domain fanout (Soulilution for K12,
etc.), and re-walking long threads that get_thread truncates.

Lesson learned today (2026-05-22): K12-related replies ride on the
`Re: Fwd: Highland Fleets - Contact Data` thread from data.ops@
soulilution.com — no K12 token in the subject, sender isn't k12-data.com.
Two cycles of "I can't find it" before owner-cited subject fragment
finally surfaced the email.

Key behavioral rule embedded in the skill: do NOT give up after one
or two failed queries when the owner has high confidence. Run all four
phases, then ask for clarification — never sooner.

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

* De-personalize email-find skill for cross-user value

Owner feedback: the v1 skill hardcoded vasiliy@ag2.ai in the broad-scan
query and shipped a partner-domain map full of one user's specific
customers (K12, Soulilution, Nokia, DNAnexus). That's user-specific
context that doesn't belong in the skill repo.

Changes:
- Replace `to:vasiliy@ag2.ai` with `to:me` (Gmail's reserved keyword
  for the authenticated user — works for every Gmail account).
- Replace the hardcoded partner-domain table with an instruction to
  read it from per-user memory (suggested file:
  `$SUTANDO_MEMORY_DIR/reference_partner_domains.md`).
- Document the file format inside the skill so users / agents know
  what to write.
- Add "offer to create the file" behavior when 2+ mappings are
  discovered in-session.
- Generalize all examples (Topic-X instead of K12, Acme Corp / Foo
  Foundation as illustrative entries).
- Update docs/built-in-tools.md pointer accordingly.

Behavioral rules unchanged: 4-phase workflow, broad-before-narrow,
don't-give-up-after-2-failed-queries, re-walk truncated threads.

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

* email-find: gracefully degrade + offer setup when partner-domains file is absent

Owner pointed out that v2 of the skill implicitly assumed users would
discover the $SUTANDO_MEMORY_DIR/reference_partner_domains.md
convention on their own. That's a footgun for first-time users.

Fixes:
- Phase 2 now explicitly says "skip if no partner-domain file exists"
  rather than implying it's required.
- New "## Where the file lives" subsection spells out the lookup order:
  env var → default path → file patterns to accept → degrade if absent.
- New "## First-time setup flow" describes the user-prompted "want me
  to save this mapping for next time?" interaction, with a concrete
  example of the offer text.
- Removed the duplicate "offer to create it" sentence at the bottom
  of File Format.

Net behavior: the skill works on a fresh install with zero setup
(Phases 1, 3, 4 do the work). After 2+ discoveries it offers to
persist the map so future invocations get the Phase 2 acceleration.

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

* email-find: silent auto-learn + timestamped rows + 365-day prune

Owner feedback 2026-05-22: "don't make user do the work — don't ask
for confirmation, but learn. ... Worst case scenario you will do extra
search. Make sure to add timestamps to memory ... use it to prune
memories — delete what was not useful for a year."

Changes:
- Replaced "First-time setup flow (offered, not required)" with
  "Auto-learn on discovery (no user confirmation)" — the skill now
  silently writes new mappings, never prompts.
- File format gains two timestamp columns: first_seen + last_useful.
- New "Timestamps and pruning" section: drop rows whose last_useful
  is >365 days old at next read; pruning runs at most once per day
  via a pruned_at header.
- Updated Phase 2 to reference the new auto-learn section.

The principle is broader than email-find — also saved as a feedback
memory (feedback_silent_auto_learn.md in owner's memory dir) so it
applies to any future skill that discovers facts about the user's
environment.

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

* email-find: tighten skill doc for long-term maintainability

Remove iteration-related cruft and redundant restatements:
- Replace owner-specific K12/soulilution example with generic
  Acme/data-ops phrasing.
- Drop the "29+ messages" specific count from the truncation rule —
  generalize to "long threads."
- Collapse "Where the file lives" 4-step numbered list to one
  paragraph; the resolution rule is the same in any number of words.
- Merge "Auto-learn on discovery" paragraph + "Implementation notes"
  bullet list — they were saying the same thing twice.
- Replace literal session-date timestamps in the file-format example
  with YYYY-MM-DD placeholders.
- Drop the Don't section's restatement of behavioral rules; keep the
  three items that aren't already covered by the numbered rules.

138 lines, down from 159. Same behavior, less cruft.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>

* fix(self-diagnose,catchup): workspace-first read for build_log + pending-questions (#1264)

gather.sh: build_log.md and pending-questions.md moved to workspace on
2026-05-18 (#762) but the reads were still hardcoded to $REPO. On any
host where $REPO/build_log.md doesn't exist (migrated installs), self-
diagnose snapshots silently went empty. Fix: resolve workspace into $WS,
prefer $WS/$file, fall back to $REPO.

catchup-after-startup.sh: inverted priority — checked $REPO/build_log.md
first, only falling through to $WS when missing. Swapped to workspace-
first to match the contract (build_log.md is a workspace file, not repo).

Addresses items 3+4 from Lucy's post-migration aud…
bassilkhilo-ag2 pushed a commit to bassilkhilo-ag2/sutando that referenced this pull request Jun 8, 2026
Covers all 7 functions: load_calls (5 — no file, empty, valid JSONL, invalid
line skipped, blank lines skipped), parse_ts (6 — Z/+00:00 suffixes, empty,
None, non-date, result type), mask_phone (6 — 11-digit US, unknown, None,
short, 10-digit branch, digits-only), filter_by_window (6 — None window,
recent/old/no-timestamp, timestamp fallback, boundary), compute_stats (9 —
empty, total, duration avg/longest/shortest, zero-duration excluded, meetings,
owner_calls, peak_hour, top_purposes/callers), format_text (7 — no-duration
fallback, avg shown, unknown purposes/callers skipped, meetings shown, window
label), main (5 — no file→stderr, --json, --all, --days N, default 7-day).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bassilkhilo-ag2 pushed a commit to bassilkhilo-ag2/sutando that referenced this pull request Jun 8, 2026
Covers all 7 functions: load_calls (5 — no file, empty, valid JSONL, invalid
line skipped, blank lines skipped), parse_ts (6 — Z/+00:00 suffixes, empty,
None, non-date, result type), mask_phone (6 — 11-digit US, unknown, None,
short, 10-digit branch, digits-only), filter_by_window (6 — None window,
recent/old/no-timestamp, timestamp fallback, boundary), compute_stats (9 —
empty, total, duration avg/longest/shortest, zero-duration excluded, meetings,
owner_calls, peak_hour, top_purposes/callers), format_text (7 — no-duration
fallback, avg shown, unknown purposes/callers skipped, meetings shown, window
label), main (5 — no file→stderr, --json, --all, --days N, default 7-day).

Co-Authored-By: Claude Sonnet 4.6 <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.

refactor(quota-tracker): narrow read-quota.py:26 bare-except to ImportError

6 participants