Skip to content

feat(sync-workspace): workspace-as-git-repo + memory translation layer (PR-1) - #1445

Merged
qingyun-wu merged 9 commits into
staging-workspace-revampfrom
feat/workspace-vault-sync-pr1
Jun 4, 2026
Merged

qingyun-wu merged 9 commits into
staging-workspace-revampfrom
feat/workspace-vault-sync-pr1

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented Jun 4, 2026 •

Copy link
Copy Markdown
Collaborator

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)

  • Workspace itself IS the git repo. No separate vault clone. .gitignore selectively tracks the carrier set. Sync = vanilla git push/git pull on the workspace.
  • Branch-per-host topology. Each host pushes only to host/<hostname>; pulls all peers via fetch + merge.
  • NO memory translation layer. Each host's Claude Code memory dir is tracked under its OWN slug at .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).
  • Only memory/ subdirs tracked under each slug. Transcripts (*.jsonl), tool-results/, file_history/ etc. stay gitignored (can be large + per-host).
  • Conflict policy. 3-way auto-merge first; on unresolvable conflict, git checkout --ours (use-local fallback).
  • Pull ordering. Oldest peer push first (minimizes per-step merge diff).

Commits in this PR

Commit Phase What
c467d5a Phase 1 Scaffold (bootstrap + dispatch).
7f999e2 Phase 2 git init + .gitignore + (then-included) translation layer + bidirectional sync.
7c72d4e Phase 3 Migration helper from ~/.sutando/memory-sync/.
4f3069f Phase 4 18-assertion integration test.
fceab86 Simplification Dropped memory translation layer per owner direction; expanded gitignore to track each <slug>/memory/ subdir (not just one canonical); test suite updated and re-passes (21/21).

.gitignore (whitelist mode)

# Whitelist: ignore everything, un-ignore the carrier set.
*
!.gitignore

# Top-level
!notes/
!notes/**
!pending-questions.md
!build_log/
!build_log/**

# Per-host Claude Code memory — only memory/ subdirs tracked, transcripts skipped
!.claude-sutando/
!.claude-sutando/projects/
!.claude-sutando/projects/*/
!.claude-sutando/projects/*/memory/
!.claude-sutando/projects/*/memory/**

# Hard-deny credentials
.env*
*.heartbeat
*.alive
*.sentinel
*.pid

Subcommands

bash scripts/sync-workspace.sh                  # default: pull + push
bash scripts/sync-workspace.sh --init           # one-time: git init + remote + first push
bash scripts/sync-workspace.sh --pull-only      # pull peers
bash scripts/sync-workspace.sh --push-only      # push own host branch
bash scripts/sync-workspace.sh --status         # show sync state
bash scripts/sync-workspace.sh --migrate-from-legacy  # one-time: ~/.sutando/memory-sync/ → workspace
bash scripts/sync-workspace.sh --help

Env vars

Var Purpose
SUTANDO_VAULT Git URL of the private vault repo (REQUIRED).
SUTANDO_MEMORY_REPO Legacy alias for SUTANDO_VAULT — honored one release with stderr warning.
SUTANDO_REPO_DIR Path to sutando code checkout (auto-detected; honored as override).
NO_COLOR Suppress ANSI escapes in warnings.
SUTANDO_SYNC_MAX_DELETE Mass-deletion tripwire threshold (default 50).
SUTANDO_FORCE_SYNC Bypass mass-deletion tripwire (=1).

Verification

  • 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)
  • Existing scripts/sync-memory.sh untouched + still works

What this PR is NOT

  • No file rename of sync-memory.sh → sync-workspace.sh. Existing prod sync stays as-is. PR-2.
  • No user-config override of the carrier set yet (vault.sync.{include,exclude} from sutando.config.local.json). Follow-up.
  • No reference updates in README/src/scripts/skills/docs. PR-2.
  • No fleet rollout. Opt-in: operators run --init manually on a test host.
  • No cross-host memory auto-merge. Visible-not-merged is the contract; operator/agent can read <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)

  1. PR-2: rename sync-memory.sh → sync-workspace.sh + ~20 reference updates + backward-compat shim.
  2. User-config carrier override: read vault.sync.{include,exclude} from sutando.config.local.json, merge into generate_gitignore. ~30 lines.
  3. Boot-time pull: background pull from src/startup.sh near init.
  4. Fleet renderer: scripts/fleet-status.sh reads peer branches via git show host/<peer>:state/....

Cross-PR linkage

qingyun-wu and others added 8 commits June 3, 2026 21:33
…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
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>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu
qingyun-wu merged commit 2020550 into staging-workspace-revamp Jun 4, 2026
5 checks passed
@qingyun-wu
qingyun-wu deleted the feat/workspace-vault-sync-pr1 branch June 4, 2026 05:56
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>
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>
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>
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.

1 participant