Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ Design principles:

- `wspace check` — read-only baseline check. Reports `CLEAN`, `DIRTY`,
`FEATURE_CLEAN`, `DIVERGED`, `UNKNOWN`, `MISSING`, and `UNMANAGED` states.
- `wspace init` — clone missing repositories from the manifest. Prints a warning
that fresh clones lack gitignored files and repo-specific setup.
- `wspace init [<repo...>]` — clone missing repositories from the manifest (or
only a specified subset of repos). Prints a warning that fresh clones lack
gitignored files and repo-specific setup.
- `wspace update` — fetch remotes and fast-forward only clean default branches.
- `wspace worktree add <repo> <feature> [<commit-ish>]` — create a git worktree
under `worktrees/<repo>/<feature>/`, branching from the repo's default-branch
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
"test": "deno test --allow-read --allow-write --allow-run"
},
"publish": {
"include": ["src/", "schema/", "LICENSE", "README.md"]
"include": ["src/", "schema/", "skills/", "LICENSE", "README.md"]
}
}
99 changes: 99 additions & 0 deletions skills/wspace/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
name: wspace
description: Manage multi-repo Wazoo workspaces, feature worktrees, conservative default-branch updates, and secret sync using wspace CLI. Use when managing multi-repo workflows, creating feature worktrees, checking baseline health, syncing secrets, or running wspace commands.
---

# `wspace` workspace skill

Orchestrate multi-repo development, isolate tasks in Git worktrees, refresh
default branch baselines safely, and synchronize environment secrets using
`wspace`.

## Quick start

Execute all workspace commands from the workspace root containing `repos.json`:

```sh
# Inspect workspace baseline health
wspace check

# Clone missing repositories (or a specific subset of repos)
wspace init [repo1 repo2 ...]

# Fast-forward clean default branches safely
wspace update

# Create isolated feature worktree anchored to origin/<default>
wspace worktree add <repo> <feature>
# (Or using raw git): git -C repos/<repo> worktree add "$PWD/worktrees/<repo>/<feature>" -b <feature>

# Propagate local secrets to checkouts and worktrees
wspace env sync

# Push branch and open PR from inside worktrees/<repo>/<feature>
git push -u origin <feature>
env GITHUB_TOKEN="" gh pr create

# List fully merged stale worktrees and clean up
wspace worktree list --stale
wspace worktree remove <repo> <feature>
```

## Worktree lifecycle and workflows

### Feature worktree creation and isolation

To start work on a feature, bug fix, or agent task:

1. **Baseline health check**: Run `wspace check` to verify the repository is
clean or on a default branch.
2. **Refresh upstreams**: Run `wspace update` to fetch remotes and fast-forward
clean default branches (`merge --ff-only`).
3. **Provision worktree**: Run `wspace worktree add <repo> <feature>` or
`git -C repos/<repo> worktree add "$PWD/worktrees/<repo>/<feature>" -b <feature>`.
- **Baseline rule**: `wspace worktree add` automatically branches from
`origin/<default>` (via `origin/HEAD`) using `--no-track`, preventing
accidental forks from dirty local `HEAD` references.
- **Path rule**: Always use `$PWD` when running `git -C repos/<repo>` so
worktrees resolve to `worktrees/<repo>/<feature>` at the workspace root
rather than nested under `repos/<repo>/`.
4. **Sync secrets**: Run `wspace env sync` (or `--dry-run` to preview) to copy
secrets from `secrets/<repo>/` into the new worktree.
5. **Develop and commit**: Perform changes strictly inside
`worktrees/<repo>/<feature>/`. Keep `repos/<repo>/` clean.

### Push, PR, and worktree cleanup

1. **Push and create PR**: Inside `worktrees/<repo>/<feature>/`, run:
```sh
git push -u origin <feature>
env GITHUB_TOKEN="" gh pr create
```
2. **Identify stale worktrees**: After PR merge, run
`wspace worktree list --stale`.
- **Staleness criteria**: Identifies worktrees whose branch has no unique
commits beyond `origin/<default>` (fully merged) or whose branch ref is
deleted.
3. **Teardown worktree**: Run `wspace worktree remove <repo> <feature>`.
- **Cleanup**: Removes the worktree, prunes stale git references, and cleans
up empty parent directories (`worktrees/<repo>/`).

### Machine inspection for agents

To verify workspace integrity before cross-repo edits:

- Call `wspace check --json` to receive structured state per repo (`CLEAN`,
`DIRTY`, `FEATURE_CLEAN`, `DIVERGED`, `UNKNOWN`, `MISSING`, `INVALID`,
`WORKTREE_DIRTY`, `ERROR`).
- If `wspace check` exits with `1` or returns any state other than `CLEAN` or
`FEATURE_CLEAN`, halt or request user resolution before applying multi-repo
edits.

## Guiding principles

- **Root anchor**: The workspace root is the single source of truth; all paths
resolve relative to the directory containing `repos.json`.
- **Conservative mutation**: `wspace update` never resets, rebases, stashes, or
rewrites history. It skips dirty or feature branches.
- **Central secret vault**: Never write `.env` files directly in `repos/` or
`worktrees/`. Always edit `secrets/<repo>/` and run `wspace env sync`.
30 changes: 25 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ function usage(): void {

Usage:
wspace check [--json]
wspace init [--json]
wspace sync [--json]
wspace init [<repo...>] [--json]
wspace sync [<repo...>] [--json]
wspace update [--json]
wspace worktree add <repo> <feature> [<commit-ish>]
wspace worktree list [--stale] [--json]
Expand Down Expand Up @@ -243,10 +243,26 @@ async function cloneMissing(
g: GitRunner,
manifest: WorkspaceManifest,
paths: ManifestPaths,
targets: string[] = [],
): Promise<{ name: string; state: string; detail?: string }[]> {
await Deno.mkdir(paths.repositoriesDirectory, { recursive: true });
let repositories = manifest.repositories;
if (targets.length > 0) {
const validNames = new Set(manifest.repositories.map((r) => r.name));
for (const name of targets) {
if (!validNames.has(name)) {
return [{
name,
state: "UNKNOWN_REPO",
detail: `Repository "${name}" not found in manifest`,
}];
}
}
const targetSet = new Set(targets);
repositories = repositories.filter((r) => targetSet.has(r.name));
}
const rows: { name: string; state: string; detail?: string }[] = [];
for (const repository of manifest.repositories) {
for (const repository of repositories) {
const repoPath = resolveRepositoryPath(repository, paths);
if (await exists(repoPath)) {
if (await exists(join(repoPath, ".git"))) {
Expand Down Expand Up @@ -290,7 +306,10 @@ async function runCommand(
}
case "sync":
case "init": {
const rows = await cloneMissing(g, manifest, paths);
const targets = opts.subcommand
? [opts.subcommand, ...opts.positional]
: [];
const rows = await cloneMissing(g, manifest, paths, targets);
if (opts.json) {
console.log(JSON.stringify(rows, null, 2));
} else {
Expand All @@ -300,7 +319,8 @@ async function runCommand(
(r) =>
r.state === "CLONE_FAILED" ||
r.state === "PATH_BLOCKED" ||
r.state === "INVALID",
r.state === "INVALID" ||
r.state === "UNKNOWN_REPO",
);
if (opts.command === "init" && !failed) {
console.error(
Expand Down
36 changes: 36 additions & 0 deletions tests/integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,42 @@ Deno.test("wspace init clones missing repositories", async () => {
}
});

Deno.test("wspace init clones only specified subset of repositories", async () => {
const dir = await Deno.makeTempDir();
try {
await makeRepoWithMain(dir, "a");
await makeRepoWithMain(dir, "b");
await makeRepoWithMain(dir, "c");
await Deno.remove(join(dir, "b"), { recursive: true });
await Deno.remove(join(dir, "c"), { recursive: true });
const manifestPath = join(dir, "repos.json");
await Deno.writeTextFile(
manifestPath,
JSON.stringify({
repositories: [
{ name: "a", url: join(dir, "a.git"), path: join(dir, "a") },
{ name: "b", url: join(dir, "b.git"), path: join(dir, "b") },
{ name: "c", url: join(dir, "c.git"), path: join(dir, "c") },
],
}),
);
const code = await run(["init", "b", "--manifest", manifestPath]);
assertEquals(code, 0);
assertEquals(
await exists(join(dir, "b", ".git")),
true,
"specified repo b should be cloned",
);
assertEquals(
await exists(join(dir, "c", ".git")),
false,
"unspecified repo c should not be cloned",
);
} finally {
await Deno.remove(dir, { recursive: true });
}
});

Deno.test("wspace check reports CLEAN via CLI", async () => {
const dir = await Deno.makeTempDir();
try {
Expand Down
Loading