Skip to content

feat(workspace): loud-warn when fallback masks a .env override - #1369

Merged
qingyun-wu merged 3 commits into
mainfrom
feat/loud-warn-workspace-fallback
May 31, 2026
Merged

qingyun-wu merged 3 commits into
mainfrom
feat/loud-warn-workspace-fallback

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Summary

When a process resolves the workspace without SUTANDO_WORKSPACE in its env but .env defines the var, the fallback silently lands in ~/.sutando/workspace/ while the rest of the fleet uses the override. Two services with the same .env then split-brain — one writes to ~/.sutando/workspace/state/..., another reads from .env-specified state/.... Invisible: no error, just empty/stale state. (This is exactly the bug class that produced #1367 in startup.sh and #1368 in skills/agent-registry/.)

This PR adds a one-line stderr warning that fires once per process when the fallback masks a .env override. Resolution behavior does NOT change — callers who genuinely want the default still get it (no .env override → no warn → fallback as before).

What changes

Applied to all three workspace resolvers — kept in parity:

  • src/workspace_default.py — adds _grep_env_for_workspace() helper + warn block in resolve_workspace().
  • src/workspace_default.ts — adds grepEnvForWorkspace() helper + warn block in resolveWorkspace().
  • src/init.sh — inline grep/cut/sed of $REPO/.env + warn block in the workspace-resolution branch.

The warn message looks like:

workspace: SUTANDO_WORKSPACE is unset in process env, falling back to /Users/me/.sutando/workspace.
NOTE: .env declares SUTANDO_WORKSPACE='/Users/me/Documents/sutando-workspace' which is NOT being honored
here — source .env or export the var before this process to avoid split-brain with other services.

Behavior matrix:

env var .env has override result warn?
set (any) use env value no
unset yes, different from default use default YES
unset no, or matches default use default no
unset no .env found use default no

Why "warn-only" (not auto-honor)

skills/agent-registry/scripts/_workspace_resolve.py (#1368) is Option B — it auto-honors the .env value when the env var is missing. That's the right behavior for an out-of-band hook (SessionStart) where there's no realistic way to source .env first.

The src/ resolvers are different: they run inside processes that could have sourced .env themselves. Auto-honoring would mask the upstream bug ("why didn't the bootstrap source .env?"). Warn-only forces the upstream fix while keeping the existing fallback semantics.

Tier 1A of the prevention plan

Owner greenlit four tiers earlier today:

  • Tier 1A ← this PR: loud-warn on fallback (lowest cost, ~99% catch for this class)
  • Tier 1B: clean-shell CI smoke test — separate PR, follow-up
  • Tier 2: unified scripts/source-env.sh helper + per-script env manifest
  • Tier 3: drop default fallback entirely (long-term; breaks first-time installers)

Test plan

Verified locally across 6 scenarios — all pass:

  • env set / PY → no warn ✅
  • env set / TS → no warn ✅
  • env set / SH → no warn ✅
  • env unset + .env override / PY → WARN ✅
  • env unset + .env override / TS → WARN ✅
  • env unset + .env override / SH → WARN ✅

Reviewers can repro by env -i HOME=/tmp/x PATH=$PATH bash -c "python3 -c 'from src.workspace_default import resolve_workspace; resolve_workspace(migrate=False)'" from a checkout with .env containing SUTANDO_WORKSPACE=....

Related

🤖 Generated with Claude Code

When a process resolves the workspace without `SUTANDO_WORKSPACE` in
its env but `.env` defines the var, the fallback silently lands in
`~/.sutando/workspace/` while the rest of the fleet uses the override.
Two services with the same `.env` then split-brain — one writes to
`~/.sutando/workspace/state/agent-registry.json`, another reads from
`.env`-specified `state/agent-registry.json`. The bug is invisible:
no error, just empty/stale state.

Adds a one-line stderr warning that fires once per process when the
fallback masks a .env override. The actual resolution behavior does
NOT change — callers who genuinely want the default still get it
(no .env override → no warn → fallback as before).

Applied to all three workspace resolvers:
- `src/workspace_default.py` (`resolve_workspace`)
- `src/workspace_default.ts` (`resolveWorkspace`)
- `src/init.sh` (inline `WORKSPACE=...` block)

The Python and TS resolvers add a small `_grep_env_for_workspace()`
helper that walks up from the module file to find `.env` and reads
the `SUTANDO_WORKSPACE=` line (best-effort, never raises). init.sh
uses inline `grep | cut | sed` against `$REPO/.env`. Behavior is
parity across all three.

Companion to:
- #1367 (startup.sh ordering — moved .env source before init.sh)
- #1368 (agent-registry — auto-honors .env when env var missing,
  Option B; this PR is Option C "loud warn" from the discussion)

Tier 1A of the prevention plan owner greenlit:
  Tier 1A: loud-warn (this PR)
  Tier 1B: clean-shell CI smoke test (separate PR, follow-up)
  Tier 2:  unified `scripts/source-env.sh` helper + manifest (later)
  Tier 3:  drop default fallback entirely (long-term)

Verified locally across 6 scenarios:
- env var set → no warn (PY/TS/SH)
- env unset + .env override → WARN fires (PY/TS/SH)

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

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

Review — feat(workspace) PR #1369

BLOCKING

None.

NON-BLOCKING

  • 3-file parity (py + ts + sh) — same helper now lives in workspace_default.py, workspace_default.ts, and inline in init.sh. Future edits must update all three or the warning conditions drift. A shared spec in docs/workspace-contract.md (or a comment header in one of the three pointing at the others) would lower the drift risk. Could also be captured by Tier 1B (clean-shell CI smoke).
  • init.sh uses head -1 so multiple SUTANDO_WORKSPACE= lines silently take the first match. Matches the python+ts behavior (also stop at first match within the first .env found). Document this as intentional, or make it loud if mismatch detected — but as written it's consistent and that consistency is what matters.
  • TS replace(/^~/, homedir()) only handles leading bare ~, not ~user form. Matches what init.sh does (${val/#\~/$HOME}) and the python Path(val).expanduser() (also bare ~ only). Consistent across the trio. Fine.
  • Warn-only vs auto-honor decision is well-justified in the PR body — src/ runs inside processes that could source .env themselves, so warn forces the upstream fix; _workspace_resolve.py in agent-registry is the auto-honor path for the SessionStart hook where sourcing isn't realistic. Clean split.

The _FALLBACK_WARN_PRINTED module-level guard (py) / _fallbackWarnPrinted (ts) ensures one warning per process. Looks right.

— Maddy

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Thanks Maddy — all four points heard.

On the 3-file parity (py + ts + sh): agree the drift risk is real and worth tracking. I can either:

  • (a) File a follow-up issue ("3-file parity contract on workspace_default helpers") so future edits stay aligned.
  • (b) Address in this PR with a 2-line "see also" header pointing the three files at each other.

Lean toward (b) if owner doesn't object — cheapest mitigation, lives at the site that needs to be touched together. Standing by for the call.

The other three (head -1, ~ form, warn-only split) are all consistency observations matching the trio's existing behavior — no change planned.

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu
qingyun-wu merged commit 7b3a318 into main May 31, 2026
3 checks passed
@qingyun-wu
qingyun-wu deleted the feat/loud-warn-workspace-fallback branch May 31, 2026 21:43
john-the-dev added a commit to john-the-dev/sutando that referenced this pull request Jun 11, 2026
…kspace_default

Part of issue sonichi#1378 sub-item 1 (workspace-resolution dedup). Replaces the
local 5-line copy of resolve_workspace() with the canonical import from
src/workspace_default.py — the same helper health-check.py, dashboard.py,
dm-result.py, and others already use.

Benefits: a rule change (e.g. the loud-warn-on-fallback added in sonichi#1369) now
applies to obsidian-mirror automatically; only one place to maintain.

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.

2 participants