-
Notifications
You must be signed in to change notification settings - Fork 0
feat: multi-agent orchestration with locking and streaming output #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import assert from "node:assert/strict"; | ||
| import { existsSync, readFileSync, rmSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { beforeEach, describe, it } from "node:test"; | ||
| import { acquireLock, getLockedKeys, isLocked, releaseLock } from "./lock.js"; | ||
|
|
||
| const TEST_LOCKS_DIR = join(process.cwd(), ".crewbit/test.locks"); | ||
|
|
||
| function cleanTestDir() { | ||
| if (existsSync(TEST_LOCKS_DIR)) { | ||
| rmSync(TEST_LOCKS_DIR, { recursive: true, force: true }); | ||
| } | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| cleanTestDir(); | ||
| }); | ||
|
|
||
| describe("acquireLock", () => { | ||
| it("creates a lock file and returns true", () => { | ||
| const result = acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| assert.equal(result, true); | ||
| assert.equal(isLocked(TEST_LOCKS_DIR, "JIR-1"), true); | ||
| }); | ||
|
|
||
| it("returns false if lock already exists", () => { | ||
| acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| const result = acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| assert.equal(result, false); | ||
| }); | ||
|
|
||
| it("creates the lock directory if it does not exist", () => { | ||
| assert.equal(existsSync(TEST_LOCKS_DIR), false); | ||
| acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| assert.equal(existsSync(TEST_LOCKS_DIR), true); | ||
| }); | ||
|
|
||
| it("writes metadata (pid, timestamp) to the lock file", () => { | ||
| acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| const lockPath = join(TEST_LOCKS_DIR, "JIR-1.lock"); | ||
| assert.equal(existsSync(lockPath), true); | ||
| const content = JSON.parse(readFileSync(lockPath, "utf8")); | ||
| assert.equal(typeof content.pid, "number"); | ||
| assert.equal(typeof content.timestamp, "string"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("releaseLock", () => { | ||
| it("removes the lock file", () => { | ||
| acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| releaseLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| assert.equal(isLocked(TEST_LOCKS_DIR, "JIR-1"), false); | ||
| }); | ||
|
|
||
| it("does not throw if lock does not exist", () => { | ||
| assert.doesNotThrow(() => releaseLock(TEST_LOCKS_DIR, "NONEXISTENT")); | ||
| }); | ||
| }); | ||
|
|
||
| describe("isLocked", () => { | ||
| it("returns true when lock file exists", () => { | ||
| acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| assert.equal(isLocked(TEST_LOCKS_DIR, "JIR-1"), true); | ||
| }); | ||
|
|
||
| it("returns false when lock file does not exist", () => { | ||
| assert.equal(isLocked(TEST_LOCKS_DIR, "JIR-999"), false); | ||
| }); | ||
|
|
||
| it("returns false when lock directory does not exist", () => { | ||
| assert.equal(isLocked("/nonexistent/path/locks", "JIR-1"), false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("getLockedKeys", () => { | ||
| it("returns empty array when no locks exist", () => { | ||
| const keys = getLockedKeys(TEST_LOCKS_DIR); | ||
| assert.deepEqual(keys, []); | ||
| }); | ||
|
|
||
| it("returns all locked issue keys", () => { | ||
| acquireLock(TEST_LOCKS_DIR, "JIR-1"); | ||
| acquireLock(TEST_LOCKS_DIR, "JIR-2"); | ||
| acquireLock(TEST_LOCKS_DIR, "PROJ-42"); | ||
| const keys = getLockedKeys(TEST_LOCKS_DIR); | ||
| assert.equal(keys.length, 3); | ||
| assert.ok(keys.includes("JIR-1")); | ||
| assert.ok(keys.includes("JIR-2")); | ||
| assert.ok(keys.includes("PROJ-42")); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const LOCK_DIR = ".crewbit.locks"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function acquireLock(lockDir: string, issueKey: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lockPath = join(lockDir, `${issueKey}.lock`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existsSync(lockPath)) return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(lockDir)) mkdirSync(lockDir, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+9
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const metadata = JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pid: process.pid, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeFileSync(lockPath, metadata); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+15
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existsSync(lockPath)) return false; | |
| if (!existsSync(lockDir)) mkdirSync(lockDir, { recursive: true }); | |
| const metadata = JSON.stringify({ | |
| pid: process.pid, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| writeFileSync(lockPath, metadata); | |
| return true; | |
| if (!existsSync(lockDir)) mkdirSync(lockDir, { recursive: true }); | |
| const metadata = JSON.stringify({ | |
| pid: process.pid, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| try { | |
| writeFileSync(lockPath, metadata, { flag: "wx" }); | |
| return true; | |
| } catch (error: unknown) { | |
| if ( | |
| typeof error === "object" && | |
| error !== null && | |
| "code" in error && | |
| error.code === "EEXIST" | |
| ) { | |
| return false; | |
| } | |
| throw error; | |
| } |
Copilot
AI
Apr 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lock metadata (pid, timestamp) is written but never used. If the daemon crashes, stale lock files will block issues indefinitely. Consider validating/removing stale locks (e.g., check whether pid is still alive and/or enforce a TTL) when acquiring/reading locks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lock directory is created under the repo root (
.crewbit.locks). Without a corresponding .gitignore entry, these lock files will appear as untracked changes and can be accidentally committed. Consider ignoring the lock dir (and any test lock dirs) in .gitignore.