jules-orchestrator-kit 0.2.8 ā 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/JULES_RULES_TEMPLATE.md +6 -6
- package/README.md +32 -43
- package/package.json +5 -2
- package/scripts/jules-dispatch.mjs +21 -5
- package/scripts/jules-nightly.mjs +7 -1
- package/scripts/jules-queue-runner.mjs +18 -2
- package/scripts/jules-self-audit.mjs +12 -8
- package/scripts/jules-swarm.mjs +42 -3
- package/.agent/history/2026-07-27-dispatch-test-swarm-task.md +0 -9
package/JULES_RULES_TEMPLATE.md
CHANGED
|
@@ -36,12 +36,12 @@ Dispatch tasks to Jules when ALL of the following apply:
|
|
|
36
36
|
|
|
37
37
|
Jules automatically infers test and build verification commands via `scripts/command-resolver.mjs`:
|
|
38
38
|
- `.agent/jules.yml` -> Custom user commands (`test_cmd`, `build_cmd`)
|
|
39
|
-
- `package.json` -> `npm test` (or `npm run lint && npm test`
|
|
40
|
-
- `Cargo.toml` -> `cargo test --workspace
|
|
41
|
-
- `go.mod` -> `go test ./...
|
|
42
|
-
- `pyproject.toml` -> `pytest`
|
|
43
|
-
- `pom.xml` -> `mvn test`
|
|
44
|
-
- `build.gradle` ->
|
|
39
|
+
- `package.json` -> `testCmd: "npm test"` (or `"npm run lint && npm test"`), `buildCmd: "npm run build"`
|
|
40
|
+
- `Cargo.toml` -> `testCmd: "cargo test --workspace"`, `buildCmd: "cargo build"`
|
|
41
|
+
- `go.mod` -> `testCmd: "go test ./..."`, `buildCmd: "go build ./..."`
|
|
42
|
+
- `pyproject.toml` -> `testCmd: "pytest"`, `buildCmd: ""`
|
|
43
|
+
- `pom.xml` -> `testCmd: "mvn test"`, `buildCmd: "mvn compile"`
|
|
44
|
+
- `build.gradle` -> `testCmd: "./gradlew test"`, `buildCmd: "./gradlew assemble"`
|
|
45
45
|
- Workspace graphs (`turbo.json`, `pnpm-workspace.yaml`, `nx.json`) -> targeted affected package filters
|
|
46
46
|
|
|
47
47
|
---
|
package/README.md
CHANGED
|
@@ -54,8 +54,7 @@ Instead of blindly trusting AI code mutations, the Orchestrator acts as a strict
|
|
|
54
54
|
2. **Secret & Path Defense:** Hides passwords, API keys (`entropy > 3.6`), and blocks path traversal (`../`).
|
|
55
55
|
3. **Jules Proposes Code:** Google Jules mutates code in an isolated environment.
|
|
56
56
|
4. **Tiered Verification:** Enforces scope bounds (`git diff`), runs fast-fail linters/type-checks, then executes full test/build suites.
|
|
57
|
-
5. **Self-
|
|
58
|
-
6. **Clean Pull Request:** Once verified green, commits & pushes clean code to GitHub.
|
|
57
|
+
5. **Self-Healing PR Gatekeeper (OODA Feedback):** Parses execution stderr traces, logs diagnostic telemetry to `.agent/history/metrics.jsonl`, and enforces strict exit code 1 to block bad code from merging.
|
|
59
58
|
|
|
60
59
|
<details>
|
|
61
60
|
<summary><b>š View System Architecture Diagram (For Power Users)</b></summary>
|
|
@@ -75,33 +74,22 @@ sequenceDiagram
|
|
|
75
74
|
Orc->>Git: Provision Worktree (`git worktree add`)
|
|
76
75
|
Orc->>Jules: Dispatch Context, Invariants & Target Scope
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Orc->>Git: Run Fast-Fail Checks (Lint / Typecheck)
|
|
87
|
-
opt Pass Static Checks
|
|
88
|
-
Orc->>Git: Run Heavy Suite (`build_cmd` & `test_cmd`)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
alt Verification Gates Pass
|
|
93
|
-
Orc->>Git: Commit, Push & Cleanup Worktree
|
|
94
|
-
Orc->>CLI: Return Success + Metrics (.agent/history/metrics.jsonl)
|
|
95
|
-
note over Jules,Git: Exit Loop
|
|
96
|
-
else Verification Gates Fail (Attempts < 4)
|
|
97
|
-
Git-->>Orc: Execution Trace (stdout/stderr / Diff)
|
|
98
|
-
Orc->>Jules: Inject OODA Feedback & Error Context
|
|
99
|
-
end
|
|
77
|
+
Jules->>Git: Propose Code Mutations
|
|
78
|
+
|
|
79
|
+
note over Orc,Git: Phase 2: Tiered Verification Gatekeeper
|
|
80
|
+
Orc->>Git: Scope Audit (`git diff -z --name-only` vs forbidden_paths)
|
|
81
|
+
alt Scope Breach
|
|
82
|
+
Git-->>Orc: Scope Violation Error (Exit 1)
|
|
83
|
+
else Scope OK
|
|
84
|
+
Orc->>Git: Run Verification Suite (`test_cmd` & `build_cmd`)
|
|
100
85
|
end
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
Orc->>Git:
|
|
104
|
-
Orc->>CLI: Return
|
|
87
|
+
alt Verification Gates Pass
|
|
88
|
+
Orc->>Git: Commit, Push & Log Telemetry (.agent/history/metrics.jsonl)
|
|
89
|
+
Orc->>CLI: Return Success (Exit 0)
|
|
90
|
+
else Verification Gates Fail
|
|
91
|
+
Git-->>Orc: Execution Trace (stdout/stderr / Diff)
|
|
92
|
+
Orc->>CLI: Log OODA Diagnostic Telemetry & Block PR (Exit 1)
|
|
105
93
|
end
|
|
106
94
|
```
|
|
107
95
|
|
|
@@ -151,28 +139,29 @@ allow_paths: []
|
|
|
151
139
|
|
|
152
140
|
> š”ļø **Zero-Trust Security Model**: `allow_paths` and `forbidden_paths` rules are read **strictly from the target base branch** (`origin/main`), never from untrusted PR branches. Even if an AI agent hallucinates and tries to modify its own security rules in a PR branch, the Orchestrator enforces the immutable rules defined on `main`.
|
|
153
141
|
> ā¹ļø Setting `build_cmd: ""` explicitly skips the build verification step (useful for pure test suites or scripts). Note that `.agent/jules.yml` uses a zero-dependency parser that supports flow (`[...]`) and block (`- item`) list subsets.
|
|
142
|
+
> š¦ `.github/` is intentionally included in `package.json`'s `files` array so that `npx jules-init` can automatically scaffold `.github/workflows/jules-audit.yml` into target repositories.
|
|
154
143
|
|
|
155
144
|
---
|
|
156
145
|
|
|
157
146
|
<details>
|
|
158
147
|
<summary><b>š ļø Supported Language Manifests & Workspace Graphs (15+ Tech Stacks)</b></summary>
|
|
159
148
|
|
|
160
|
-
| Stack / Ecosystem | Manifest / Workspace File |
|
|
161
|
-
|
|
162
|
-
| **Turborepo** | `turbo.json` | `npx turbo run test --filter=<pkg>...` |
|
|
163
|
-
| **pnpm Workspace** | `pnpm-workspace.yaml` | `pnpm --filter=...<pkg> test` |
|
|
164
|
-
| **Nx Workspace** | `nx.json` | `npx nx run-many -t test -p <pkg> --with-deps` |
|
|
165
|
-
| **Bun** | `bunfig.toml` / `bun.lockb` | `bun test
|
|
166
|
-
| **Deno** | `deno.json` / `deno.jsonc` | `deno test
|
|
167
|
-
| **JavaScript / TypeScript** | `package.json` | `npm run lint && npm test`
|
|
168
|
-
| **Rust** | `Cargo.toml` | `cargo test
|
|
169
|
-
| **Go** | `go.mod` | `go test
|
|
170
|
-
| **Python** | `pyproject.toml` / `requirements.txt` | `pytest` |
|
|
171
|
-
| **Elixir** | `mix.exs` | `mix test
|
|
172
|
-
| **Ruby** | `Gemfile` | `bundle exec rake test` |
|
|
173
|
-
| **Swift** | `Package.swift` | `swift test
|
|
174
|
-
| **Java (Maven/Gradle)** | `pom.xml` / `build.gradle` | `mvn test` / `./gradlew test` |
|
|
175
|
-
| **C / C++** | `Makefile` | `make test
|
|
149
|
+
| Stack / Ecosystem | Manifest / Workspace File | Test Command (`testCmd`) | Build Command (`buildCmd`) |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| **Turborepo** | `turbo.json` | `npx turbo run test --filter=<pkg>...` | `npx turbo run build --filter=<pkg>...` |
|
|
152
|
+
| **pnpm Workspace** | `pnpm-workspace.yaml` | `pnpm --filter=...<pkg> test` | `pnpm --filter=...<pkg> build` |
|
|
153
|
+
| **Nx Workspace** | `nx.json` | `npx nx run-many -t test -p <pkg> --with-deps` | `npx nx run-many -t build -p <pkg> --with-deps` |
|
|
154
|
+
| **Bun** | `bunfig.toml` / `bun.lockb` | `bun test` | `bun run build` |
|
|
155
|
+
| **Deno** | `deno.json` / `deno.jsonc` | `deno test` | `deno task build` |
|
|
156
|
+
| **JavaScript / TypeScript** | `package.json` | `npm run lint && npm test` | `npm run build` |
|
|
157
|
+
| **Rust** | `Cargo.toml` | `cargo test --workspace` | `cargo build` |
|
|
158
|
+
| **Go** | `go.mod` | `go test ./...` | `go build ./...` |
|
|
159
|
+
| **Python** | `pyproject.toml` / `requirements.txt` | `pytest` | *(none)* |
|
|
160
|
+
| **Elixir** | `mix.exs` | `mix test` | `mix compile` |
|
|
161
|
+
| **Ruby** | `Gemfile` | `bundle exec rake test` | *(none)* |
|
|
162
|
+
| **Swift** | `Package.swift` | `swift test` | `swift build` |
|
|
163
|
+
| **Java (Maven/Gradle)** | `pom.xml` / `build.gradle` | `mvn test` / `./gradlew test` | `mvn compile` / `./gradlew assemble` |
|
|
164
|
+
| **C / C++** | `Makefile` | `make test` | `make build` |
|
|
176
165
|
|
|
177
166
|
</details>
|
|
178
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/FullThrottle83/jules-orchestrator-kit.git"
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/",
|
|
19
19
|
"scripts/",
|
|
20
|
-
".agent/",
|
|
20
|
+
".agent/jules.yml",
|
|
21
|
+
".agent/rules/",
|
|
22
|
+
".agent/workflows/",
|
|
23
|
+
".agent/jules-queue/README.md",
|
|
21
24
|
".github/",
|
|
22
25
|
".env.example",
|
|
23
26
|
"JULES_RULES_TEMPLATE.md",
|
|
@@ -28,7 +28,7 @@ function loadEnv() {
|
|
|
28
28
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
29
29
|
const eqIdx = trimmed.indexOf("=");
|
|
30
30
|
if (eqIdx > 0) {
|
|
31
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
31
|
+
const key = trimmed.slice(0, eqIdx).trim().replace(/^export\s+/, "");
|
|
32
32
|
let val = trimmed.slice(eqIdx + 1).trim();
|
|
33
33
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
34
34
|
val = val.slice(1, -1);
|
|
@@ -162,9 +162,23 @@ const repo = process.env.JULES_REPO || "";
|
|
|
162
162
|
|
|
163
163
|
if (isMainModule) {
|
|
164
164
|
const possiblePath = path.resolve(process.cwd(), taskPromptArg);
|
|
165
|
-
if (fs.existsSync(possiblePath)
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
if (fs.existsSync(possiblePath)) {
|
|
166
|
+
try {
|
|
167
|
+
const verifiedPath = assertPathWithinWorkspace(possiblePath);
|
|
168
|
+
const fd = fs.openSync(verifiedPath, "r");
|
|
169
|
+
try {
|
|
170
|
+
const stat = fs.fstatSync(fd);
|
|
171
|
+
if (stat.isFile()) {
|
|
172
|
+
rawPrompt = fs.readFileSync(fd, "utf-8");
|
|
173
|
+
} else {
|
|
174
|
+
rawPrompt = taskPromptArg;
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
fs.closeSync(fd);
|
|
178
|
+
}
|
|
179
|
+
} catch (_) {
|
|
180
|
+
rawPrompt = taskPromptArg;
|
|
181
|
+
}
|
|
168
182
|
} else {
|
|
169
183
|
rawPrompt = taskPromptArg;
|
|
170
184
|
}
|
|
@@ -210,7 +224,9 @@ function cleanupTmp() {
|
|
|
210
224
|
if (fs.existsSync(tmpDir)) {
|
|
211
225
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
212
226
|
}
|
|
213
|
-
} catch (
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.warn(`ā ļø Failed to remove temporary payload directory at ${tmpDir}:`, err.message);
|
|
229
|
+
}
|
|
214
230
|
}
|
|
215
231
|
|
|
216
232
|
async function executeDispatch() {
|
|
@@ -110,7 +110,13 @@ function main() {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
logNightlyHistory(results, isDryRun);
|
|
113
|
-
|
|
113
|
+
const hasFailures = results.some((r) => r.status && (r.status.includes("Failed") || r.status.includes("error")));
|
|
114
|
+
if (hasFailures) {
|
|
115
|
+
console.error("\nā Nightly Jules audit suite completed with task errors.");
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
} else {
|
|
118
|
+
console.log("\nā
Nightly Jules audit suite execution completed cleanly.");
|
|
119
|
+
}
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
main();
|
|
@@ -11,6 +11,8 @@ const queueDir = path.resolve(process.cwd(), ".agent/jules-queue");
|
|
|
11
11
|
const completedDir = path.resolve(process.cwd(), ".agent/jules-queue/completed");
|
|
12
12
|
const queueLogFile = path.join(queueDir, "queue.jsonl");
|
|
13
13
|
|
|
14
|
+
const processingDir = path.resolve(process.cwd(), ".agent/jules-queue/.processing");
|
|
15
|
+
|
|
14
16
|
if (!fs.existsSync(queueDir)) {
|
|
15
17
|
console.error(`ā Queue directory not found: ${queueDir}`);
|
|
16
18
|
process.exit(1);
|
|
@@ -20,6 +22,10 @@ if (!fs.existsSync(completedDir)) {
|
|
|
20
22
|
fs.mkdirSync(completedDir, { recursive: true });
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
if (!fs.existsSync(processingDir)) {
|
|
26
|
+
fs.mkdirSync(processingDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function logQueueState(file, status, error = null) {
|
|
24
30
|
const entry = JSON.stringify({
|
|
25
31
|
timestamp: new Date().toISOString(),
|
|
@@ -43,6 +49,16 @@ console.log(`š Found ${files.length} tasks in the queue. Processing...`);
|
|
|
43
49
|
|
|
44
50
|
files.forEach((file, index) => {
|
|
45
51
|
const filePath = path.join(queueDir, file);
|
|
52
|
+
const processingPath = path.join(processingDir, file);
|
|
53
|
+
|
|
54
|
+
// Atomic claim: move from queueDir to processingDir before dispatching
|
|
55
|
+
try {
|
|
56
|
+
fs.renameSync(filePath, processingPath);
|
|
57
|
+
} catch (claimErr) {
|
|
58
|
+
// Another runner already claimed this task file
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
// Generate title from filename (e.g. "TASK-001-auth-spec.md" -> "TASK 001 auth spec")
|
|
47
63
|
const title = file.replace(/\.md$/, "").replace(/-/g, " ");
|
|
48
64
|
|
|
@@ -51,13 +67,13 @@ files.forEach((file, index) => {
|
|
|
51
67
|
logQueueState(file, "RUNNING");
|
|
52
68
|
|
|
53
69
|
try {
|
|
54
|
-
execFileSync("node", [dispatchScript, title,
|
|
70
|
+
execFileSync("node", [dispatchScript, title, processingPath], {
|
|
55
71
|
stdio: "inherit",
|
|
56
72
|
});
|
|
57
73
|
|
|
58
74
|
// Move to completed
|
|
59
75
|
const destPath = path.join(completedDir, file);
|
|
60
|
-
fs.renameSync(
|
|
76
|
+
fs.renameSync(processingPath, destPath);
|
|
61
77
|
logQueueState(file, "COMPLETED");
|
|
62
78
|
console.log(`ā
Moved ${file} to completed/`);
|
|
63
79
|
} catch (error) {
|
|
@@ -17,7 +17,9 @@ function runCommand(cmd, ignoreError = false) {
|
|
|
17
17
|
|
|
18
18
|
export function matchGlob(filepath, globPattern) {
|
|
19
19
|
const cleanPath = filepath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
20
|
-
|
|
20
|
+
// Normalize consecutive glob wildcards (e.g., "**/**" -> "**")
|
|
21
|
+
const normalizedGlob = globPattern.replace(/\\/g, "/").replace(/(?:\*\*\/)+/g, "**/");
|
|
22
|
+
const segments = normalizedGlob.split("/");
|
|
21
23
|
const regexParts = [];
|
|
22
24
|
for (let i = 0; i < segments.length; i++) {
|
|
23
25
|
const seg = segments[i];
|
|
@@ -33,12 +35,14 @@ export function matchGlob(filepath, globPattern) {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
let patternStr = regexParts.join("/");
|
|
36
|
-
patternStr = patternStr.replace(/(
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
38
|
+
patternStr = patternStr.replace(/(?:^|\/)\*\*(?:\/|$)/g, (m) => {
|
|
39
|
+
if (m === "**") return ".*";
|
|
40
|
+
if (m === "/**/") return "(?:/|/.*/)";
|
|
41
|
+
if (m === "/**") return "(?:/.*)?";
|
|
42
|
+
if (m === "**/") return "(?:.*/)?";
|
|
40
43
|
return ".*";
|
|
41
44
|
});
|
|
45
|
+
patternStr = patternStr.replace(/\*\*/g, ".*");
|
|
42
46
|
return new RegExp(`^${patternStr}$`, "i").test(cleanPath);
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -166,9 +170,9 @@ export function runSelfAudit() {
|
|
|
166
170
|
|
|
167
171
|
console.log(`š Merge-Base Hash: ${mergeBase}`);
|
|
168
172
|
|
|
169
|
-
const rawDiffFiles = runCommand(`git diff --name-only ${mergeBase}...HEAD`)
|
|
170
|
-
.split("\
|
|
171
|
-
.map((f) => f.trim())
|
|
173
|
+
const rawDiffFiles = runCommand(`git diff -z --name-only ${mergeBase}...HEAD`)
|
|
174
|
+
.split("\0")
|
|
175
|
+
.map((f) => f.trim().replace(/^["']|["']$/g, ""))
|
|
172
176
|
.filter(Boolean);
|
|
173
177
|
|
|
174
178
|
const isBloatFile = (file) => /(\.lock|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.png|\.jpg|\.jpeg|\.pdf|\.min\.js|\.map)$/i.test(file);
|
package/scripts/jules-swarm.mjs
CHANGED
|
@@ -38,31 +38,70 @@ process.env.JULES_PROJECT_ROOT = process.cwd();
|
|
|
38
38
|
|
|
39
39
|
console.log(`š Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const activeWorktrees = new Set();
|
|
42
|
+
|
|
43
|
+
function cleanupAllWorktreesSync() {
|
|
44
|
+
for (const item of Array.from(activeWorktrees)) {
|
|
45
|
+
if (item.wtDir && fs.existsSync(item.wtDir)) {
|
|
46
|
+
try {
|
|
47
|
+
fs.chmodSync(item.wtDir, 0o755);
|
|
48
|
+
} catch (_) {}
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("git", ["worktree", "remove", "--force", item.wtDir], { stdio: "ignore" });
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
if (item.branchName) {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync("git", ["branch", "-D", item.branchName], { stdio: "ignore" });
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
activeWorktrees.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
process.on("SIGINT", () => {
|
|
63
|
+
console.warn("\nā ļø SIGINT received. Cleaning up active Git worktrees...");
|
|
64
|
+
cleanupAllWorktreesSync();
|
|
65
|
+
process.exit(130);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
process.on("SIGTERM", () => {
|
|
69
|
+
console.warn("\nā ļø SIGTERM received. Cleaning up active Git worktrees...");
|
|
70
|
+
cleanupAllWorktreesSync();
|
|
71
|
+
process.exit(143);
|
|
72
|
+
});
|
|
42
73
|
|
|
43
74
|
async function createWorktree(taskId) {
|
|
44
75
|
const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
45
76
|
const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
|
|
46
77
|
const branchName = `jules/${slug}`;
|
|
78
|
+
let item = null;
|
|
47
79
|
try {
|
|
48
80
|
if (!fs.existsSync(path.dirname(wtDir))) {
|
|
49
81
|
fs.mkdirSync(path.dirname(wtDir), { recursive: true });
|
|
50
82
|
}
|
|
51
83
|
await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
|
|
52
|
-
|
|
84
|
+
item = { wtDir, branchName };
|
|
53
85
|
} catch (err) {
|
|
54
86
|
try {
|
|
55
87
|
await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
|
|
56
|
-
|
|
88
|
+
item = { wtDir, branchName: null };
|
|
57
89
|
} catch (fallbackErr) {
|
|
58
90
|
console.warn(`ā ļø Failed to create worktree for ${taskId}:`, fallbackErr.message);
|
|
59
91
|
return null;
|
|
60
92
|
}
|
|
61
93
|
}
|
|
94
|
+
if (item) activeWorktrees.add(item);
|
|
95
|
+
return item;
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
async function removeWorktree(wtDir, branchName) {
|
|
65
99
|
if (!wtDir) return;
|
|
100
|
+
for (const item of Array.from(activeWorktrees)) {
|
|
101
|
+
if (item.wtDir === wtDir) {
|
|
102
|
+
activeWorktrees.delete(item);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
66
105
|
try {
|
|
67
106
|
await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
|
|
68
107
|
} catch (err) {
|