mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
* fix: add unique spawn IDs to prevent history record corruption
History records were matched by heuristic ("most recent record for this
cloud without a connection"), which caused saveVmConnection and
saveLaunchCmd to overwrite the wrong record during concurrent or failed
spawns.
Fix: every SpawnRecord now has a unique `id` (UUID). All history
operations (saveVmConnection, saveLaunchCmd, removeRecord,
markRecordDeleted, mergeLastConnection) match by id when available,
falling back to the old heuristic for pre-migration records.
The orchestrator (TS path) now creates the history record AFTER server
creation succeeds, not before — so failed provisions don't leave orphan
entries.
Also adds "Remove from history" option to the spawn ls action picker,
restoring the ability to soft-delete entries without destroying the VM.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add 18 unit tests for spawn ID history behavior
Tests cover:
- generateSpawnId returns unique UUIDs
- saveSpawnRecord auto-generates id when not provided
- saveVmConnection matches by spawnId (not heuristic)
- saveVmConnection does not cross-contaminate concurrent spawns
- saveVmConnection falls back to heuristic without spawnId
- saveLaunchCmd matches by spawnId (not heuristic)
- saveLaunchCmd falls back without spawnId
- removeRecord matches by id, not by timestamp+agent+cloud
- removeRecord handles duplicate timestamps correctly
- removeRecord falls back for legacy records without id
- markRecordDeleted targets correct record by id
- mergeLastConnection uses spawn_id from last-connection.json
- mergeLastConnection falls back to heuristic without spawn_id
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: enable biome import sorting with grouped imports
Adds organizeImports to biome assist config with groups:
1. Type imports
2. Node built-ins
3. Third-party packages
4. @openrouter/* packages
5. Aliases
Auto-fixed import order and lint issues across all TypeScript files,
including .claude/skills/ and packages/cli/src/.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
83 lines
2 KiB
TypeScript
83 lines
2 KiB
TypeScript
/**
|
|
* PreToolUse hook for Write|Edit — ensures edits happen in a git worktree on a feature branch.
|
|
*
|
|
* Reads hook JSON from stdin, extracts tool_input.file_path.
|
|
* Blocks (exit 2) if the file is in the main checkout or on the main branch.
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { dirname } from "node:path";
|
|
import { FilePathInput, parseStdin } from "./schemas.ts";
|
|
|
|
const raw = await Bun.stdin.text();
|
|
const parsed = parseStdin(raw, FilePathInput);
|
|
if (!parsed) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const filePath = parsed.tool_input.file_path;
|
|
|
|
const dir = dirname(filePath);
|
|
if (!existsSync(dir)) {
|
|
process.exit(0);
|
|
}
|
|
|
|
function git(...args: string[]): string {
|
|
return execFileSync("git", args, {
|
|
cwd: dir,
|
|
encoding: "utf-8",
|
|
}).trim();
|
|
}
|
|
|
|
let gitDir: string;
|
|
let gitCommonDir: string;
|
|
try {
|
|
gitDir = git("rev-parse", "--git-dir");
|
|
gitCommonDir = git("rev-parse", "--git-common-dir");
|
|
} catch {
|
|
// Not a git repo — let it pass
|
|
process.exit(0);
|
|
}
|
|
|
|
// Resolve to absolute paths
|
|
const resolveFromDir = (p: string) => {
|
|
if (p.startsWith("/")) {
|
|
return p;
|
|
}
|
|
return execFileSync(
|
|
"realpath",
|
|
[
|
|
"-m",
|
|
`${dir}/${p}`,
|
|
],
|
|
{
|
|
encoding: "utf-8",
|
|
},
|
|
).trim();
|
|
};
|
|
|
|
const absGitDir = resolveFromDir(gitDir);
|
|
const absCommonDir = resolveFromDir(gitCommonDir);
|
|
|
|
if (absGitDir === absCommonDir) {
|
|
console.error("BLOCKED: Edits must happen in a git worktree, not the main checkout.");
|
|
console.error("Create a worktree first: git worktree add /tmp/spawn-worktrees/FEATURE -b branch-name");
|
|
console.error("Then use absolute paths under /tmp/spawn-worktrees/FEATURE/ for all edits.");
|
|
process.exit(2);
|
|
}
|
|
|
|
let branch: string;
|
|
try {
|
|
branch = git("rev-parse", "--abbrev-ref", "HEAD");
|
|
} catch {
|
|
process.exit(0);
|
|
}
|
|
|
|
if (branch === "main") {
|
|
console.error("BLOCKED: Cannot edit on main branch, even in a worktree.");
|
|
console.error(
|
|
"Create a worktree with a feature branch: git worktree add /tmp/spawn-worktrees/FEATURE -b branch-name",
|
|
);
|
|
process.exit(2);
|
|
}
|