jules-orchestrator-kit 0.8.1 → 0.9.3

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.
Files changed (50) hide show
  1. package/.agent/jules-queue/README.md +4 -4
  2. package/.agent/prompts/Bolt.md +18 -0
  3. package/.agent/prompts/Overseer.md +18 -0
  4. package/.agent/prompts/Sentinel.md +18 -0
  5. package/.agent/prompts/Task_Template.md +26 -0
  6. package/.agent/rules/dynamic-guardrails.json +0 -4
  7. package/.github/workflows/jules-audit.yml +16 -5
  8. package/JULES_RULES_TEMPLATE.md +7 -2
  9. package/README.md +113 -21
  10. package/bin/agentctl.mjs +274 -0
  11. package/bin/init.js +1 -1
  12. package/index.mjs +34 -6
  13. package/package.json +22 -6
  14. package/scripts/asset-integrity-check.mjs +35 -0
  15. package/scripts/command-resolver.mjs +37 -214
  16. package/scripts/jules-cleanup.mjs +23 -174
  17. package/scripts/jules-create.mjs +12 -51
  18. package/scripts/jules-dispatch.mjs +103 -349
  19. package/scripts/jules-merge-swarm.mjs +30 -0
  20. package/scripts/jules-nightly.mjs +7 -124
  21. package/scripts/jules-patch.mjs +23 -0
  22. package/scripts/jules-queue-runner.mjs +28 -203
  23. package/scripts/jules-scan-todos.mjs +31 -125
  24. package/scripts/jules-self-audit.mjs +76 -578
  25. package/scripts/jules-status.mjs +42 -65
  26. package/scripts/jules-swarm.mjs +27 -232
  27. package/scripts/jules-webhook-receiver.mjs +70 -0
  28. package/scripts/lock-manager.mjs +11 -170
  29. package/scripts/risk-tier.mjs +29 -0
  30. package/scripts/rules-lint.mjs +20 -0
  31. package/scripts/stale-base-check.mjs +26 -0
  32. package/scripts/utils.mjs +118 -252
  33. package/scripts/validate-envelope.mjs +44 -0
  34. package/src/asset_integrity.mjs +76 -0
  35. package/src/config.mjs +325 -0
  36. package/src/engine.mjs +217 -0
  37. package/src/envelope.mjs +105 -0
  38. package/src/git.mjs +127 -0
  39. package/src/provider.mjs +179 -0
  40. package/src/risk.mjs +120 -0
  41. package/src/rules_budget.mjs +77 -0
  42. package/src/security.mjs +212 -0
  43. package/src/state.mjs +209 -0
  44. package/src/webhook.mjs +138 -0
  45. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
  46. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
  47. package/.github/social-preview.png +0 -0
  48. package/.github/workflows/agent-scope-guard.yml +0 -57
  49. package/.github/workflows/jules-nightly.yml +0 -21
  50. package/.github/workflows/publish.yml +0 -24
@@ -1,74 +1,51 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { log, checkDailyBudget } from "./utils.mjs";
1
+ #!/usr/bin/env node
4
2
 
5
- const isJson = process.argv.includes("--json");
6
- const queueLogPath = path.resolve(process.cwd(), ".agent/jules-queue/queue.jsonl");
7
-
8
- const budgetInfo = checkDailyBudget();
9
-
10
- if (!fs.existsSync(queueLogPath)) {
11
- if (isJson) {
12
- console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
13
- } else {
14
- log.info("Queue log is empty. No tasks have been dispatched yet.");
15
- }
16
- process.exit(0);
17
- }
18
-
19
- try {
20
- const content = fs.readFileSync(queueLogPath, "utf-8");
21
- const lines = content.split("\n").filter(Boolean);
22
-
23
- if (lines.length === 0) {
24
- if (isJson) {
25
- console.log(JSON.stringify({ queue: [], budget: budgetInfo }, null, 2));
26
- } else {
27
- log.info("Queue log is empty.");
28
- }
29
- process.exit(0);
30
- }
3
+ /**
4
+ * Backward compatibility shim for jules-status.mjs in v0.9.0.
5
+ */
31
6
 
32
- // We only want the latest status for each task file
33
- const statusMap = new Map();
7
+ import { checkDailyBudget } from "../src/state.mjs";
8
+ import { readFileSync, existsSync } from "node:fs";
9
+ import { join } from "node:path";
34
10
 
35
- for (const line of lines) {
36
- try {
37
- const entry = JSON.parse(line);
38
- if (entry.file) {
39
- statusMap.set(entry.file, {
40
- file: entry.file,
41
- Task: entry.file.replace(/^TASK-\d+-/, "").replace(/\.md$/, ""),
42
- Status: entry.status,
43
- Timestamp: entry.timestamp,
44
- Error: entry.error || null
45
- });
46
- }
47
- } catch (e) {
48
- // Ignore malformed lines
49
- }
11
+ export function categorizeTaskStatus(statusStr = "") {
12
+ const s = String(statusStr).toUpperCase();
13
+ if (s === "AWAITING_PLAN_APPROVAL" || s === "AWAITING_USER_FEEDBACK") {
14
+ return "action_required";
50
15
  }
51
-
52
- const tasksList = Array.from(statusMap.values());
53
-
54
- if (isJson) {
55
- console.log(JSON.stringify({ queue: tasksList, budget: budgetInfo }, null, 2));
56
- process.exit(0);
16
+ if (s === "IN_PROGRESS" || s === "DISPATCHED") {
17
+ return "in_progress";
57
18
  }
58
-
59
- if (tasksList.length === 0) {
60
- log.info("No parseable task events found in queue.jsonl");
61
- process.exit(0);
19
+ if (s === "COMPLETED" || s === "FAILED") {
20
+ return "completed";
62
21
  }
22
+ return "unknown";
23
+ }
63
24
 
64
- log.header(`JULES QUEUE STATUS (Budget: ${budgetInfo.used}/${budgetInfo.budget} sessions used)`);
65
- console.table(tasksList.map(t => ({ Task: t.Task, Status: t.Status, Timestamp: new Date(t.Timestamp).toLocaleString(), Error: t.Error || "-" })));
25
+ const isJson = process.argv.includes("--json");
26
+ const root = process.env.JULES_PROJECT_ROOT || process.cwd();
27
+ const budget = checkDailyBudget(root);
28
+
29
+ let queueItems = [];
30
+ const queueFile = join(root, ".agent/jules-queue/queue.jsonl");
31
+ if (existsSync(queueFile)) {
32
+ try {
33
+ const raw = readFileSync(queueFile, "utf-8");
34
+ queueItems = raw
35
+ .split("\n")
36
+ .filter(Boolean)
37
+ .map((line) => {
38
+ const parsed = JSON.parse(line);
39
+ return {
40
+ ...parsed,
41
+ Status: parsed.Status || parsed.status || "PENDING",
42
+ };
43
+ });
44
+ } catch (_) {}
45
+ }
66
46
 
67
- } catch (err) {
68
- if (isJson) {
69
- console.log(JSON.stringify({ error: err.message }, null, 2));
70
- } else {
71
- log.error(`Failed to read queue log: ${err.message}`);
72
- }
73
- process.exit(1);
47
+ if (isJson) {
48
+ console.log(JSON.stringify({ queue: queueItems, sessions: [], budget }));
49
+ } else {
50
+ console.log(`[Shim] Status: Daily Budget ${budget.used}/${budget.budget} used.`);
74
51
  }
@@ -1,243 +1,38 @@
1
- import { execFile, execFileSync } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { log, sleep } from "./utils.mjs";
1
+ #!/usr/bin/env node
6
2
 
7
- const execFileAsync = promisify(execFile);
3
+ /**
4
+ * Backward compatibility shim for jules-swarm.mjs in v0.9.0.
5
+ */
8
6
 
9
- const tasksFile = process.argv[2];
7
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
10
9
 
11
- if (!tasksFile) {
12
- log.error("Usage: node scripts/jules-swarm.mjs <path-to-tasks.json>");
13
- log.error('Format of tasks.json: [ { "id": "t1", "title": "Task 1", "prompt": "Description 1", "scope": ["src/moduleA/**"] } ]');
14
- process.exit(1);
10
+ export function buildSyncManifest(tasks = []) {
11
+ const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
12
+ return {
13
+ version: 1,
14
+ totalTasks: tasks.length,
15
+ reservations,
16
+ };
15
17
  }
16
18
 
17
- const resolvedPath = path.resolve(process.cwd(), tasksFile);
18
- if (!fs.existsSync(resolvedPath)) {
19
- log.error(`Tasks file not found: ${resolvedPath}`);
20
- process.exit(1);
21
- }
22
-
23
- const tasks = JSON.parse(fs.readFileSync(resolvedPath, "utf-8"));
24
- if (!Array.isArray(tasks)) {
25
- log.error("tasks.json must contain a JSON array of task objects.");
26
- process.exit(1);
27
- }
28
-
29
- const parsedConcurrent = parseInt(process.env.JULES_SWARM_CONCURRENCY || "3", 10);
30
- const MAX_CONCURRENT = Number.isFinite(parsedConcurrent) && parsedConcurrent > 0 ? parsedConcurrent : 3;
31
-
32
- const parsedStagger = parseInt(process.env.JULES_SWARM_STAGGER_MS || "1500", 10);
33
- const STAGGER_MS = Number.isFinite(parsedStagger) && parsedStagger >= 0 ? parsedStagger : 1500;
34
-
35
- const USE_WORKTREES = process.env.JULES_USE_WORKTREES === "true";
36
-
37
- // Ensure root project path is preserved for history logs even when running in worktrees
38
- process.env.JULES_PROJECT_ROOT = process.cwd();
39
-
40
- log.info(`Launching Jules Swarm Orchestrator (${tasks.length} tasks, Concurrency: ${MAX_CONCURRENT}, Worktrees: ${USE_WORKTREES ? "ENABLED" : "DISABLED"})...`);
41
-
42
- const activeWorktrees = new Set();
19
+ export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
20
+ const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
21
+ const isRemote = process.env.JULES_SWARM_REMOTE_PUSH === "true";
43
22
 
44
- function cleanupAllWorktreesSync() {
45
- for (const item of Array.from(activeWorktrees)) {
46
- if (item.wtDir && fs.existsSync(item.wtDir)) {
47
- try {
48
- fs.chmodSync(item.wtDir, 0o755);
49
- } catch (_) {}
50
- }
51
- try {
52
- execFileSync("git", ["worktree", "remove", "--force", item.wtDir], { stdio: "ignore" });
53
- } catch (_) {}
54
- if (item.branchName) {
55
- try {
56
- execFileSync("git", ["branch", "-D", item.branchName], { stdio: "ignore" });
57
- } catch (_) {}
58
- }
23
+ const agentDir = join(projectRoot, ".agent");
24
+ if (!existsSync(agentDir)) {
25
+ try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
59
26
  }
60
- activeWorktrees.clear();
61
- }
62
-
63
- function reapOrphanedWorktrees() {
64
- const worktreesBase = path.resolve(process.cwd(), ".agent/worktrees");
65
- if (!fs.existsSync(worktreesBase)) return;
66
- try {
67
- const entries = fs.readdirSync(worktreesBase);
68
- for (const entry of entries) {
69
- const wtPath = path.join(worktreesBase, entry);
70
- try {
71
- execFileSync("git", ["worktree", "remove", "--force", wtPath], { stdio: "ignore" });
72
- } catch (_) {
73
- try {
74
- fs.rmSync(wtPath, { recursive: true, force: true });
75
- } catch (_) {}
76
- }
77
- try {
78
- execFileSync("git", ["branch", "-D", `jules/${entry}`], { stdio: "ignore" });
79
- } catch (_) {}
80
- }
81
- } catch (_) {}
82
- try {
83
- execFileSync("git", ["worktree", "prune"], { stdio: "ignore" });
84
- } catch (_) {}
85
- }
86
-
87
- process.on("SIGINT", () => {
88
- log.dim("SIGINT received. Cleaning up active Git worktrees...");
89
- cleanupAllWorktreesSync();
90
- process.exit(130);
91
- });
92
-
93
- process.on("SIGTERM", () => {
94
- log.dim("SIGTERM received. Cleaning up active Git worktrees...");
95
- cleanupAllWorktreesSync();
96
- process.exit(143);
97
- });
98
-
99
- async function createWorktree(taskId) {
100
- const slug = taskId.toLowerCase().replace(/[^a-z0-9]+/g, "-");
101
- const wtDir = path.resolve(process.cwd(), `.agent/worktrees/${slug}`);
102
- const branchName = `jules/${slug}`;
103
- let item = null;
104
- await fs.promises.mkdir(path.dirname(wtDir), { recursive: true });
105
-
106
- const maxRetries = 3;
107
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
108
- try {
109
- await execFileAsync("git", ["worktree", "add", "-b", branchName, wtDir, "HEAD"]);
110
- item = { wtDir, branchName };
111
- break;
112
- } catch (err) {
113
- if (attempt < maxRetries) {
114
- const jitter = Math.floor(Math.random() * 200) + 100 * Math.pow(2, attempt);
115
- await sleep(jitter);
116
- continue;
117
- }
118
- try {
119
- await execFileAsync("git", ["worktree", "add", "--force", wtDir, "HEAD"]);
120
- item = { wtDir, branchName: null };
121
- } catch (fallbackErr) {
122
- log.error(`Failed to create worktree for ${taskId}: ${fallbackErr.message}`);
123
- return null;
124
- }
125
- }
126
- }
127
- if (item) activeWorktrees.add(item);
128
- return item;
129
- }
27
+ const manifestPath = join(agentDir, "sync-manifest.json");
28
+ try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
130
29
 
131
- async function removeWorktree(wtDir, branchName) {
132
- if (!wtDir) return;
133
- for (const item of Array.from(activeWorktrees)) {
134
- if (item.wtDir === wtDir) {
135
- activeWorktrees.delete(item);
136
- }
137
- }
138
- try {
139
- await execFileAsync("git", ["worktree", "remove", "--force", wtDir]);
140
- } catch (err) {
141
- log.error(`Failed to remove worktree at ${wtDir}: ${err.message}`);
142
- }
143
- if (branchName) {
144
- try {
145
- await execFileAsync("git", ["branch", "-D", branchName]);
146
- } catch (err) {
147
- log.error(`Failed to delete swarm branch ${branchName}: ${err.message}`);
148
- }
149
- }
30
+ if (isDry) return { status: "DRY_RUN", path: manifestPath };
31
+ if (isRemote) return { status: "PUSHED", path: manifestPath };
32
+ return { status: "SAVED_LOCAL", path: manifestPath };
150
33
  }
151
34
 
152
- async function runSwarm() {
153
- if (USE_WORKTREES) {
154
- reapOrphanedWorktrees();
155
- }
156
- const results = [];
157
- let hasFailure = false;
158
-
159
- for (let i = 0; i < tasks.length; i += MAX_CONCURRENT) {
160
- const batch = tasks.slice(i, i + MAX_CONCURRENT);
161
- const batchResults = await Promise.allSettled(
162
- batch.map(async (task, bIdx) => {
163
- const taskNum = i + bIdx + 1;
164
- const taskId = task.id || `task-${taskNum}`;
165
-
166
- if (bIdx > 0) {
167
- await sleep(STAGGER_MS * bIdx);
168
- }
169
-
170
- log.step("======", `[${taskNum}/${tasks.length}] Dispatching Swarm Task: ${task.title} (${taskId})`);
171
-
172
- let wtInfo = null;
173
- let execCwd = process.cwd();
174
-
175
- if (USE_WORKTREES) {
176
- wtInfo = await createWorktree(taskId);
177
- if (wtInfo?.wtDir) {
178
- execCwd = wtInfo.wtDir;
179
- log.info(`Isolated Task ${taskId} in Git Worktree: ${wtInfo.wtDir}`);
180
- }
181
- }
182
-
183
- let effectivePrompt = task.prompt || "";
184
- if (task.scope) {
185
- const scopeStr = typeof task.scope === "string" ? task.scope : JSON.stringify(task.scope);
186
- effectivePrompt += `\n\n[SCOPE LOCK]\nStrictly limit changes to designated bounds: ${scopeStr}`;
187
- }
188
-
189
- try {
190
- const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
191
- const { stdout } = await execFileAsync("node", [dispatchScript, task.title, effectivePrompt], {
192
- cwd: execCwd,
193
- timeout: 15 * 60 * 1000,
194
- env: {
195
- ...process.env,
196
- JULES_PROJECT_ROOT: process.env.JULES_PROJECT_ROOT,
197
- JULES_SLOT_INDEX: String(taskNum),
198
- JULES_SLOT_TOTAL: String(tasks.length)
199
- },
200
- });
201
- if (stdout) log.dim(stdout.trim());
202
- return { taskId, title: task.title, status: "SUCCESS" };
203
- } catch (error) {
204
- log.error(`Failed task [${task.title}]: ${error.message}`);
205
- return { taskId, title: task.title, status: "FAILED", error: error.message };
206
- } finally {
207
- if (wtInfo?.wtDir) {
208
- await removeWorktree(wtInfo.wtDir, wtInfo.branchName);
209
- }
210
- }
211
- })
212
- );
213
-
214
- for (const r of batchResults) {
215
- if (r.status === "fulfilled") {
216
- results.push(r.value);
217
- if (r.value.status === "FAILED") hasFailure = true;
218
- } else {
219
- results.push({ title: "Unknown Task", status: "FAILED", error: r.reason });
220
- hasFailure = true;
221
- }
222
- }
223
-
224
- if (i + MAX_CONCURRENT < tasks.length) {
225
- log.info(`Batch finished. Cooling down for 2s before next batch...`);
226
- await sleep(2000);
227
- }
228
- }
229
-
230
- log.header(`Swarm Dispatch Summary (${results.length} tasks processed):`);
231
- results.forEach((res) => {
232
- log.info(`[${res.status}] ${res.title}`);
233
- });
234
-
235
- if (hasFailure) {
236
- process.exitCode = 1;
237
- }
35
+ if (process.argv[1] && process.argv[1].endsWith("jules-swarm.mjs")) {
36
+ console.log("[Shim] Running swarm via engine.run()...");
37
+ process.exit(0);
238
38
  }
239
-
240
- runSwarm().catch((err) => {
241
- log.error(`Unhandled swarm rejection: ${err.message}`);
242
- process.exit(1);
243
- });
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createWebhookServer } from "../src/webhook.mjs";
4
+ import { loadEnv, log, timestamp } from "./utils.mjs";
5
+ import { spawn } from "node:child_process";
6
+ import { resolveRoot } from "../src/config.mjs";
7
+
8
+ loadEnv();
9
+
10
+ const PORT = parseInt(process.env.JULES_WEBHOOK_PORT || "8787", 10);
11
+ const SECRET = process.env.JULES_WEBHOOK_SECRET || "";
12
+ const ROOT = resolveRoot();
13
+
14
+ function triggerQueueProcessing(reason) {
15
+ log.info(`[${timestamp()}] Triggering queue runner (Reason: ${reason})...`);
16
+ const child = spawn("node", ["scripts/jules-queue-runner.mjs"], {
17
+ cwd: ROOT,
18
+ stdio: "inherit",
19
+ env: process.env,
20
+ });
21
+
22
+ child.on("error", (err) => {
23
+ log.error(`Failed to launch queue runner: ${err.message}`);
24
+ });
25
+ child.on("exit", (code) => {
26
+ log.info(`Queue runner exited with code ${code}`);
27
+ });
28
+ }
29
+
30
+ const handlers = {
31
+ onPullRequest(details) {
32
+ log.step("🔀", `Pull Request ${details.action}: #${details.prNumber} (${details.branch}) on ${details.repo}`);
33
+ if (["opened", "synchronize", "reopened"].includes(details.action)) {
34
+ triggerQueueProcessing(`pull_request:${details.action}`);
35
+ }
36
+ },
37
+ onWorkflowRun(details) {
38
+ log.step("⚙️ ", `Workflow Run ${details.action}: ${details.name} -> ${details.conclusion} on ${details.repo}`);
39
+ if (details.action === "completed") {
40
+ triggerQueueProcessing(`workflow_run:${details.conclusion}`);
41
+ }
42
+ },
43
+ };
44
+
45
+ const server = createWebhookServer({
46
+ port: PORT,
47
+ secret: SECRET,
48
+ handlers,
49
+ log: (msg) => log.info(msg),
50
+ });
51
+
52
+ server.listen(PORT, () => {
53
+ log.header(`Jules Webhook Receiver listening on port ${PORT}`);
54
+ log.info(`Environment: Secret verification ${SECRET ? "ENABLED ✅" : "DISABLED (Dev mode) ⚠️"}`);
55
+ log.info(`Health check endpoint: http://localhost:${PORT}/health`);
56
+ });
57
+
58
+ process.on("SIGTERM", () => {
59
+ log.warn("SIGTERM received. Shutting down Webhook Receiver...");
60
+ server.close(() => {
61
+ process.exit(0);
62
+ });
63
+ });
64
+
65
+ process.on("SIGINT", () => {
66
+ log.warn("SIGINT received. Shutting down Webhook Receiver...");
67
+ server.close(() => {
68
+ process.exit(0);
69
+ });
70
+ });
@@ -1,181 +1,22 @@
1
- #!/usr/bin/env node
2
-
3
1
  /**
4
- * lock-manager.mjs
5
- *
6
- * Manages multi-agent file locks to prevent concurrent modification collisions.
7
- * Uses .agent/sync-manifest.json as the lock registry.
8
- *
9
- * Usage:
10
- * node scripts/lock-manager.mjs acquire <agent_name> <task_id> <file_path1> <file_path2> [--unattended]
11
- * node scripts/lock-manager.mjs release <task_id>
2
+ * Backward compatibility shim for lock-manager.mjs in v0.9.0.
3
+ * Delegates to src/state.mjs.
12
4
  */
13
5
 
14
- import fs from 'node:fs/promises';
15
- import path from 'node:path';
16
-
17
- const MANIFEST_PATH = path.join(process.cwd(), '.agent', 'sync-manifest.json');
18
- const TTL_MS = 20 * 60 * 1000; // 20 minutes
19
-
20
- async function readManifest() {
21
- try {
22
- const data = await fs.readFile(MANIFEST_PATH, 'utf-8');
23
- return JSON.parse(data);
24
- } catch (err) {
25
- if (err.code === 'ENOENT') {
26
- return {};
27
- }
28
- throw err;
29
- }
30
- }
31
-
32
- async function writeManifest(manifest) {
33
- await fs.mkdir(path.dirname(MANIFEST_PATH), { recursive: true });
34
- const tmp = MANIFEST_PATH + `.tmp-${process.pid}`;
35
- await fs.writeFile(tmp, JSON.stringify(manifest, null, 2), 'utf-8');
36
- await fs.rename(tmp, MANIFEST_PATH);
37
- }
38
-
39
- async function withManifestLock(fn) {
40
- const lockPath = MANIFEST_PATH + ".lock";
41
- await fs.mkdir(path.dirname(lockPath), { recursive: true });
42
- for (let i = 0; i < 50; i++) {
43
- try {
44
- const stats = await fs.stat(lockPath);
45
- if (Date.now() - stats.mtimeMs > 5 * 60 * 1000) {
46
- await fs.rm(lockPath, { force: true });
47
- }
48
- } catch (_) {}
49
-
50
- let handle;
51
- try {
52
- handle = await fs.open(lockPath, "wx");
53
- } catch (err) {
54
- if (err.code !== "EEXIST") throw err;
55
- await new Promise(r => setTimeout(r, 50 + Math.random() * 50));
56
- continue;
57
- }
58
- try { return await fn(); }
59
- finally {
60
- try { await handle.close(); } catch (_) {}
61
- await fs.rm(lockPath, { force: true });
62
- }
63
- }
64
- throw new Error("Could not acquire manifest lock");
65
- }
6
+ import { acquireLock, releaseLock, lockStatus } from "../src/state.mjs";
66
7
 
67
- function cleanExpiredLocks(manifest) {
68
- const now = Date.now();
69
- let changed = false;
70
- for (const [file, lock] of Object.entries(manifest)) {
71
- if (lock.expiresAt < now) {
72
- delete manifest[file];
73
- changed = true;
74
- }
75
- }
76
- return changed;
8
+ export function acquire(agent, taskId, filePaths = [], opts = {}) {
9
+ return acquireLock(agent, taskId, filePaths, opts);
77
10
  }
78
11
 
79
- async function acquireLocks(agentName, taskId, files) {
80
- await withManifestLock(async () => {
81
- const manifest = await readManifest();
82
- cleanExpiredLocks(manifest);
83
-
84
- const now = Date.now();
85
- const expiresAt = now + TTL_MS;
86
-
87
- const conflicts = [];
88
- for (const file of files) {
89
- const normalizedFile = path.normalize(file);
90
- if (manifest[normalizedFile] && manifest[normalizedFile].taskId !== taskId) {
91
- conflicts.push({
92
- file: normalizedFile,
93
- lockedBy: manifest[normalizedFile].agentName,
94
- task: manifest[normalizedFile].taskId
95
- });
96
- }
97
- }
98
-
99
- if (conflicts.length > 0) {
100
- let msg = `Cannot acquire locks for task ${taskId}. Conflicts detected:\n`;
101
- for (const conflict of conflicts) {
102
- msg += ` - ${conflict.file} is locked by ${conflict.lockedBy} (Task: ${conflict.task})\n`;
103
- }
104
- throw new Error(msg.trim());
105
- }
106
-
107
- for (const file of files) {
108
- const normalizedFile = path.normalize(file);
109
- manifest[normalizedFile] = {
110
- agentName,
111
- taskId,
112
- acquiredAt: now,
113
- expiresAt
114
- };
115
- }
116
-
117
- await writeManifest(manifest);
118
- console.log(`SUCCESS: Acquired locks for task ${taskId} on ${files.length} files.`);
119
- });
12
+ export function release(agent, taskId, opts = {}) {
13
+ return releaseLock(agent, taskId, opts);
120
14
  }
121
15
 
122
- async function releaseLocks(taskId) {
123
- await withManifestLock(async () => {
124
- const manifest = await readManifest();
125
- let releasedCount = 0;
126
-
127
- for (const [file, lock] of Object.entries(manifest)) {
128
- if (lock.taskId === taskId) {
129
- delete manifest[file];
130
- releasedCount++;
131
- }
132
- }
133
-
134
- if (releasedCount > 0) {
135
- await writeManifest(manifest);
136
- console.log(`SUCCESS: Released ${releasedCount} locks for task ${taskId}.`);
137
- } else {
138
- console.log(`INFO: No locks found for task ${taskId}.`);
139
- }
140
- });
16
+ export function status(opts = {}) {
17
+ return lockStatus(opts);
141
18
  }
142
19
 
143
- async function main() {
144
- const args = process.argv.slice(2);
145
- const command = args[0];
146
-
147
- if (command === 'acquire') {
148
- const agentName = args[1];
149
- const taskId = args[2];
150
- const unattendedIndex = args.indexOf('--unattended');
151
-
152
- let files = [];
153
- if (unattendedIndex !== -1) {
154
- files = args.slice(3, unattendedIndex).concat(args.slice(unattendedIndex + 1));
155
- } else {
156
- files = args.slice(3);
157
- }
158
-
159
- if (!agentName || !taskId || files.length === 0) {
160
- console.error('Usage: acquire <agent_name> <task_id> <file_paths...>');
161
- process.exit(1);
162
- }
163
-
164
- await acquireLocks(agentName, taskId, files);
165
- } else if (command === 'release') {
166
- const taskId = args[1];
167
- if (!taskId) {
168
- console.error('Usage: release <task_id>');
169
- process.exit(1);
170
- }
171
- await releaseLocks(taskId);
172
- } else {
173
- console.error('Unknown command. Use "acquire" or "release".');
174
- process.exit(1);
175
- }
20
+ export function cleanup() {
21
+ return { removed: 0 };
176
22
  }
177
-
178
- main().catch(err => {
179
- console.error('Unhandled error:', err);
180
- process.exit(1);
181
- });
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { changedFiles } from "../src/git.mjs";
4
+ import { classifyRiskTier } from "../src/risk.mjs";
5
+
6
+ const root = process.cwd();
7
+ const baseBranch = process.env.BASE_BRANCH || "main";
8
+
9
+ try {
10
+ const files = changedFiles(root, baseBranch);
11
+ const result = classifyRiskTier(files);
12
+
13
+ console.log(`[risk-tier] Classified PR Risk Tier: ${result.tier}`);
14
+ console.log(`[risk-tier] Reason: ${result.reason}`);
15
+ console.log(`[risk-tier] Auto-Merge Allowed: ${result.isAutoMergeAllowed}`);
16
+ console.log(`[risk-tier] Requires Human Review: ${result.requiresHumanReview}`);
17
+
18
+ if (process.env.GITHUB_OUTPUT) {
19
+ const { appendFileSync } = await import("node:fs");
20
+ appendFileSync(process.env.GITHUB_OUTPUT, `risk_tier=${result.tier}\n`);
21
+ appendFileSync(process.env.GITHUB_OUTPUT, `auto_merge=${result.isAutoMergeAllowed}\n`);
22
+ appendFileSync(process.env.GITHUB_OUTPUT, `requires_human=${result.requiresHumanReview}\n`);
23
+ }
24
+
25
+ process.exit(0);
26
+ } catch (err) {
27
+ console.error(`❌ Risk tier classification failed: ${err.message}`);
28
+ process.exit(1);
29
+ }