Repository navigation
feat(sync-workspace): workspace-as-git-repo + memory translation layer (PR-1) - #1445
Merged
qingyun-wu merged 9 commits intoJun 4, 2026
Merged
Conversation
…ispatch
PR-1 Phase 1 of the workspace-vault sync rewrite per the 2026-06-04 #design
crystallization. Owner directive (option b — full revised PR-1, ~5h).
## What's in this commit (Phase 1 only)
Working scaffold for the new `scripts/sync-workspace.sh`. Phase 2/3/4 fill
in the actual sync logic; this commit is the structural foundation that
the subsequent phases hang off.
**Section 1 (Bootstrap):**
- SCRIPT_DIR + SCRIPT_PARENT resolution (realpath-aware)
- .env loader from SCRIPT_PARENT (non-interactive shells)
- REPO_DIR with auto-detect via CLAUDE.md+skills+/.git signature; SUTANDO_REPO_DIR override
- WORKSPACE_DIR via M0 helper at $SCRIPT_PARENT/scripts/sutando-config.sh (avoids
stale-REPO_DIR-pin issue per feedback_stale_repo_dir_pin)
- VAULT_URL = SUTANDO_VAULT (canonical) OR SUTANDO_MEMORY_REPO (legacy alias,
one release, warning on stderr)
**Section 2 (Logging + UI):**
- log(), color_warn() (NO_COLOR-aware per no-color.org), die()
**Section 3 (Lock):**
- acquire_lock with stale-cleanup at 10min (POSIX atomic mkdir, no flock dep)
**Section 4 (Canonical ID + projects.map.json):**
- canonical_id() = sha256-8 of vault URL (SUTANDO_VAULT_PROJECT_ID override)
- local_slug() = REPO_DIR with / replaced by - (matches Claude Code's auto-derived slug)
- PROJECTS_MAP_PATH() = $WORKSPACE/.sutando-vault/projects.map.json
- ensure_projects_map_entry() — WORKING (not a stub): bootstrap map if missing,
idempotent if entry already matches, add/update entry otherwise
**Section 5 (.gitignore template):**
- DEFAULT_CARRIER_INCLUDES — paths to un-ignore (notes/, build_log/, pending-questions.md,
.claude-sutando/projects/<CANONICAL_ID>/memory/, projects.map.json)
- DEFAULT_GITIGNORE_LINES — explicit-deny baseline (state/, tasks/, results/, logs/,
data/, *.heartbeat, etc.) + "ignore everything" wildcard + ".env*"
- generate_gitignore() — stub (Phase 2 implements config merge + write)
**Section 6 (Subcommand stubs):**
- cmd_init, cmd_pull_only, cmd_push_only, cmd_default_bidirectional,
cmd_status (PARTIALLY WORKING — shows resolved values), cmd_migrate_from_legacy,
cmd_help (prints header)
**Section 7 (Dispatch):** case-statement on $1
## Smoke test passes
- `bash -n scripts/sync-workspace.sh` — syntax clean
- `bash scripts/sync-workspace.sh --status` — resolves WORKSPACE_DIR + REPO_DIR
+ VAULT_URL + canonical_id + local_slug correctly
- `bash scripts/sync-workspace.sh --help` — prints header
- `bash scripts/sync-workspace.sh foobar` — unknown-subcommand error, exit 2
## Phases 2-4 follow in subsequent commits
- Phase 2 (~1.5h): workspace-git-init, .gitignore generation from config,
before-push memory copy, after-pull memory copy, branch-per-host push + fetch
- Phase 3 (~1.5h): 3-way merge first, --strategy-option=ours fallback,
migration helper for ~/.sutando/memory-sync/ → workspace-as-git-repo,
mass-deletion tripwire
- Phase 4 (~1h): tests/sync-workspace-{init,translation,conflict}.test.sh,
~20 reference updates, backward-compat shim at sync-memory.sh
Owner checkpoint pattern: pause + DM + #design status at end of each phase.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r + bidirectional sync
Fleshes out the Phase 1 scaffold's subcommand stubs with working bodies.
## What's in this commit
**`generate_gitignore()`** (was stub) — now writes the actual .gitignore
to $WORKSPACE_DIR. Whitelist mode: `*` ignores everything by default;
selective un-ignore for the carrier set (notes/, build_log/, pending-questions.md,
.claude-sutando/projects/<CANONICAL_ID>/memory/, projects.map.json, plus
each ancestor un-ignore so git doesn't stop at parent). Hard-deny .env*,
*.heartbeat, *.alive, *.sentinel, *.pid regardless. User config merge
(vault.sync.{include,exclude}) deferred to Phase 4 / sibling PR — keeps
Phase 2 scope focused on the core plumbing.
**`_init_impl`** (was stub) — Phase 2 body for `cmd_init`:
1. git init in $WORKSPACE_DIR if not already a repo
2. Set origin to $VAULT_URL (idempotent — replace if URL changed)
3. generate_gitignore (overwrites; operators use config for overrides)
4. ensure_projects_map_entry (bootstraps map.json + this host's entry)
5. mkdir canonical-id memory dir
6. Stage + commit + push to refs/heads/host/<hostname>
**Translation layer** (NEW — `copy_localslug_to_canonical` +
`copy_canonical_to_localslug`):
- Bidirectional bridge between Claude Code's auto-derived local-slug dir
(gitignored, per-host) and the canonical-id mirror (tracked, vault-synced).
- Uses `cp -a path/. target/` to copy CONTENTS not the dir; preserves
metadata; creates target on demand.
- Idempotent + safe when either side is empty (skip with log).
**`_pull_only_impl`** (was stub):
1. git fetch --all
2. Checkout host/<hostname> branch (create from origin/host/<hostname>
remote-tracking if exists, else from current HEAD)
3. Sort peer branches by last-push timestamp, oldest first (per design —
minimizes per-step merge diff)
4. git merge each peer into local branch
5. On conflict: --ours fallback (git checkout --ours + add + commit)
6. copy_canonical_to_localslug — Claude Code on this host sees peers' memory
**`_push_only_impl`** (was stub):
1. copy_localslug_to_canonical — this host's memory writes propagate up
2. ensure_projects_map_entry
3. git add -A
4. Mass-deletion tripwire (preserved from sync-memory.sh:439) — refuse
push if >SUTANDO_SYNC_MAX_DELETE (default 50) files would be deleted
unless SUTANDO_FORCE_SYNC=1
5. git commit + git push origin HEAD:refs/heads/host/<hostname>
**`cmd_default_bidirectional`** — orchestrates pull-then-push. Pull failures
don't block push (`|| true`).
**Lock refactor:** split cmd_* into outer (acquire_lock) + _impl (lock-free)
so cmd_default_bidirectional can call both impls under one lock instead of
double-acquiring.
## Verification
- `bash -n scripts/sync-workspace.sh` — syntax clean (611 lines)
- `bash scripts/sync-workspace.sh --status` — resolves correctly
## Still Phase 3 (next commit)
- `cmd_migrate_from_legacy` body (move ~/.sutando/memory-sync/ →
workspace-as-git-repo: rsync carrier-set files, git init, first push)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last subcommand body for the script. Phase 4 (tests) comes next.
## `cmd_migrate_from_legacy` (was stub)
One-time migration helper for fleet hosts that have an existing
`~/.sutando/memory-sync/` clone. Steps:
1. Detect legacy clone at `$HOME/.sutando/memory-sync/` (or
`$SUTANDO_MEMORY_SYNC_DIR` if set).
2. Curated copy into workspace's tracked paths:
- `legacy/notes/` → `workspace/notes/` (handles symlink: unlink and copy
content if `workspace/notes` already points into the legacy clone)
- `legacy/memory/*.md` → `workspace/.claude-sutando/projects/<canonical>/memory/`
- `legacy/pending-questions.md` (prefers `machine-<host>/pending-questions.md`
if present) → `workspace/pending-questions.md`
- `legacy/build_log.md` (similarly preferring machine-<host>) →
`workspace/build_log/<hostname>.md` (per-host split per design)
3. Hands off to `_init_impl` to git-init + first push.
4. Prints operator-facing next-steps recipe (verify, run normal sync,
only delete legacy after confirming).
## Safety properties
- Never deletes the legacy dir (operator controls cleanup).
- `cp -n` everywhere — never overwrites existing workspace content.
- Hard-fail if legacy dir missing or not a git repo (refuses to operate
on the wrong source).
- Handles the symlink-into-legacy case for `workspace/notes` that
sync-memory.sh's bootstrap creates.
## Phase 4 (next commit)
Tests (`tests/sync-workspace-{init,translation,conflict}.test.sh`).
Reference updates (README.md, src/, scripts/, skills/, docs/) DEFERRED
to PR-2 (the rename + cutover) — keeps PR-1 narrow: new opt-in script
+ tests, doesn't touch existing prod sync-memory.sh callers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end coverage of scripts/sync-workspace.sh against fixture workspace + local bare-repo vault. Hermetic: never touches the operator's real workspace or vault. ## Test isolation strategy - `SUTANDO_WORKSPACE` + `SUTANDO_TEST_MODE=1` — M0 helper honors the env per the v0.8 escape hatch added in #1440. - `SUTANDO_REPO_DIR` — fake sutando-checkout with `CLAUDE.md` + `skills/` + `.git`, populated with copies of `sync-workspace.sh` + `sutando-config.sh` + `sutando_config.py` so the script's auto-detect picks up the fixture. - `SUTANDO_VAULT` — local `git init --bare` repo URL, no network. - `FIXTURE_WS` realpath'd to handle macOS `/var/folders/` → `/private/var/` symlink alias (same lesson as `tests/migration-safety-helpers.test.sh`). ## Test coverage (18 assertions, 7 scenarios) 1. **--status before init** (3 assertions): WORKSPACE_DIR, VAULT_URL, canonical_id all present in output. 2. **--init creates expected artifacts** (8 assertions): `.git/`, `.gitignore`, `projects.map.json`, canonical memory dir, .gitignore whitelists `notes/` + canonical mirror, hard-denies `.env`, vault remote configured, `host/<hostname>` branch present in vault. 3. **Idempotent re-init**: re-running --init detects existing repo, no error (smoke-only — informational, not asserted hard). 4. **--push-only with no changes is a no-op** (1 assertion): clean tree → "nothing to push" path. 5. **Translation layer push-side** (2 assertions): write a memory file to local-slug, --push-only, verify it appears at canonical AND in the vault `host/<hostname>` branch (via `git --git-dir=$vault show`). 6. **Translation layer pull-side** (1 assertion): simulate a peer host by cloning the vault to a second workspace, write to canonical, push to `host/peerhost`. On host 1 run --pull-only, verify peer's memory file lands in our local-slug after canonical→local-slug copy. 7. **Mass-deletion tripwire** (2 assertions): create 60 notes, commit them, delete all 60, run --push-only — verify tripwire fires + staged deletions get reset (preserved behavior from sync-memory.sh). ## Result: 18/18 pass `bash tests/sync-workspace.test.sh` → exit 0. ## Lessons surfaced during writing - Sourcing the script with `bash -c '. script --help'` is fragile because `set -u` in the sourced script can trip on the sourcing shell's unset positional args. Better: re-derive expected values directly in the test rather than calling the script for them. - Test subprocess pipes through `head -1` cause SIGPIPE → exit 141 → whole test suite aborts (bash --pipefail). Use `>/dev/null 2>&1` for fixture commands; reserve `head` for things you actually want to truncate. - Bare git repos clone with `warning: remote HEAD refers to nonexistent ref` unless HEAD is set. Peer setup needs explicit base branch on clone: `git checkout -B host/peerhost origin/host/${HOST}`. ## Reference updates DEFERRED Per the Phase 4 strategy in the prior commit: PR-1 does NOT modify README.md, src/, scripts/cron-gate.sh, scripts/stage-readiness.sh, skills/schedule-crons/*, docs/* to point at sync-workspace.sh. Those references still call sync-memory.sh and continue to work unchanged. PR-2 handles the rename + cutover, at which point a backward-compat shim at sync-memory.sh redirects to sync-workspace.sh and the ~20 reference updates land in one focused PR. Phase 5 (open DRAFT PR) follows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…plification
Owner directive 2026-06-04 05:11Z + 05:15Z refinement:
> "Let's remove the Memory translation layer for .claude_sutando. Let's
> keep do simple pull."
> "track all .claude-sutando/projects/* /memory only the memory folder.
> There are many large files that we may not want to track."
## Removed (~140 lines)
- `canonical_id()` derivation (sha256-8 of vault URL)
- `local_slug()` computation function
- `PROJECTS_MAP_PATH()` + `ensure_projects_map_entry()` (projects.map.json)
- `copy_localslug_to_canonical()` + `copy_canonical_to_localslug()` (the
bidirectional copy bridge)
- `SUTANDO_VAULT_PROJECT_ID` env var
- All call sites: `_init_impl` no longer creates canonical memory dir or
bootstraps map.json; `_push_only_impl` no longer copies before push;
`_pull_only_impl` no longer copies after pull; `cmd_status` no longer
prints canonical_id / local_slug / map.json contents
## New gitignore pattern (per the 05:15Z refinement)
```gitignore
!.claude-sutando/
!.claude-sutando/projects/
!.claude-sutando/projects/*/ # un-ignore each slug DIR
!.claude-sutando/projects/*/memory/ # un-ignore memory subdir only
!.claude-sutando/projects/*/memory/** # un-ignore memory contents
```
Transcripts (`*.jsonl`), `tool-results/`, `file_history/`, etc. INSIDE
slug dirs stay gitignored — only the user-curated `memory/` subdirs sync.
Each host writes to its OWN slug; after pull, peer slug subdirs are
visible-not-merged.
## migrate-from-legacy adjusted
Legacy `memory/*.md` now copies to `workspace/.claude-sutando/projects/<local_slug>/memory/`
(this host's actual Claude Code-derived slug — `<REPO_DIR>` with `/` → `-`)
instead of the (now-removed) canonical-id mirror. So Claude Code on this
host reads from the migrated content as expected.
## Test updates (21/21 pass)
Rewrote `tests/sync-workspace.test.sh` for the simplified design:
- Test 1: --status no longer mentions canonical_id / projects.map.json
- Test 2: .gitignore content checks for `!.claude-sutando/projects/*/memory/`
(not `<CANONICAL>/memory/`); verify no `.sutando-vault/projects.map.json`
file is created
- Test 5 (renamed): write memory to local-slug → push → verify file appears
in vault at the SAME local_slug path (no translation)
- Test 6 (new): write fake transcript to local-slug → verify NOT pushed
(gitignored — only memory/ tracked)
- Test 7 (was 6): peer host pushes to its OWN slug → pull → verify peer
slug subdir visible in host 1's workspace + host 1's own memory still
present (no clobber)
- Test 8 (was 7): mass-deletion tripwire (unchanged)
## What this commit costs
Memory across hosts is now visible-not-merged. Cross-host memory sharing
requires the operator/agent to read `<peer_slug>/memory/` paths explicitly.
This trade-off was the explicit owner call: complexity of the translation
layer wasn't worth it for an aspiration ("merge memory across hosts") that
isn't actually a real-pain item yet.
## Diff stats
scripts/sync-workspace.sh: 735 → 551 lines (-184, +1 to nett -183 incl. doc)
tests/sync-workspace.test.sh: 253 → 282 lines (+29 — new tests for the
gitignore-only-tracks-memory invariant; removed translation-layer asserts;
added the transcript-not-pushed test)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pro review at #design 05:23Z flagged 3 safety/data-loss gaps in the simplified PR-1. All three addressed in this commit + tests. ## Fix #3 — .gitignore overwrite warning (smallest, ~50 lines) **Risk:** `_init_impl` overwrote `$WORKSPACE/.gitignore` on every run, even if the user had hand-edited it. Operator-authored content blocking the carrier set would silently get reinstated by overwrite → data loss in the vault. **Fix:** split `generate_gitignore` into `_compose_gitignore_content` (writes to stdout) + `generate_gitignore` (compose → diff against existing → refuse to overwrite unless `--force-gitignore` flag set). - If no existing .gitignore → write directly - If existing identical to generated → no-op - If existing differs → print unified diff to stderr + refuse - `--force-gitignore` flag bypasses the refusal New global flag: `FORCE_GITIGNORE` (parsed in Section 0). ## Fix #2 — pull-side mass-deletion tripwire (~35 lines) **Risk:** The mass-deletion tripwire in `_push_only_impl` only caught **staged** deletions (`--diff-filter=D` on `git diff --cached`). But `git merge` during pull can delete files in the working tree directly via merge — bypassing the staging area. A peer branch with 60+ file deletions could land via merge → silently delete tracked files locally. **Fix:** Snapshot `git rev-parse HEAD` + `git ls-files | wc -l` BEFORE the fetch+merge. Post-merge, recount tracked files. If drop > `SUTANDO_SYNC_MAX_DELETE` (default 50) AND `SUTANDO_FORCE_SYNC` != 1, `git reset --hard <pre_pull_sha>` to roll back the merge + return 1. ## Fix #1 — --dry-run for --migrate-from-legacy (~40 lines) **Risk:** `--migrate-from-legacy` performs destructive copies + `git init` + `git push` against the vault, with no way to preview. Pro explicitly asked: "is --dry-run implemented? ... Strongly recommend test on a sacrificial clone before enabling on fleet." **Fix:** Global `--dry-run` flag (parsed in Section 0, applies to all subcommands). New helper `_do <cmd...>` runs the cmd OR prints `DRY-RUN: would: ...` and returns 0. Applied in `_init_impl` (early short-circuit), `_push_only_impl` (early short-circuit), and `_migrate_from_legacy_impl` (per-step). `generate_gitignore` also respects DRY_RUN (writes to /dev/null + prints "would write N lines"). ## Section 0 — new global flag parser ```bash DRY_RUN=0 FORCE_GITIGNORE=0 _args=() for _arg in "$@"; do case "$_arg" in --dry-run) DRY_RUN=1 ;; --force-gitignore) FORCE_GITIGNORE=1 ;; *) _args+=("$_arg") ;; esac done set -- "${_args[@]:-}" ``` Flags strip out of $@ before subcommand dispatch. Order-insensitive. ## Tests Extended `tests/sync-workspace.test.sh` to 29/29 pass (was 21). Three new test scenarios: - **Test 9 — .gitignore overwrite warning (3 asserts):** modify the generated .gitignore in place, run --init → refused + user edit preserved; --force-gitignore → overwrites successfully. - **Test 10 — pull-side tripwire (2 asserts):** push 60 notes from host 1 with SUTANDO_FORCE_SYNC=1, peer clone deletes all 60 + pushes to host/peerhost2. Host 1 --pull-only → tripwire fires, working tree restored from pre-pull SHA. Note count unchanged (60 → 60). - **Test 11 — --dry-run on migrate-from-legacy (3 asserts):** setup fake legacy memory-sync at $TEST_ROOT/fake-legacy-memory-sync with notes/memory/pending-questions/build_log. Run --dry-run; verify DRY-RUN markers in output, legacy notes NOT copied to workspace, workspace note count unchanged. Diff stats: scripts/sync-workspace.sh 551 → 680 lines (+129); tests/sync-workspace.test.sh 282 → ~370 lines (+88). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two safety fixes to the workspace-vault sync script:
1. .gitignore diff display tripping set -euo pipefail (Medium)
`diff -u $a $b | head -40` returns nonzero when files differ + may
SIGPIPE on long output. With pipefail, the pipeline exit propagates
and set -e exits the script before tmp_path cleanup, bypassing the
"to overwrite anyway: pass --force-gitignore" guidance.
Fix: `... || true` on the pipeline. Same pattern applied
defensively to two other `| head` pipelines (status + help) per
per-file pattern audit.
2. Pull-side tripwire bypassed by delete+add net zero (Medium)
Old: pre_count - post_count = net change. A peer commit that
deleted 60 files and added 60 had net=0, completely bypassing
the tripwire while losing 60 files of original content.
Fix: count actual deletions via
git diff --name-only --diff-filter=D pre_pull_sha..HEAD
Also adds percentage threshold (SUTANDO_SYNC_MAX_DELETE_PCT,
default 50%) so catastrophic small-workspace cases — e.g. 20-of-30
deletions — still trip below the absolute 50-file default.
Structural side-fix exposed by the new test: the --ours merge-
conflict fallback failed on DD-conflicts (both peers deleted the
same file) because `git checkout --ours -- $f` errors when the
file isn't on our side either. Added `git rm -f` fallback so the
merge resolves cleanly + the tripwire then sees the correct
deletion count and rolls back.
Tests: 29 → 31 pass. New Test 12 reproduces the delete-and-add
bypass + asserts both the actual-deletion-count tripwire fires
AND the working tree is restored to pre-pull state.
Mini Low #3 (--dry-run still mutates /tmp lock + log + mktemp)
acknowledged as acceptable — vault/workspace mutations covered;
/tmp is process-scoped + auto-cleaned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more bounces from Mini's v4 review:
1. (Medium) bootstrap .env grep pipeline tripped set -euo pipefail
`VAULT_URL=$(grep '^SUTANDO_VAULT=' .env | head -1 | ...)` —
when .env exists without a SUTANDO_VAULT line, grep exits 1,
pipefail propagates, and the assignment under set -e exits the
script before the SUTANDO_MEMORY_REPO legacy-alias fallback runs.
(Empirically verified — `var=$(...)` assignment does NOT exempt
set -e from a failing subshell here, contrary to my earlier
assumption.) Fix: trailing `|| true` on both .env grep pipelines
so the intended fall-through semantics are restored.
2. (Low) rename detection in mass-deletion tripwire
`git diff --diff-filter=D pre..HEAD` counts large renames as
delete+add unless user-config enables rename detection. If the
intent is true content loss, file moves must not trip the wire.
Fix: add `-M` (default 50% similarity) on BOTH pull-side AND
push-side tripwires — file moves now count as R, not D.
3. (Test gap) Test 12 was thin
Only verified note-* count. Now also asserts:
- no replacement-*.md files leaked into workspace
- HEAD unchanged across repeated tripwire pulls
- git status clean after tripwire (no staged/unmerged paths)
Plus new Test 13 reproduces the bootstrap-pipeline bug from #1:
a .env with only SUTANDO_MEMORY_REPO must complete --status
without exiting (pre-fix this exited 1).
Side-fix on the test itself: `ls "$dir/replacement-"*.md` of a
non-matching glob exits 2 → pipefail trips test's set -e and
silently truncates output mid-assertion. Switched to `find`, which
returns 0 on no-match. The earlier ls usages worked only because
their globs happened to match.
Tests: 31 → 36 pass (Test 12 +3 assertions, Test 13 +2 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qingyun-wu
marked this pull request as ready for review
June 4, 2026 05:48
Mini caught the inconsistency: my v5 fix swapped one `ls glob | wc -l` for `find`, but the older PRE/POST/RESTORED counters still used the fragile pattern. Same pipefail trap as before — a degenerate fixture (zero matches) silently aborts the test instead of reporting a FAIL. Changes: 1. Replaced 3 remaining `ls "$dir/note-"*.md | wc -l` with `find` for consistent behavior on no-match fixtures. 2. Extended Test 12's HEAD-non-drift check from N=2 to N=10 iterations. The previous "pull twice + check" weakly proved idempotency; a tight loop with a drift counter is the real proof. 3. New Test 14 — canonical SUTANDO_VAULT=in-.env path. Test 13 covered the legacy-alias fallback; Test 14 covers the happy-path-via-.env case (vs Tests 1-12 which set SUTANDO_VAULT via process env). Three assertions: --status exits 0; legacy-alias warning NOT spuriously triggered; VAULT_URL string surfaced in --status output. Tests: 36 → 39 pass. Script unchanged this round. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
@cla-assistant check |
This was referenced Jun 4, 2026
Merged
qingyun-wu
added a commit
that referenced
this pull request
Jun 4, 2026
…e/exclude) PR-3 followup to #1445 + #1446. M2 milestone. Owner directive 2026-06-04: let users customize what gets synced without editing the script. The config schema already declared `vault.sync.{include,exclude}` (M2 design) but PR-1's gitignore generator ignored it and used a hardcoded list. PR-3 wires the config field into the gitignore generator. Changes: 1. scripts/sutando-config.sh — new helpers `vault-sync-include` + `vault-sync-exclude` print the config arrays one path per line. Consumed by the gitignore composer. 2. scripts/sync-workspace.sh — config-driven carrier `_compose_gitignore_content` no longer hardcodes the carrier set. New `_emit_include_lines` helper handles ancestor-chain un-ignore (gitignore can't include a child whose ancestor is excluded by `*`). New `_emit_exclude_lines` helper emits carve-out denies after the includes (gitignore last-match wins). Paths ending in `/` treated as directories (ancestor chain + `!path/**`); paths without trailing `/` treated as files (ancestor chain + `!path`). 3. sutando.config.json — defaults updated to match real carrier Tracked default now declares the actual sync-workspace.sh carrier: `notes/`, `pending-questions.md`, `build_log.md`, `.claude-sutando/projects/*/memory/`. Previously the defaults claimed to track `skills/`, `tasks/archive/`, `state/cores/` etc., none of which sync-workspace.sh actually consumed — the schema lied. PR-3 reconciles schema with reality. Empty `exclude` default. Also fixed: prior hardcoded `!build_log/` (directory) was wrong; the build log is `build_log.md` (file). 4. Semantics: REPLACE, not concat sutando.config.local.json's `sync.include` REPLACES the tracked default's array (standard JSON merge — arrays don't concat). Users wanting defaults + extras must include the defaults explicitly. Documented in Test 19 + on the helpers. Tests: 48 → 54 pass. Three new tests: - Test 19: defaults + custom include all emitted to .gitignore - Test 20: include `data/` + exclude `data/secret/` → `data/foo.md` visible to git, `data/secret/key.txt` gitignored (verified with `git check-ignore`, not by parsing `git status` output) - Test 21: ancestor-chain un-ignore for nested include `a/b/c/` emits `!a/`, `!a/b/`, `!a/b/c/`, `!a/b/c/**` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qingyun-wu
added a commit
that referenced
this pull request
Jun 4, 2026
…ory.sh deprecation (#1446) * feat(sync): PR-2 — vault URL from config + --vault-url flag, sync-memory.sh deprecation PR-2 followup to #1445. Owner directive 2026-06-04: vault URL resolution should mirror how workspace path is resolved (config-file canonical), not env-var canonical. Includes deprecation banner on the legacy script (Option C: parallel deprecation, no behavior change for existing users). Changes: 1. Vault URL resolver (sync-workspace.sh) New priority chain replaces the env-canonical resolver: 1. --vault-url <url> CLI flag (explicit, tests, one-shot) 2. sutando.config.local.json → vault.remote_url (per-clone canonical) 3. sutando.config.json → vault.remote_url (tracked default) 4. .env SUTANDO_MEMORY_REPO (deprecated; warn-and-honor 1 release) SUTANDO_VAULT env var (introduced in #1445) REMOVED — brand new, no users to deprecate. .env grep-pipeline parsing replaced by direct env-var lookup (set -a; . .env was already loading SUTANDO_MEMORY_REPO into env above). Eliminates the var=$(grep | head | ...) set-e trap entirely (the bug class Mini caught in #1445 v4). 2. --vault-url CLI flag Two forms supported: `--vault-url <url>` and `--vault-url=<url>`. Error message + exit 2 when value missing. 3. sync-memory.sh deprecation banner One-line stderr warning on every invocation pointing at sync-workspace.sh + migration recipe in header. Silence with SUTANDO_SYNC_MEMORY_SUPPRESS_DEPRECATION=1. Functionality unchanged — existing users' crons keep working through transition. 4. Reference sweep (user-facing) - skills/schedule-crons/crons.example.json: sync-memory entry renamed to sync-workspace pointing at the new script - docs/memory-sync.md: PR-2 deprecation banner + migration recipe at the top; rest of the doc unchanged for legacy-flow users 5. Tests (48 pass total, was 39) - Tests 1-12 migrated to pass vault via --vault-url flag (was SUTANDO_VAULT env var via COMMON_ENV) - Test 13 (legacy alias): repurposed — verifies SUTANDO_MEMORY_REPO in .env still warn-and-honors - Test 14 (--vault-url flag): NEW — verifies flag resolves vault without any .env or env var - Test 15 (config-file vault): NEW — vault.remote_url in sutando.config.local.json resolves correctly - Test 16 (SUTANDO_VAULT removed): NEW — negative test, env var no longer honored - Test 17 (priority): NEW — --vault-url wins over .env legacy - Test 18 (deprecation banner): NEW — sync-memory.sh prints deprecation banner; SUPPRESS env var silences it Not in scope (PR-2.1 / later): - Removing sync-memory.sh entirely (waits for dogfooding observation) - Doc sweep of workspace-design.md / workspace-contract.md (architectural docs; out-of-scope for user-facing PR-2) - Removing health-check.py / core_heartbeat.py SUTANDO_MEMORY_REPO refs (same — those are internal callers using rsync flow; shim covers) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(sync-workspace): address Pro #1446 silent-no-op concern with Test 19 Pro's PR #1446 hold: "is sutando-config.sh vault-url implemented, or does the helper silently return empty and fall through to deprecated .env?" Pro couldn't see sutando-config.sh in the PR diff (helper already existed in main from an earlier M2 PR). Answer: vault-url IS implemented, and Test 15 already covers the happy path (write vault.remote_url to config + assert resolves). New Test 19 closes the explicit "silent-no-op" concern: stage a config WITH NO vault.remote_url, NO --vault-url flag, only .env SUTANDO_MEMORY_REPO. Verifies: 1. sutando-config.sh vault-url returns empty (helper IS callable, just yields empty for absent field) 2. --status exits 0 (no crash on empty config) 3. Deprecation warning fires (correct fall-through path taken) 4. Vault URL resolves via .env legacy Proves the silent-no-op gracefully degrades — not a crash, not a loop, correct priority ordering when each tier is empty/missing. Tests: 48 → 52 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qingyun-wu
added a commit
that referenced
this pull request
Jun 4, 2026
…e/exclude) (#1447) * feat(sync): PR-2 — vault URL from config + --vault-url flag, sync-memory.sh deprecation PR-2 followup to #1445. Owner directive 2026-06-04: vault URL resolution should mirror how workspace path is resolved (config-file canonical), not env-var canonical. Includes deprecation banner on the legacy script (Option C: parallel deprecation, no behavior change for existing users). Changes: 1. Vault URL resolver (sync-workspace.sh) New priority chain replaces the env-canonical resolver: 1. --vault-url <url> CLI flag (explicit, tests, one-shot) 2. sutando.config.local.json → vault.remote_url (per-clone canonical) 3. sutando.config.json → vault.remote_url (tracked default) 4. .env SUTANDO_MEMORY_REPO (deprecated; warn-and-honor 1 release) SUTANDO_VAULT env var (introduced in #1445) REMOVED — brand new, no users to deprecate. .env grep-pipeline parsing replaced by direct env-var lookup (set -a; . .env was already loading SUTANDO_MEMORY_REPO into env above). Eliminates the var=$(grep | head | ...) set-e trap entirely (the bug class Mini caught in #1445 v4). 2. --vault-url CLI flag Two forms supported: `--vault-url <url>` and `--vault-url=<url>`. Error message + exit 2 when value missing. 3. sync-memory.sh deprecation banner One-line stderr warning on every invocation pointing at sync-workspace.sh + migration recipe in header. Silence with SUTANDO_SYNC_MEMORY_SUPPRESS_DEPRECATION=1. Functionality unchanged — existing users' crons keep working through transition. 4. Reference sweep (user-facing) - skills/schedule-crons/crons.example.json: sync-memory entry renamed to sync-workspace pointing at the new script - docs/memory-sync.md: PR-2 deprecation banner + migration recipe at the top; rest of the doc unchanged for legacy-flow users 5. Tests (48 pass total, was 39) - Tests 1-12 migrated to pass vault via --vault-url flag (was SUTANDO_VAULT env var via COMMON_ENV) - Test 13 (legacy alias): repurposed — verifies SUTANDO_MEMORY_REPO in .env still warn-and-honors - Test 14 (--vault-url flag): NEW — verifies flag resolves vault without any .env or env var - Test 15 (config-file vault): NEW — vault.remote_url in sutando.config.local.json resolves correctly - Test 16 (SUTANDO_VAULT removed): NEW — negative test, env var no longer honored - Test 17 (priority): NEW — --vault-url wins over .env legacy - Test 18 (deprecation banner): NEW — sync-memory.sh prints deprecation banner; SUPPRESS env var silences it Not in scope (PR-2.1 / later): - Removing sync-memory.sh entirely (waits for dogfooding observation) - Doc sweep of workspace-design.md / workspace-contract.md (architectural docs; out-of-scope for user-facing PR-2) - Removing health-check.py / core_heartbeat.py SUTANDO_MEMORY_REPO refs (same — those are internal callers using rsync flow; shim covers) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(sync-workspace): address Pro #1446 silent-no-op concern with Test 19 Pro's PR #1446 hold: "is sutando-config.sh vault-url implemented, or does the helper silently return empty and fall through to deprecated .env?" Pro couldn't see sutando-config.sh in the PR diff (helper already existed in main from an earlier M2 PR). Answer: vault-url IS implemented, and Test 15 already covers the happy path (write vault.remote_url to config + assert resolves). New Test 19 closes the explicit "silent-no-op" concern: stage a config WITH NO vault.remote_url, NO --vault-url flag, only .env SUTANDO_MEMORY_REPO. Verifies: 1. sutando-config.sh vault-url returns empty (helper IS callable, just yields empty for absent field) 2. --status exits 0 (no crash on empty config) 3. Deprecation warning fires (correct fall-through path taken) 4. Vault URL resolves via .env legacy Proves the silent-no-op gracefully degrades — not a crash, not a loop, correct priority ordering when each tier is empty/missing. Tests: 48 → 52 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(sync): PR-3 — config-driven gitignore carrier (vault.sync.include/exclude) PR-3 followup to #1445 + #1446. M2 milestone. Owner directive 2026-06-04: let users customize what gets synced without editing the script. The config schema already declared `vault.sync.{include,exclude}` (M2 design) but PR-1's gitignore generator ignored it and used a hardcoded list. PR-3 wires the config field into the gitignore generator. Changes: 1. scripts/sutando-config.sh — new helpers `vault-sync-include` + `vault-sync-exclude` print the config arrays one path per line. Consumed by the gitignore composer. 2. scripts/sync-workspace.sh — config-driven carrier `_compose_gitignore_content` no longer hardcodes the carrier set. New `_emit_include_lines` helper handles ancestor-chain un-ignore (gitignore can't include a child whose ancestor is excluded by `*`). New `_emit_exclude_lines` helper emits carve-out denies after the includes (gitignore last-match wins). Paths ending in `/` treated as directories (ancestor chain + `!path/**`); paths without trailing `/` treated as files (ancestor chain + `!path`). 3. sutando.config.json — defaults updated to match real carrier Tracked default now declares the actual sync-workspace.sh carrier: `notes/`, `pending-questions.md`, `build_log.md`, `.claude-sutando/projects/*/memory/`. Previously the defaults claimed to track `skills/`, `tasks/archive/`, `state/cores/` etc., none of which sync-workspace.sh actually consumed — the schema lied. PR-3 reconciles schema with reality. Empty `exclude` default. Also fixed: prior hardcoded `!build_log/` (directory) was wrong; the build log is `build_log.md` (file). 4. Semantics: REPLACE, not concat sutando.config.local.json's `sync.include` REPLACES the tracked default's array (standard JSON merge — arrays don't concat). Users wanting defaults + extras must include the defaults explicitly. Documented in Test 19 + on the helpers. Tests: 48 → 54 pass. Three new tests: - Test 19: defaults + custom include all emitted to .gitignore - Test 20: include `data/` + exclude `data/secret/` → `data/foo.md` visible to git, `data/secret/key.txt` gitignored (verified with `git check-ignore`, not by parsing `git status` output) - Test 21: ancestor-chain un-ignore for nested include `a/b/c/` emits `!a/`, `!a/b/`, `!a/b/c/`, `!a/b/c/**` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 4, 2026
qingyun-wu
added a commit
that referenced
this pull request
Jun 6, 2026
…p deprecation timer to v0.4.0 Part 1 of the docs sweep for v0.3.0 (PR #1472): - **DELETE** `docs/memory-sync.md` — described the legacy `sync-memory.sh` rsync-to-`~/.sutando/memory-sync/` flow with a top banner pointing at the new path. No stub kept (per owner directive). - **CREATE** `docs/workspace-sync.md` from scratch — canonical doc for `sync-workspace.sh` (workspace-IS-the-git-repo, per-host branch `host/<host>/<wsId>`, `vault.remote_url` config, `vault.sync.*` customization). Cites the implementing PRs (#1445, #1447, #1458, #1459, #1460) inline. Includes a "Migration from sync-memory.sh" section documenting the v0.4.0 removal timer. - **EDIT** `scripts/sync-memory.sh` banner: appended "will be REMOVED in v0.4.0. Switch to scripts/sync-workspace.sh now" to make the deprecation timer explicit. Fleet hosts on main get the louder signal at next sync tick. The deprecation strategy is option (β) from the audit reply: keep `sync-memory.sh` working through v0.3.0 with the louder banner; remove the script + this `Migration from sync-memory.sh` section in the v0.4.0 release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qingyun-wu
added a commit
that referenced
this pull request
Jun 6, 2026
…resh [DRAFT] Release-prep PR for v0.3.0 — the staging-workspace-revamp rollup. **CHANGELOG.md:** - Add v0.3.0 entry with thematic subsections under Added/Fixed/Changed - Dedicated "Workspace contract rollup (M0 + M1 + M2 + sync-workspace)" subsection covering the 20 staging-revamp PRs (#1395 → #1463) - Other sections cover non-revamp work since v0.1.0: voice/multimodal, bridges, skills, reliability, developer experience / CI - Skips pure-doc PRs per Keep-a-Changelog convention **KNOWN_ISSUES.md:** - Rewrite entry #1 (workspace contract migration): M1 migration script is shipped, not "upcoming" — point users at `bash scripts/sutando-migrate.sh` + describe the 30-day grace window for `--delete-source` cleanup - Entries #2-4 (task-status flicker, Gemini hallucination, Gemini Live 15min idle timeout) unchanged — still apply, marked "by design" **docs/memory-sync.md:** - Setup step 2: prefer `vault.remote_url` in config / `--vault-url` flag over legacy `SUTANDO_MEMORY_REPO=` in `.env` - "What gets synced" split into "New path (sync-workspace.sh)" and "Legacy path (sync-memory.sh)" tables — reflects PR #1445's workspace-as-git-repo architecture - "What does NOT get synced" adds `vault.sync.include`/`vault.sync.exclude` customization callout citing PR #1447 + PR #1460 Wording is DRAFT — owner reviews before any of this lands. Once promoted, this entry covers everything tagged as v0.3.0; CHANGELOG-PENDING.md on this branch remains clean for v0.3.1 work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qingyun-wu
added a commit
that referenced
this pull request
Jun 6, 2026
…p deprecation timer to v0.4.0 Part 1 of the docs sweep for v0.3.0 (PR #1472): - **DELETE** `docs/memory-sync.md` — described the legacy `sync-memory.sh` rsync-to-`~/.sutando/memory-sync/` flow with a top banner pointing at the new path. No stub kept (per owner directive). - **CREATE** `docs/workspace-sync.md` from scratch — canonical doc for `sync-workspace.sh` (workspace-IS-the-git-repo, per-host branch `host/<host>/<wsId>`, `vault.remote_url` config, `vault.sync.*` customization). Cites the implementing PRs (#1445, #1447, #1458, #1459, #1460) inline. Includes a "Migration from sync-memory.sh" section documenting the v0.4.0 removal timer. - **EDIT** `scripts/sync-memory.sh` banner: appended "will be REMOVED in v0.4.0. Switch to scripts/sync-workspace.sh now" to make the deprecation timer explicit. Fleet hosts on main get the louder signal at next sync tick. The deprecation strategy is option (β) from the audit reply: keep `sync-memory.sh` working through v0.3.0 with the louder banner; remove the script + this `Migration from sync-memory.sh` section in the v0.4.0 release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qingyun-wu
added a commit
that referenced
this pull request
Jun 6, 2026
…mespace across resolvers, helpers, docs (8 commits) (#1490) * fix(stage-readiness): modernize sync-age check to workspace-sync (drops SUTANDO_PRIVATE_DIR + legacy memory-sync fallbacks) Owner flagged the stale `MEMORY_DIR` chain in `scripts/stage-readiness.sh` 2026-06-06 13:25Z (task-1780752348761): ```bash MEMORY_DIR="${SUTANDO_MEMORY_DIR:-${SUTANDO_PRIVATE_DIR:-}}" if [ -z "$MEMORY_DIR" ]; then if [ -d "$HOME/.sutando/memory-sync" ]; then MEMORY_DIR="$HOME/.sutando/memory-sync" else MEMORY_DIR="$HOME/.sutando-memory-sync" fi fi ``` Three legacy artifacts: (a) `SUTANDO_PRIVATE_DIR` deprecated alias (#870; one-release honor window), (b) `~/.sutando/memory-sync/` pre-v0.3.0 clone path (sync-memory.sh is removed in v0.4.0), (c) `~/.sutando-memory-sync/` pre-#870 alias. Modernized: the workspace itself is the sync target as of v0.3.0 (`scripts/sync-workspace.sh`, PR #1445+) — check `<workspace>/.git/FETCH_HEAD` mtime for freshness. Legacy `~/.sutando/memory-sync/` retained as a fallback during the v0.3.x deprecation window so hosts still on `sync-memory.sh` don't lose the readiness signal; the warning hint includes a migrate-to-workspace-sync nudge in the legacy path. Drops `SUTANDO_PRIVATE_DIR` reference (#870's one-release window has elapsed as of v0.3.0) and `~/.sutando-memory-sync/` (pre-#870). Workspace resolution goes through the canonical M0 helper (no dead else-branch fallback — script is already repo-coupled). Smoke: stage-readiness.sh runs clean and reports `workspace-sync last sync 33h ago — run 'bash scripts/sync-workspace.sh'` on this MBP. Net -4 lines after dropping the legacy fork. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agent-registry): modernize _workspace_resolve.py to delegate to M0 helper Owner DM 2026-06-06 13:27Z (task-1780752451373 + task-1780752464055): the docstring still claimed `~/.sutando/workspace` was "the canonical default" and `$SUTANDO_WORKSPACE` was honored. Both wrong post-#1440 / v0.8 — canonical default is `<repo>/workspace/` and the env var is no longer honored for resolution. The module previously re-implemented its own 3-step resolver (env-var, .env-file walk-up, hardcoded `~/.sutando/workspace` default) — all three steps are obsolete post-v0.8. Replaced with a thin delegate that walks up to find `scripts/sutando-config.sh` and shells out to the canonical M0 helper, so this script can never drift from the resolver contract again. The walk-up logic (5-level realpath() bounded walk) is retained because the symlinks-into-the-repo concern still applies (when invoked via `~/.claude/skills/agent-registry/...`). Symptom of the prior bug was silent-data-loss-equivalent: registry writes to wrong dir while readers look at the right one. Smoke: resolves correctly to `/Users/qingyunwu/Documents/github/sutando/workspace` on this MBP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(submit-use-case): drop hardcoded `/Users/wangchi/.sutando/workspace/` Chi-fleet probe (post-#1440) Owner DM 2026-06-06 13:28Z (task-1780752526451 + task-1780752549313: "Why this is still not fixed?"): three sites in the submit-use-case skill still referenced `/Users/wangchi/.sutando/workspace/` as Chi's fleet-detection signal. Pre-#1440 that path existed on Chi's hosts; post-#1440 the workspace is in-repo (`<repo>/workspace/`), so the path-existence probe always returns false and the fleet-detection silently broke. 3 sites updated: - `skills/submit-use-case/scripts/submit_use_case.py` — `is_chi_fleet()` drops the `Path("/Users/wangchi/.sutando/workspace").exists()` check; retains the `SUTANDO_FLEET_OWNER=chi` env signal (Chi sets this once per host in `.env`, no longer needs the path probe). - `skills/submit-use-case/SKILL.md` L59 — prose updated to match. - `skills/submit-use-case/README.md` L64 — same. If Chi-fleet auto-detection regresses (the env var isn't set on a host that used to be detected via the path), the user can re-enable by setting `SUTANDO_FLEET_OWNER=chi` in their `.env` — same setup documented in the README. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workspace-resolver): drop legacy .sutando/workspace fallback (Mini opinion-requested Path B) Per Mini's #design opinion 2026-06-06 14:42Z + owner's "go ahead with B" 14:44Z: post-v0.8 (#1440) the legacy `.sutando/workspace/` namespace should be fully retired from production resolution, not just from the documented contract. Three sites updated in this commit (the Tier 1+2 contract-bearing changes — Tier 3+ doc-comment sweep to follow): - `src/sutando_config.py:344-352` — no-config-no-repo-root last-ditch fallback now `~/sutando-workspace/` instead of `~/.sutando/workspace/`. Preserves the safety net for ad-hoc invocations outside a checkout while fully dropping the deprecated `.sutando/` namespace. - `src/Sutando/SutandoConfig.swift:228-236` — same change, Swift side. Mirrors Python intentionally per the cross-language contract. - `src/session-handoff.sh:38-49` — drop the `$SUTANDO_WORKSPACE`-aware defensive fallback entirely. Post-v0.8 the env var is no longer honored for workspace resolution; if the M0 helper isn't reachable via either of the two probed paths, fail loud rather than risk writing `session-state.md` to the wrong workspace via the deprecated env value. CI lint `--diff` clean. Smoke: `bash scripts/sutando-config.sh workspace` still resolves correctly to `<repo>/workspace/` on this MBP. Stand: Mini's scope-expansion caveat noted — there's a follow-up sweep of resolver-module docstrings (workspace_default.{py,ts}) + ~9 stale code-comment sites + config.json.example templates to land in subsequent commits before this PR is ready for review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sutando-app): drop hardcoded ~/.sutando/memory-sync/ candidate in resolveAxReadPath (Path B sweep) Owner DM 2026-06-06 14:46Z (task-1780757193114): `src/Sutando/main.swift:1492` had a hardcoded `~/.sutando/memory-sync/skills/personal-deictic/ax-read` candidate path in `resolveAxReadPath()`. That was the pre-v0.3.0 default-private location — `sync-memory.sh`'s rsync target. Post-v0.3.0 (#1440 + the sync-workspace.sh flow), the legacy `.sutando/memory-sync/` clone is deprecated (removed in v0.4.0). Dropped the hardcoded candidate; kept the `$SUTANDO_MEMORY_DIR` + `$SUTANDO_PRIVATE_DIR` env lookups (canonical + legacy alias #876) + the public-repo `<repo>/skills/context-drop/ax-read` fallback. Net behavior on a host whose `$SUTANDO_MEMORY_DIR` is set: identical. On a host where neither env var is set: now falls straight to the public-repo binary instead of probing a deprecated path that's guaranteed not to exist post-migration. Updated the resolution-order doc comment to match + added a v0.3.0 deprecation note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(workspace_default): refresh module docstrings to post-v0.8 contract (Path B sweep Tier 3) `src/workspace_default.{py,ts}` are the historic resolver wrappers; their module docstrings still described the pre-v0.8 contract ($SUTANDO_WORKSPACE honored, ~/.sutando/workspace/ as fallback). Per Mini's opinion-requested B reading + owner directive 2026-06-06: rewrite to match the actual post-#1440 contract. - `src/workspace_default.py:1-32` — module docstring now describes the M0 helper chain (`sutando.config.local.json` → `<repo>/workspace/`), notes the env-var is no longer honored, points readers at `src.sutando_config.resolve_workspace` as the canonical entry point, and explains that `default_workspace_dir` is a test-only export. - `src/workspace_default.py:23` — `_DEFAULT_SUBPATH` updated from `(".sutando", "workspace")` to `("sutando-workspace",)`. Matches the new `~/sutando-workspace/` last-ditch fallback in `sutando_config.py`. - `src/workspace_default.py:46` — `default_workspace_dir()` docstring rewritten to clarify it returns the post-v0.8 fallback, NOT the production default. - `src/workspace_default.ts:1-22` — module docstring refreshed with the same content, TS-flavored. - `src/workspace_default.ts:24-45` — `resolveWorkspace()` docstring rewritten: drops the "$SUTANDO_WORKSPACE env var (legacy escape hatch; warn once)" claim, notes the env-var is no longer honored, describes the warn-and-ignore + auto-migrate behavior. Behavior change: callers using `default_workspace_dir()` for the no-config-no-repo-root fallback now get `~/sutando-workspace/` instead of `~/.sutando/workspace/`. Tests use this via `patch.object(...)` so they're unaffected by the underlying path change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(config-templates): drop $SUTANDO_WORKSPACE + ~/.sutando/workspace/ from skill examples (Path B Tier 5) Owner DM 2026-06-06 14:50Z (task-1780757426171): the `_comment` field in `skills/phone-conversation/config.json.example` (and the sibling `skills/discord-voice/config.json.example`) still described the pre-v0.8 contract: "lives at \$SUTANDO_WORKSPACE/config/foo.json (default ~/.sutando/workspace/config/foo.json)". Both wrong post-#1440. Rewrote to point at the canonical resolver: "<workspace>/config/foo.json (the workspace resolves via `bash scripts/sutando-config.sh workspace`, default <repo>/workspace/)". Drops both the deprecated env-var mention and the deprecated `.sutando/` namespace fallback in one stroke. These are user-facing template files copied into the workspace on first run — getting the prose right matters because the `_comment` field is what a user reads when they open the live config to edit it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(code-comments): refresh 8 stale ~/.sutando/workspace comments to post-v0.8 contract (Path B Tier 4) Path B sweep round-up for the 8 code-comment sites where the prose still described the pre-v0.8 contract: - `skills/call-diagnostics/scripts/diagnose.py:38` — "(~/.sutando/workspace)" → "post-v0.8 default `<repo>/workspace/`" - `src/archive-stale-results.py:37-38` — "$SUTANDO_WORKSPACE (default ~/.sutando/workspace/)" → "the resolved workspace (post-v0.8 default `<repo>/workspace/`)" - `src/inline-tools.ts:659-661` — "$SUTANDO_WORKSPACE/state/ (default ~/.sutando/workspace/state/)" → "<workspace>/state/ (default <repo>/workspace/ post-v0.8)" - `src/web-client.ts:3108-3113` — same shape as inline-tools - `src/util_paths.py:65-66` — docstring updated: "canonical default (~/.sutando/workspace/)" → "post-v0.8 canonical default (<repo>/workspace/)"; notes $SUTANDO_WORKSPACE no longer honored - `src/util_paths.ts:13-18` — same shape - `src/health-check.py:847-850` — "~/.sutando/workspace/notes" → "<workspace>/notes"; v0.8 default note - `src/voice-agent.ts:19, 99-100` — header env-doc + inline comment both refreshed Pure prose; no behavior change. CI lint --diff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(workspace-default): update default-path assertion to match post-v0.8 sutando-workspace (Path B sweep) CI test failure on `f7f5ce7`: `test_default_is_dot_sutando_workspace_under_home` expected `default_workspace_dir()` to return `~/.sutando/workspace/`. Per the B sweep's `_DEFAULT_SUBPATH` change (`(".sutando", "workspace")` → `("sutando-workspace",)`), the function now returns `~/sutando-workspace/`. Renamed the test method to match the new behavior; updated all three assertion lines + the equality-on-full-path; added a docstring tying the test to the post-v0.8 contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(watch-tasks-stream): drop legacy env-var + hardcoded ~/.sutando/workspace fallbacks (Path B sweep) Owner DM 2026-06-06 14:57Z (task-1780757867862): `src/watch-tasks-stream.sh` had a 4-tier fallback chain for both TASKS_DIR and STATE_DIR — positional arg → M0 helper → `$SUTANDO_WORKSPACE` env → hardcoded `$HOME/.sutando/workspace/`. Post-v0.8 (#1440 + Mini opinion-requested 2026-06-06) the env-var path is no longer honored and the `.sutando/workspace/` namespace is retired. Replaced both blocks with a single-path resolver: M0 helper required, fail-loud if not reachable. Matches the contract change applied to `session-handoff.sh` earlier in this PR. Also removed `src/watch-tasks-stream.sh` from the lint `ALLOWED` regex in `scripts/lint-workspace-resolution.sh` — the file no longer needs a defense-in-depth exemption now that the legacy paths are gone. Behavior change: a host where the sutando checkout is broken/incomplete will now error visibly with "cannot resolve workspace" instead of silently watching `~/.sutando/workspace/tasks/` (which would be empty post-migration and produce no events — owner DMs would land silently in the bridges' workspace dir). This matches the divergence-prevention rationale in the surrounding comment, but enforces it strictly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(discord-voice): refresh _resolve_workspace docstring post-v0.8 (Path B sweep tail) `skills/discord-voice/scripts/join_trigger.py:46` had a docstring still describing the pre-v0.8 resolver chain ("$SUTANDO_WORKSPACE env, else ~/.sutando/workspace/"). Refreshed to point at the canonical helper + note the env-var is no longer honored post-v0.8. Behavior unchanged — this is the docstring only; the actual code already prefers the helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(memory-sync-refs): refresh 4 stale "memory-sync" references to workspace-sync (Path B sweep cont'd) Owner DM 2026-06-06 15:08Z (task-1780758535617): grep for "memory-sync" turned up 19 files. 4 had stale references that should now point at the v0.3.0 canonical workspace-sync flow + deprecation note: - `README.md:218` — multi-machine sync row pointed at deleted `docs/memory-sync.md` and recommended `sync-memory.sh` as primary path. Updated to: vault config + `sync-workspace.sh --init`, with link to `docs/workspace-sync.md`; called out the v0.4.0 sync-memory.sh removal. - `skills/relay/SKILL.md:33` — "Sync via the existing memory-sync engine" → "Sync via the workspace-sync engine (`scripts/sync-workspace.sh`)"; added deprecation note. - `skills/catchup-after-startup/SKILL.md:57` — "across multiple cores via memory-sync" → "via workspace-sync". - `docs/state-sync-allowlist.md:171` — link to deleted `docs/memory-sync.md` → `docs/workspace-sync.md` (with note that it's canonical as of v0.3.0). Remaining 15 of 19 are either (a) intentional historical / deprecation prose (CHANGELOG.md, scripts/sync-memory.sh, scripts/sync-workspace.sh --migrate-from-legacy block, docs/workspace-sync.md deprecation section, tests/sync-{memory,workspace}-migration.test.sh), (b) already addressed in this PR (stage-readiness.sh, main.swift), or (c) describing a real concept that's separate from the legacy clone (skills/MANIFEST.md, context-drop/Package.swift, health-check.py legacy-path lookup, inline-tools.ts skills location, workspace_default.py symlink comment, sutando-migrate.sh conflict-resolution comment, schedule-crons example narrative). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 of 29 tasks
john-the-dev
added a commit
that referenced
this pull request
Aug 3, 2026
…ng up files (#2566) * fix(health-check): a stale carrier set silently stops the vault backing up files `sync-workspace.sh:generate_exclude` refuses to rewrite an existing `.git/info/exclude` that differs from the generated content (the #1445 guard against clobbering operator-authored rules), and `_enforce_carrier_set_pre` (`:613`) swallows the refusal so the tick continues to a successful push. The guard is correct and this does not touch it. The problem is that nothing reports the consequence: the config IS correct, the checkout IS current, and only the generated artifact is stale, so every existing check reads healthy while the vault quietly stops carrying paths the config says it carries. Measured independently on two hosts (#2565): 65 refusals with 63 followed by `pushed to` on one, 4/4 on the other. On the second, `state/current-track.md` was never carried. Adds a `carrier-set` probe that asks git whether each configured include is actually un-ignored. Asking for the OUTCOME rather than regenerating the exclude and diffing means it cannot drift with the generator's formatting, and it is immune to the two legitimate no-diff branches (`:562` no-op when already matching, `:570` safe legacy `hosts/<label>/` -> `hosts/*/` widening) — after either, the paths ARE un-ignored. Reports the two causes separately because their remedies differ and conflating them sends the operator the wrong way: 1. stale exclude -> `--force-gitignore` 2. a local `vault.sync.include` REPLACES the shipped default (#2531), so upstream additions never arrive -> edit the local list. Here the on-disk exclude legitimately matches the resolved config, so cause 1 is dormant and a resolved-vs-disk comparison alone reads healthy. `--no-index` on the check-ignore call is load-bearing: without it git reports an already-tracked file as not-ignored regardless of the rules, so a host that carried a file once and later went stale reads healthy forever. The first version of this probe had that bug and was caught by restoring a real host's pre-fix exclude and watching the probe still say OK. Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(health-check): cover the carrier-set probe's guard branches Diff coverage on the previous commit was 79.6% against the repo's 95% gate. Measured locally with the repo's own .coveragerc rather than waiting for CI. Covers the four reachable branches that had no test: entries resolving to nothing (empty string, whitespace, a glob matching nothing, a missing file), a host with no configured include, a git invocation that raises, and an unreadable shipped config. The last two assert the probe degrades to `ok` rather than manufacturing a failure — a broken subprocess or malformed config must not be reported as a stale carrier set, or the check cries wolf and gets ignored. Collapses the run-list call site from three lines to one. It cannot be reached from unit tests, so it is the single remaining uncovered line; three lines of it were dragging the ratio down for no added coverage. Now 97.9% (47 executable, 46 covered). One note for anyone reusing the subprocess-failure test: `self.hc.subprocess` IS the shared module object, so restoring `run` from a fresh `import subprocess` hands back the already-patched function. The first version of that test did exactly that and leaked a broken `subprocess.run` into every later case — 7 errors. It saves the original before patching. Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(health-check): route carrier-set probe through git_argv; stop reading an unmeasured set as healthy qingyun-wu, reviewing c8c9511. Both findings were real and reproduced. 1. The probe spawned a bare "git". On macOS /usr/bin/git is an Xcode-CLT shim that opens an install dialog when the tools are absent, and this runs from a BACKGROUND health check where nobody is there to dismiss it. Now goes through `git_argv`, which refuses that shim instead of spawning it. 2. It converted execution errors into a confident pass. `check-ignore`'s contract is 0 = ignored, 1 = NOT ignored, anything else = the command failed; testing `returncode == 0` folded exit 128 in with exit 1, so a git that could not run reported the carrier set as fine. Three states now: ignored -> stale, 1 -> carried, anything else or an exception -> UNMEASURED and surfaced as "UNKNOWN, not fine". The second one is this PR's own disease in this PR's own patch. It exists because an unmeasured carrier set was being reported as healthy; the probe did exactly that when its measurement failed. `test_a_failing_git_invocation_does_not_crash_the_probe` asserted `status == "ok"` on OSError and so CODIFIED the wrong behavior — I wrote that test reasoning "don't cry wolf", which is the right instinct pointed at the wrong risk. Replaced rather than extended. Three new controls, all failing at c8c9511 and passing here: - a git that cannot run reports UNKNOWN, not ok - exit 128 is not folded in with exit 1 - no bare-"git" spawn; argv[0] must be an absolute resolved path Verified on a live host, not only in fixtures: the real stale carrier set still reports the specific path and remedy, and forcing the resolver unavailable now yields "could NOT measure 4 carrier path(s) ... UNKNOWN, not fine" where the previous head said ok. 14/14 in the suite; diff coverage 98.1% (the sole uncovered line is the run-list call site, unreachable from unit tests). Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(health-check): keep the git_argv import in the form the resolver guard asserts CI red on 95b02a6: `tests/git-binary-resolution.test.py` failed under instrumentation, which also failed the coverage gate — it refuses to report coverage on a non-green suite, so both checks were one root cause. `git-binary-resolution.test.py:109` asserts the LITERAL string `from git_binary import git_argv` to prove each call site imports the resolver rather than hardcoding a git path. Adding GitUnavailable to that same import line removed the substring, so the guard read health-check.py as no longer importing the resolver. Split into two imports rather than loosening the assertion. The guard covers 27 call sites; bending my import is the smaller change, and weakening a hardcoded-git guard to accommodate one PR is the wrong direction. Comment added at the import so the next person does not helpfully merge them back. My own miss: I ran the 60-suite sweep for the FIRST commit of this PR and only the focused carrier test for the second, where the import actually changed. Re-ran the full sweep here. tests/git-binary-resolution.test.py 27/27 OK (was 1 failure) tests/health-check-carrier-set-enforced.test.py 14/14 OK 60 suites touching health-check / git_binary 60 pass, 0 fail diff coverage 98.1% (>= 95) Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(health-check): never tell an operator to re-add a carrier path that causes cross-host overwrite qingyun-wu, P1 on e73e259. The dropped-entry remedy said "add them to the local list" for EVERY missing shipped entry — including `state/current-track.md`, which #2534 ships at a flat, SHARED vault path. Following that instruction re-tracks per-host state where a peer also writes, which is precisely the incident that destroyed a peer's 1056-line anchor on 2026-08-03. Split the remedy rather than filtering the entry. Silently dropping it from the report would hide a real divergence — the failure mode this probe exists to prevent. What had to go is the INSTRUCTION, not the information: safe entries -> "add them to the local list" (the #2531 case, unchanged) unsafe entries -> "must NOT be re-added … re-adding re-creates the cross-host overwrite that destroyed a peer's anchor (#2567)" `UNSAFE_TO_READD` is deliberately a named, one-item list with a delete-me note: it stops being reachable the moment the host-qualified migration lands and the flat path is no longer shipped. One existing test moved off that path. `test_the_DORMANT_override_case_is_still_caught` used `state/current-track.md` as its example, but it is about the MECHANISM (a local include replacing the shipped default), not that path — re-pointed at `hosts/*/` so the two concerns cannot collide again. Note on the review's premise: #2568 is no longer draft (OPEN/CHANGES_REQUESTED, BEHIND). It still has not landed and main still ships the flat entry, so the substance holds and merge ordering alone does not resolve this — which is why the fix is here rather than deferred. parent e73e259 : 16 tests, FAILED (failures=1) HEAD : 16 tests, OK 60 suites touching health-check / git_binary : 60 pass, 0 fail diff coverage 98.3% Live host unchanged: still reports the real stale-exclude case with its own remedy. Stand: Echo Act IV Mini Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Workspace-vault sync — PR-1 of N (simplified per owner directive 05:11Z)
Implements the workspace-as-git-repo architecture from the 2026-06-04 #design discussion (planning thread starts here).
STATUS: DRAFT — owner has been guiding the architecture iteratively in DM. Latest simplification (05:11Z + 05:15Z) dropped the memory translation layer.
Architecture (post-simplification)
.gitignoreselectively tracks the carrier set. Sync = vanillagit push/git pullon the workspace.host/<hostname>; pulls all peers via fetch + merge..claude-sutando/projects/<local_slug>/memory/. Different hosts write to different slugs. After pull, peer slug subdirs are visible-not-merged (operator/agent can browse them; no auto-merge).memory/subdirs tracked under each slug. Transcripts (*.jsonl),tool-results/,file_history/etc. stay gitignored (can be large + per-host).git checkout --ours(use-local fallback).Commits in this PR
c467d5a7f999e27c72d4e~/.sutando/memory-sync/.4f3069ffceab86<slug>/memory/subdir (not just one canonical); test suite updated and re-passes (21/21)..gitignore (whitelist mode)
Subcommands
Env vars
SUTANDO_VAULTSUTANDO_MEMORY_REPOSUTANDO_VAULT— honored one release with stderr warning.SUTANDO_REPO_DIRNO_COLORSUTANDO_SYNC_MAX_DELETESUTANDO_FORCE_SYNCVerification
bash -n scripts/sync-workspace.sh— syntax clean (551 lines)bash tests/sync-workspace.test.sh— 21/21 pass (was 18/18 before simplification; added 3 new asserts for the gitignore-only-tracks-memory invariant + status-no-longer-mentions-translation-layer + transcript-not-pushed)scripts/sync-memory.shuntouched + still worksWhat this PR is NOT
sync-memory.sh→sync-workspace.sh. Existing prod sync stays as-is. PR-2.vault.sync.{include,exclude}fromsutando.config.local.json). Follow-up.--initmanually on a test host.<peer_slug>/memory/paths if interested. (Removed from PR-1 per owner direction; could be revisited in a follow-up if real-pain emerges.)Open follow-ups (file as separate issues post-merge)
sync-memory.sh→sync-workspace.sh+ ~20 reference updates + backward-compat shim.vault.sync.{include,exclude}fromsutando.config.local.json, merge intogenerate_gitignore. ~30 lines.src/startup.shnear init.scripts/fleet-status.shreads peer branches viagit show host/<peer>:state/....Cross-PR linkage