Skip to content

Port dream setup to Python on Windows - #33

Merged
Xingdi (Eric) Yuan (xingdi-eric-yuan) merged 16 commits into
microsoft:mainfrom
wmaciel:wmaciel/phase4-dream-setup-py
Sep 28, 2026
Merged

Xingdi (Eric) Yuan (xingdi-eric-yuan) merged 16 commits into
microsoft:mainfrom
wmaciel:wmaciel/phase4-dream-setup-py

Conversation

@wmaciel

Copy link
Copy Markdown
Collaborator

Summary

Closes #11.
Part of #7.

Ports the Dream setup entry point from Bash to Python. dream-setup.py now emits structured JSON instead of shell code, so the workflow can be consumed safely and consistently on Windows, Linux, and macOS.

Reviewer guide

The commits are intentionally small and ordered for review:

  1. Python setup port: adds dream-setup.py, ports the setup tests, switches the skill to JSON consumption, and removes dream-setup.sh plus its Bash test.
  2. Review 01 lifecycle fixes: shares the resolved worktree root between setup, reconciliation, and existing shell cleanup/GC consumers; preserves JSON values safely; and makes cleanup outcomes honest.
  3. Review 02 safety fixes: prevents the known unsafe Bash GC fallback on Windows, makes roots canonical, scopes reconciliation cleanup to the resolved namespace, and fails closed when worktree ownership is detached or indeterminate.
  4. Regression coverage: covers the setup JSON to reconciliation cleanup lifecycle with a real Git worktree and bare remote.

Behavior and safety changes

  • Setup emits JSON only, eliminating the prior eval / shell-export boundary.
  • Relative DREAM_WORKTREE_BASE overrides are canonicalized before setup emits or cleanup consumes them.
  • Every documented reconciliation flow passes setup's resolved namespace explicitly; indexed cleanup cannot remove another namespace's branches.
  • Windows skips the POSIX-only dream-gc.sh auto-GC fallback until Phase 5 provides dream-gc.py.
  • Cleanup preserves a worktree whenever Git reports detached or indeterminate ownership, preventing same-slug cleanup from force-removing uncommitted work.

Scope boundary

This PR does not port dream-cleanup.sh or dream-gc.sh; those scripts remain in place and their Python replacements remain Phase 5 (#12). The small dream-reconcile.py changes are limited to maintaining the setup-to-cleanup lifecycle contract and preventing destructive behavior exposed by the new Windows-capable setup entry point.

Regression coverage added

  • Windows Bash-GC fallback suppression.
  • Canonical worktree root propagation through setup JSON and reconciler cleanup.
  • Namespace resolution consistency, quoted .env values, and multi-namespace cleanup isolation.
  • Attached, detached, indeterminate, and case-insensitive worktree registration handling.

🤖 Co-authored with GitHub Copilot.

Cross-platform Python port of the bash dream-setup.sh. Prints the dream
context as JSON (the eval/export env mode is dropped; JSON is the only
output). Mirrors the .sh contract: slug/namespace validation, .shadow/
gitignore guard (child-path probe), default-branch detection, namespace
resolution (env > TASK_INFO.json > .env > basename), external worktree
path, idempotent create-with-retry, and RUN_PREFIX detection.

Pins UTF-8 on stdout/stderr and imports the shared _worktree_safety gate
(not a subprocess). Auto-GC prefers a dream-gc.py sibling and otherwise
falls back to the bash dream-gc.sh; it is best-effort and never breaks
setup, cleanly no-opping where no usable shell exists.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35800a4a-a5d9-4e3a-b0e3-0d093e083b24
Port test_dream_setup_sh.py to invoke dream-setup.py via sys.executable
(no bash), so the suite runs on both OSes. The base env starts from the
real environment (Python + git need SystemRoot/PATH/TEMP on Windows),
pins git config to devnull, and clears inherited DREAM_* vars. Worktree
registration is compared via resolved Paths since `git worktree list`
prints forward-slash paths on Windows.

The auto-GC throttle/opt-out tests (dream-setup's own logic) run on every
OS. The two tests that assert an orphan is actually swept still need the
bash dream-gc.sh, so they sit in a requires_bash class skipped on Windows.
The eval-stdout-purity test becomes a JSON-stdout-purity assertion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35800a4a-a5d9-4e3a-b0e3-0d093e083b24
Point the dream skill at the ported dream-setup.py and its JSON contract:
frontmatter scripts list, the helper table, the .shadow/ gitignore-guard
note, the Script-Failure-Recovery list, the setup-script finder, and the
two setup examples now run `dream-setup.py` and parse the JSON object it
prints instead of eval-ing shell exports. References to dream-gc.sh being
auto-triggered now name dream-setup.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35800a4a-a5d9-4e3a-b0e3-0d093e083b24
The Python dream-setup.py fully replaces the bash entry point, so drop
dream-setup.sh and its bash-only test_dream_setup_sh.py. This also retires
that module's requires_bash Windows skip. dream-cleanup.sh and dream-gc.sh
remain until they are ported to Python.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35800a4a-a5d9-4e3a-b0e3-0d093e083b24
Route documented setup through a Python interpreter, document the JSON-to-variable mapping, canonicalize repo roots before isolation checks, and avoid throttling auto-GC when the sweeper cannot launch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use one platform-aware root resolver for setup and reconciliation, propagate it to auto-GC, and preserve failed cleanup worktrees for recovery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Protect root propagation, failed auto-GC retry behavior, controlled TASK_INFO validation, safe bridge parsing, and cleanup reporting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the NUL-safe JSON bridge and pass the setup-selected root to reconciliation and Bash cleanup commands.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Canonicalize shared worktree roots and prevent the POSIX GC fallback from running on Windows, avoiding cleanup paths that cannot safely interpret Windows worktree metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve namespaces consistently across setup and reconciliation, propagate the setup namespace explicitly, and prevent cleanup from touching indexed branches outside the selected namespace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve slug-colliding worktrees whenever Git reports a detached or indeterminate registration, and normalize worktree identity for Windows path casing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercise setup JSON root propagation through reconciler cleanup with a real worktree and bare remote.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve the Python setup and canonical root/namespace flow alongside current pinned tooling, coherent lineage retention, and streamlined documentation. Add cross-history integration coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Merged current main (4d863a3, including #38 and #39) into this branch without rewriting its history. The resolution preserves the Python setup port, shared namespace/native worktree-root handling, pinned current tooling, coherent ancestor retention, and streamlined docs. The PR is now mergeable. Added coverage for setup dependencies in both pinned installed layouts and for coherent ancestors across namespaces.

467 combined setup/reconciliation/tooling/validator/Nap tests passed. The previously identified TMPDIR-sensitive test was excluded from that combined run and reproduced separately as a failure. I re-ran the production findings against the merged tree using private repositories and local bare remotes; the three comments below still apply and were deliberately not folded into the conflict resolution.

Comment on lines +1508 to +1512
# Remove the registered worktree before deleting its branch. Git
# refuses to delete a branch that remains checked out in a worktree.
_gc_worktree_after_merge(
repo_root, dream_ns, dream_id, branch, worktree_root,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Preserve unpublished local commits before removing the worktree/branch. This new ordering removes the worktree before the later git branch -D, so Git no longer protects a checked-out local branch. The artifact/index/default-branch checks establish that an earlier experiment was reconciled, not that the current local tip was published.

Reproduced after this merge: push an experiment, commit/push its reconciled artifacts on main, then make one additional local commit in the still-attached dream worktree without pushing. Cleanup removes that worktree and both branch refs; git for-each-ref --contains <unpublished-tip> becomes empty. Against the original base, the local branch survived because branch deletion happened while it was checked out. The commit object still exists temporarily, but its only durable ref is lost.

Before either removal, verify that the local tip is covered by the archived/persisted experiment tip (and avoid removing dirty worktrees). If it is ahead, diverged, or cannot be established as preserved, retain the worktree and branch with an actionable message. Add a real-Git regression for a clean worktree containing an unpushed local commit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fixed in 9ef4e11. cleanup_branches() in dream-reconcile.py now verifies that local and remote-tracking tips are ancestors of the archived tip_commit before deletion. Registered worktrees are removed without --force; an unsafe or failed removal retains the branch. Real-Git regressions cover unpushed/diverged commits, dirty worktrees, and unverifiable archive tips.

Comment thread skills/shadow-frog-dream/dream-setup.py Outdated
if r.returncode != 0:
_err("ERROR: Not in a git repository")
sys.exit(1)
return os.path.abspath(r.stdout.strip())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Do not trim filename whitespace from the repository root. stdout.strip() removes legitimate trailing spaces/tabs from the path returned by Git. An existing POSIX repository named source repo works with the old Bash setup, but the Python port returns .../source repo and fails at os.chdir(repo_root) with FileNotFoundError, even with explicit --repo-root and --namespace.

Remove only Git's output terminator while preserving the actual filename, and add a real repository-path regression. I reproduced this unchanged at bcbeeff.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fixed in 9ef4e11. _resolve_repo_root() in dream-setup.py now removes only the final newline emitted by Git, preserving actual filename whitespace. Added real-repository regressions for trailing spaces, tabs, and newlines, with both explicit --repo-root and automatic discovery.

Comment on lines +240 to +243
env_extra={
"DREAM_GC_AUTO": "0",
"TEMP": str(system_temp),
"TMP": str(system_temp),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Isolate TMPDIR as well as TEMP/TMP in this test. _base_env() copies the host environment, and tempfile.gettempdir() checks TMPDIR before these two overrides. With a valid inherited TMPDIR (as on macOS), setup correctly uses that directory and the assertion at line 250 fails. This also creates the worktree outside the directory the test intended to control.

The focused run with a private inherited TMPDIR reproduced the failure; the same test passed when TMPDIR was removed. Set all three variables to system_temp, or clear the higher-priority variable explicitly. This is a test-isolation problem, not a reason to change the production native-temp resolution.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fixed in 9ef4e11. test_uses_system_temp_root_without_override() in test_dream_setup.py now sets TMPDIR, TEMP, and TMP to the same test-owned directory. It also seeds a conflicting inherited TMPDIR and verifies that no worktree is created there.

Preserve reconciliation path preflight alongside shared namespace resolution and consolidate the dated changelog entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Check branch tips against the archived experiment before cleanup, retain registered worktrees that cannot be safely removed, and isolate TMPDIR in the native-temp test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve Python 3.9 execution and lossless NUL-delimited worktree paths, correctly account for remote-only cleanup, and reject newline-terminated identifiers. Extend real-Git regression cases and the existing Python 3.9 smoke job.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@xingdi-eric-yuan

Copy link
Copy Markdown
Collaborator

Follow-up fixes in f39bb31:

  • Restored Python 3.9 compatibility in the reconciler.
  • Preserved tabs, newlines, and carriage returns in worktree paths so ownership checks maintain dirty-worktree protection.
  • Corrected cleanup reporting when no local branch exists.
  • Rejected trailing newlines in slugs and namespaces.

Added regression coverage and Python 3.9 checks for Dream entry points.

@xingdi-eric-yuan
Xingdi (Eric) Yuan (xingdi-eric-yuan) merged commit 9249b63 into microsoft:main Sep 28, 2026
7 checks passed
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.

[Windows port] 4/8: Port dream-setup.sh to Python

3 participants