Repository navigation
fix(workspace): migrate build_log.md from repo root to $SUTANDO_WORKSPACE - #859
Conversation
…PACE build_log.md is per-user mutable runtime state and belongs in the workspace per the contract in CLAUDE.md. Pre-fix state was split-brain: - dashboard.py + health-check.py already read from WORKSPACE_DIR/build_log.md - voice-context.ts + sync-memory.sh wrote to REPO/build_log.md - self-diagnose's gather.sh used build_log.md as a marker to LOCATE the repo Changes: - src/voice-context.ts:43 → reads from resolveWorkspace()/build_log.md - src/init.sh:66 → stop seeding at repo (workspace_default.py owns seeding) - skills/self-diagnose/scripts/gather*.sh → swap repo-marker from build_log.md to CLAUDE.md (stable, identity-bearing) - src/workspace_default.py → add _migrate_inrepo_build_log() symmetric to _migrate_inrepo_notes(); fires from resolve_workspace() in both env-set and env-unset paths; sentinel-gated for O(1) re-entry Separate follow-up (not in this PR): - ~/.sutando/memory-sync/scripts/sync-memory.sh MACHINE_FILES path (private repo, separate commit) Tests: 18 init-script tests + 25 workspace-default tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
left a comment
There was a problem hiding this comment.
LGTM — comprehensive, symmetric, well-scoped. This is the systemic version of the manual mv+symlink I did on Mini earlier today.
Verified each change:
-
skills/self-diagnose/scripts/gather*.sh— marker swap build_log.md → CLAUDE.md is the right secondary fix. CLAUDE.md is identity-bearing + stable + always-present in a sutando checkout; build_log.md was a fragile choice that broke the moment it moved. Comment cite makes the rationale clear. -
src/init.sh— dropping the build_log.md seeding at repo root is correct. Comment explicitly cites the workspace contract + names the consumers (workspace_default.py + dashboard + health-check) so the next reader sees why it's not there. -
src/voice-context.ts— voice-context build-log read migrated from REPO_DIR to WORKSPACE_DIR. This was one of the two unmigrated TS readers (Mini and dashboard already on WORKSPACE_DIR via different paths). Closes the divergence completely. -
src/workspace_default.py:_migrate_inrepo_build_log— clean implementation:- Sentinel-gated (
.build_log-migrated) for O(1) re-entry on subsequent calls - Non-destructive on collision (skips if workspace target already exists)
- Fires in both env-set and env-unset paths — right call, the migration needs to happen regardless of where workspace is resolved
- Same shape as
_migrate_inrepo_notes— easy mental model for the next contributor
- Sentinel-gated (
-
Tests updated to the new contract — init-script test now asserts NOT-created at repo root rather than testing the old seeding behavior. Test owns the contract change explicitly.
One non-blocking nit: the new _migrate_inrepo_build_log should ideally bump _migrate_from_legacy's _LEGACY_DIRS recognition. If a legacy install also has build_log.md at the repo root AND env is unset AND new default doesn't yet exist, the _migrate_from_legacy runs first → creates the new default with tasks/results/state/notes → then _migrate_inrepo_build_log runs against the NOW-existing target → skips (collision). End result is still right (build_log stays where it is for a moment), but the file gets stranded. Worth a follow-up test case OR an ordering tweak in resolve_workspace. Not blocking — current behavior is correct for the common path.
CI green (SUCCESS on the visible check). MERGEABLE. Ship — Chi's offline and asked Mini to drive autonomously.
… build_log) (#861) Adds a test verifying the migration ordering Mini flagged as non-blocking in #859 review: - Legacy install (repo has tasks/results/state with content) - $SUTANDO_WORKSPACE unset - New default doesn't exist - build_log.md at repo root Expected: _migrate_from_legacy runs first (moves dirs), then _migrate_inrepo_build_log runs and migrates the file. Mini's concern was that step 2 might skip due to "collision" with the newly-created target dir. This test pins the actual behavior — collision check is against the FILE workspace/build_log.md not the workspace DIR, so migration proceeds cleanly. Verified manually before adding the test (Mini's nit is non-issue), but worth permanent coverage so the door stays sealed. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the workspace-vs-repo path mismatch that surfaced as false-positive
health-check warnings (voice-agent.log "not found" at /workspace/logs while
the actual file was written to /repo/logs by startup.sh).
Behavior change:
- Add $WORKSPACE resolution at top of service-start block, mirroring
src/workspace_default.py / .ts: env override `$SUTANDO_WORKSPACE`,
fallback to `~/.sutando/workspace/`.
- All 8 service log redirections (voice-agent, web-client, dashboard,
agent-api, screen-capture, telegram-bridge, discord-bridge,
slack-bridge) now write to `"$LOGS_DIR/$name.log"` instead of the
CWD-relative `logs/$name.log`.
- mkdir -p $WORKSPACE/{logs,tasks,results,data} so the redirections
don't fail on a fresh install.
- Diagnostic-failure echo also points at $LOGS_DIR.
- Legacy repo-root `tasks/ results/ data/` mkdir kept for now —
back-compat with any script that still resolves CWD-relative paths.
Why this matters: health-check.py + dashboard already look at
$WORKSPACE/logs (per PR #821/#859 workspace-design contract). Without
this change they ran on a stale subset of services every cycle and
emitted false "log not found" warnings that masked real issues.
No code changes inside the services — they emit to stdout/stderr and
startup.sh redirects, so this is a one-file shell-only diff. The
services never opened log files directly, which is why this didn't
need to ride along with #821's TS migration.
Tested: smoke-checked resolution snippet — `$LOGS_DIR` resolves to
`/Users/wangchi/.sutando/workspace/logs` (existing dir with current
logs from prior bridges). `bash -n` clean.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n first run Closes the gap Susan flagged in PR #913 review: after #911 (workspace-aware service logs), every install pre-dating that PR keeps two copies of runtime state — stale repo-root `logs/`, `state/`, `pending-questions.md`, `core-status.json` alongside the new workspace versions. git status pollution + future debugging confusion. This adds a one-time migration function modeled on `workspace_default.py::_migrate_from_legacy` (PR #762): - Fires only when `\$WORKSPACE/.legacy-migrated-911` sentinel is absent - Scans `\$REPO/` for evidence (any of logs/state/tasks/results/notes/data non-empty); skips entirely on fresh installs - Moves each runtime dir + file `\$REPO/X` → `\$WORKSPACE/X` ONLY when the workspace destination doesn't already exist (non-destructive on collision) - Writes sentinel after the sweep; second run sees it and bails (zero log noise on every subsequent startup) - Stderr line per moved item so users can audit what happened Files moved: dirs `logs/`, `state/`, `tasks/`, `results/`, `notes/`, `data/`; files `pending-questions.md`, `core-status.json`, `contextual-chips.json`, `voice-state.json`, `build_log.md`. Matches the set #911 + earlier #821/#859 contract migrated. Smoke-tested locally with a synthetic test repo + workspace: - First run: migrated 4 fake stale files (logs/, state/, notes/, core-status.json) from /tmp/sutando-migration-test → /tmp/test-workspace with one stderr line each. Sentinel written. - Second run: silent (sentinel present, bails immediately). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oot (#913) * feat(init.sh): create runtime dirs/files under \$WORKSPACE, not repo root Closes the breadcrumb loop Mini flagged in #911 review: startup.sh now redirects service logs to \$WORKSPACE/logs/, but init.sh was still mkdir-ing repo-root logs/state/tasks/results/notes/data/ + writing pending-questions.md, contextual-chips.json, core-status.json, voice-state.json into the repo. So even with #911 merged, every startup.sh run re-created stale repo-root breadcrumbs. This PR makes init.sh workspace-aware: - Resolve \$WORKSPACE at top (same shape as startup.sh, workspace_default.py) - create_file_if_missing / create_dir_if_missing now write under \$WORKSPACE - copy_if_missing kept repo-rooted — it's used today only for skills/schedule-crons/crons.json which lives in the repo by design Smoke-tested: `bash src/init.sh --auto` creates the previously-missing \$WORKSPACE/data/ + \$WORKSPACE/pending-questions.md cleanly. Existing workspace files left alone (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(init.sh): migrate stale repo-root runtime state to \$WORKSPACE on first run Closes the gap Susan flagged in PR #913 review: after #911 (workspace-aware service logs), every install pre-dating that PR keeps two copies of runtime state — stale repo-root `logs/`, `state/`, `pending-questions.md`, `core-status.json` alongside the new workspace versions. git status pollution + future debugging confusion. This adds a one-time migration function modeled on `workspace_default.py::_migrate_from_legacy` (PR #762): - Fires only when `\$WORKSPACE/.legacy-migrated-911` sentinel is absent - Scans `\$REPO/` for evidence (any of logs/state/tasks/results/notes/data non-empty); skips entirely on fresh installs - Moves each runtime dir + file `\$REPO/X` → `\$WORKSPACE/X` ONLY when the workspace destination doesn't already exist (non-destructive on collision) - Writes sentinel after the sweep; second run sees it and bails (zero log noise on every subsequent startup) - Stderr line per moved item so users can audit what happened Files moved: dirs `logs/`, `state/`, `tasks/`, `results/`, `notes/`, `data/`; files `pending-questions.md`, `core-status.json`, `contextual-chips.json`, `voice-state.json`, `build_log.md`. Matches the set #911 + earlier #821/#859 contract migrated. Smoke-tested locally with a synthetic test repo + workspace: - First run: migrated 4 fake stale files (logs/, state/, notes/, core-status.json) from /tmp/sutando-migration-test → /tmp/test-workspace with one stderr line each. Sentinel written. - Second run: silent (sentinel present, bails immediately). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PACE (sonichi#859) build_log.md is per-user mutable runtime state and belongs in the workspace per the contract in CLAUDE.md. Pre-fix state was split-brain: - dashboard.py + health-check.py already read from WORKSPACE_DIR/build_log.md - voice-context.ts + sync-memory.sh wrote to REPO/build_log.md - self-diagnose's gather.sh used build_log.md as a marker to LOCATE the repo Changes: - src/voice-context.ts:43 → reads from resolveWorkspace()/build_log.md - src/init.sh:66 → stop seeding at repo (workspace_default.py owns seeding) - skills/self-diagnose/scripts/gather*.sh → swap repo-marker from build_log.md to CLAUDE.md (stable, identity-bearing) - src/workspace_default.py → add _migrate_inrepo_build_log() symmetric to _migrate_inrepo_notes(); fires from resolve_workspace() in both env-set and env-unset paths; sentinel-gated for O(1) re-entry Separate follow-up (not in this PR): - ~/.sutando/memory-sync/scripts/sync-memory.sh MACHINE_FILES path (private repo, separate commit) Tests: 18 init-script tests + 25 workspace-default tests pass. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…unset + build_log) (sonichi#861) Adds a test verifying the migration ordering Mini flagged as non-blocking in sonichi#859 review: - Legacy install (repo has tasks/results/state with content) - $SUTANDO_WORKSPACE unset - New default doesn't exist - build_log.md at repo root Expected: _migrate_from_legacy runs first (moves dirs), then _migrate_inrepo_build_log runs and migrates the file. Mini's concern was that step 2 might skip due to "collision" with the newly-created target dir. This test pins the actual behavior — collision check is against the FILE workspace/build_log.md not the workspace DIR, so migration proceeds cleanly. Verified manually before adding the test (Mini's nit is non-issue), but worth permanent coverage so the door stays sealed. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the workspace-vs-repo path mismatch that surfaced as false-positive
health-check warnings (voice-agent.log "not found" at /workspace/logs while
the actual file was written to /repo/logs by startup.sh).
Behavior change:
- Add $WORKSPACE resolution at top of service-start block, mirroring
src/workspace_default.py / .ts: env override `$SUTANDO_WORKSPACE`,
fallback to `~/.sutando/workspace/`.
- All 8 service log redirections (voice-agent, web-client, dashboard,
agent-api, screen-capture, telegram-bridge, discord-bridge,
slack-bridge) now write to `"$LOGS_DIR/$name.log"` instead of the
CWD-relative `logs/$name.log`.
- mkdir -p $WORKSPACE/{logs,tasks,results,data} so the redirections
don't fail on a fresh install.
- Diagnostic-failure echo also points at $LOGS_DIR.
- Legacy repo-root `tasks/ results/ data/` mkdir kept for now —
back-compat with any script that still resolves CWD-relative paths.
Why this matters: health-check.py + dashboard already look at
$WORKSPACE/logs (per PR sonichi#821/sonichi#859 workspace-design contract). Without
this change they ran on a stale subset of services every cycle and
emitted false "log not found" warnings that masked real issues.
No code changes inside the services — they emit to stdout/stderr and
startup.sh redirects, so this is a one-file shell-only diff. The
services never opened log files directly, which is why this didn't
need to ride along with sonichi#821's TS migration.
Tested: smoke-checked resolution snippet — `$LOGS_DIR` resolves to
`/Users/wangchi/.sutando/workspace/logs` (existing dir with current
logs from prior bridges). `bash -n` clean.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oot (sonichi#913) * feat(init.sh): create runtime dirs/files under \$WORKSPACE, not repo root Closes the breadcrumb loop Mini flagged in sonichi#911 review: startup.sh now redirects service logs to \$WORKSPACE/logs/, but init.sh was still mkdir-ing repo-root logs/state/tasks/results/notes/data/ + writing pending-questions.md, contextual-chips.json, core-status.json, voice-state.json into the repo. So even with sonichi#911 merged, every startup.sh run re-created stale repo-root breadcrumbs. This PR makes init.sh workspace-aware: - Resolve \$WORKSPACE at top (same shape as startup.sh, workspace_default.py) - create_file_if_missing / create_dir_if_missing now write under \$WORKSPACE - copy_if_missing kept repo-rooted — it's used today only for skills/schedule-crons/crons.json which lives in the repo by design Smoke-tested: `bash src/init.sh --auto` creates the previously-missing \$WORKSPACE/data/ + \$WORKSPACE/pending-questions.md cleanly. Existing workspace files left alone (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(init.sh): migrate stale repo-root runtime state to \$WORKSPACE on first run Closes the gap Susan flagged in PR sonichi#913 review: after sonichi#911 (workspace-aware service logs), every install pre-dating that PR keeps two copies of runtime state — stale repo-root `logs/`, `state/`, `pending-questions.md`, `core-status.json` alongside the new workspace versions. git status pollution + future debugging confusion. This adds a one-time migration function modeled on `workspace_default.py::_migrate_from_legacy` (PR sonichi#762): - Fires only when `\$WORKSPACE/.legacy-migrated-911` sentinel is absent - Scans `\$REPO/` for evidence (any of logs/state/tasks/results/notes/data non-empty); skips entirely on fresh installs - Moves each runtime dir + file `\$REPO/X` → `\$WORKSPACE/X` ONLY when the workspace destination doesn't already exist (non-destructive on collision) - Writes sentinel after the sweep; second run sees it and bails (zero log noise on every subsequent startup) - Stderr line per moved item so users can audit what happened Files moved: dirs `logs/`, `state/`, `tasks/`, `results/`, `notes/`, `data/`; files `pending-questions.md`, `core-status.json`, `contextual-chips.json`, `voice-state.json`, `build_log.md`. Matches the set sonichi#911 + earlier sonichi#821/sonichi#859 contract migrated. Smoke-tested locally with a synthetic test repo + workspace: - First run: migrated 4 fake stale files (logs/, state/, notes/, core-status.json) from /tmp/sutando-migration-test → /tmp/test-workspace with one stderr line each. Sentinel written. - Second run: silent (sentinel present, bails immediately). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…solvers (closes #863) Guards against re-introducing the workspace-vs-repo split-brain bug class fixed in PR family #815-#859. Any future `process.cwd()` in TS code or `Path.cwd()`/`os.getcwd()` in Python outside the resolver modules now fails CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…solvers (closes #863) (#1322) Guards against re-introducing the workspace-vs-repo split-brain bug class fixed in PR family #815-#859. Any future `process.cwd()` in TS code or `Path.cwd()`/`os.getcwd()` in Python outside the resolver modules now fails CI. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>
Summary
build_log.mdis per-user mutable runtime state — belongs in$SUTANDO_WORKSPACEper the contract inCLAUDE.md, not the repo root.dashboard.py+health-check.pyalready read fromWORKSPACE_DIR/build_log.md(missing on Chi's machine), whilevoice-context.ts+sync-memory.sh+init.shstill wrote to repo root.skills/self-diagnose/scripts/gather*.shusedbuild_log.mdas a marker file to LOCATE the repo, so a simplemvwould have broken self-diagnose. Marker swapped toCLAUDE.md(stable, identity-bearing, unambiguous).Changes
src/voice-context.ts:43resolveWorkspace()/build_log.md(wasREPO_DIR)src/init.sh:66build_log.mdat repo root (workspace_default.py owns seeding now)skills/self-diagnose/scripts/gather.shbuild_log.md→CLAUDE.mdskills/self-diagnose/scripts/gather-remote.shsrc/workspace_default.py_migrate_inrepo_build_log()— symmetric to_migrate_inrepo_notes; sentinel-gated; runs fromresolve_workspace()on both env-set + env-unset pathstests/init-script.test.tstests/workspace-default.test.pyOut of scope (intentional)
~/.sutando/memory-sync/scripts/sync-memory.shMACHINE_FILES path — private repo, separate commit.init.shcleanup (logs/,state/,tasks/,results/are also seeded at REPO and should arguably move to workspace) — separate work to keep this PR small + reviewable.*.mdfiles — comments only, no behavior; will pick up on next pass.Test plan
python3 tests/workspace-default.test.py— 25/25 passnpx tsx --test tests/init-script.test.ts— 18/18 passnpx tsc --noEmitcleanresolve_workspace()auto-migrates Chi's local repo'sbuild_log.mdto~/.sutando/workspace/build_log.mdon next Python service start🤖 Generated with Claude Code