jules-orchestrator-kit 0.22.4 ā 0.22.6
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/bin/agentctl.mjs +8 -6
- package/index.mjs +4 -2
- package/package.json +1 -1
- package/scripts/jules-queue-runner.mjs +2 -1
- package/src/engine.mjs +52 -2
- package/src/git.mjs +69 -7
- package/src/journal.mjs +176 -0
- package/src/mcp.mjs +4 -1
- package/src/state.mjs +14 -8
package/bin/agentctl.mjs
CHANGED
|
@@ -4,16 +4,17 @@ import { parseArgs } from "node:util";
|
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { loadConfig, resolveRoot, detectStack } from "../src/config.mjs";
|
|
7
|
-
import { gate, dispatch, run } from "../src/engine.mjs";
|
|
7
|
+
import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
|
|
8
8
|
import { acquireLock, releaseLock, lockStatus, checkDailyBudget, getQueueDir, ensureDir } from "../src/state.mjs";
|
|
9
9
|
import { worktreePrune } from "../src/git.mjs";
|
|
10
|
+
import { reapOrphanedIntents } from "../src/journal.mjs";
|
|
10
11
|
|
|
11
12
|
const args = process.argv.slice(2);
|
|
12
13
|
const command = args[0];
|
|
13
14
|
|
|
14
15
|
function printHelp() {
|
|
15
16
|
console.log(`
|
|
16
|
-
š agentctl v0.22.
|
|
17
|
+
š agentctl v0.22.6 ā Universal Agent Orchestrator & Safety Gatekeeper
|
|
17
18
|
|
|
18
19
|
Usage: agentctl <command> [options]
|
|
19
20
|
|
|
@@ -43,11 +44,12 @@ async function main() {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
46
|
-
console.log("agentctl v0.22.
|
|
47
|
+
console.log("agentctl v0.22.6");
|
|
47
48
|
process.exit(0);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const root = resolveRoot();
|
|
52
|
+
reapOrphanedIntents(root);
|
|
51
53
|
const config = loadConfig(root);
|
|
52
54
|
|
|
53
55
|
switch (command) {
|
|
@@ -150,7 +152,7 @@ async function main() {
|
|
|
150
152
|
|
|
151
153
|
case "queue": {
|
|
152
154
|
const queueDir = getQueueDir(root);
|
|
153
|
-
const files = readdirSync(queueDir).filter((f) => f
|
|
155
|
+
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
154
156
|
console.log(` Found ${files.length} queued task(s) in .agent/queue/`);
|
|
155
157
|
if (files.length > 0) {
|
|
156
158
|
const tasks = files.map((f) => ({
|
|
@@ -168,7 +170,7 @@ async function main() {
|
|
|
168
170
|
case "swarm": {
|
|
169
171
|
console.log("š Running Swarm Orchestrator...");
|
|
170
172
|
const queueDir = getQueueDir(root);
|
|
171
|
-
const files = readdirSync(queueDir).filter((f) => f
|
|
173
|
+
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
172
174
|
if (files.length === 0) {
|
|
173
175
|
console.log("No pending tasks found for swarm.");
|
|
174
176
|
process.exit(0);
|
|
@@ -223,7 +225,7 @@ async function main() {
|
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
case "doctor": {
|
|
226
|
-
console.log(`\nš agentctl System Diagnostics (v0.22.
|
|
228
|
+
console.log(`\nš agentctl System Diagnostics (v0.22.6)`);
|
|
227
229
|
console.log(`--------------------------------------------------`);
|
|
228
230
|
console.log(` Project Root : ${root}`);
|
|
229
231
|
console.log(` Config File : ${config._file || "None (Using defaults)"}`);
|
package/index.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
} from "./src/security.mjs";
|
|
17
17
|
export { sanitizeUntrustedData, buildAgentEnvelope } from "./src/prompt-guard.mjs";
|
|
18
18
|
export { isolateMcpStdout, writeMcpFrame } from "./src/mcp.mjs";
|
|
19
|
-
export { git, runCmd, changedFiles, diffBytes, diffText } from "./src/git.mjs";
|
|
19
|
+
export { git, runCmd, resolveBase, changedFiles, diffBytes, diffText } from "./src/git.mjs";
|
|
20
20
|
export { createProvider, JULES_PRESET, CLAUDE_PRESET, CODEX_PRESET } from "./src/provider.mjs";
|
|
21
21
|
export {
|
|
22
22
|
appendLedger,
|
|
@@ -36,8 +36,9 @@ export {
|
|
|
36
36
|
BudgetError,
|
|
37
37
|
isPidAlive,
|
|
38
38
|
getProcessStartTime,
|
|
39
|
+
parseProcStat,
|
|
39
40
|
} from "./src/state.mjs";
|
|
40
|
-
export { gate, dispatch, repair, run, fingerprintFailureState } from "./src/engine.mjs";
|
|
41
|
+
export { gate, dispatch, repair, run, fingerprintFailureState, isTaskFile } from "./src/engine.mjs";
|
|
41
42
|
export { validateEnvelope } from "./src/envelope.mjs";
|
|
42
43
|
export {
|
|
43
44
|
createExecutionEnvelope,
|
|
@@ -49,6 +50,7 @@ export { checkAssetIntegrity } from "./src/asset_integrity.mjs";
|
|
|
49
50
|
export { classifyRiskTier, RISK_TIERS } from "./src/risk.mjs";
|
|
50
51
|
export { checkRulesBudget } from "./src/rules_budget.mjs";
|
|
51
52
|
export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
|
|
53
|
+
export { journalIntent, journalDone, reapOrphanedIntents } from "./src/journal.mjs";
|
|
52
54
|
|
|
53
55
|
// Legacy SDK shims for backward compatibility
|
|
54
56
|
export { resolveProjectCommands, resolveWorkspaceExecutionBoundary } from "./scripts/command-resolver.mjs";
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getQueueDir, ensureDir } from "../src/state.mjs";
|
|
8
|
+
import { isTaskFile } from "../src/engine.mjs";
|
|
8
9
|
import { readdirSync, renameSync } from "node:fs";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
|
|
@@ -19,7 +20,7 @@ export function classifyQueueFailure(err) {
|
|
|
19
20
|
if (process.argv[1] && process.argv[1].endsWith("jules-queue-runner.mjs")) {
|
|
20
21
|
const root = process.env.JULES_PROJECT_ROOT || process.cwd();
|
|
21
22
|
const queueDir = getQueueDir(root);
|
|
22
|
-
const files = readdirSync(queueDir).filter((f) => f
|
|
23
|
+
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
23
24
|
|
|
24
25
|
if (files.length === 0) {
|
|
25
26
|
console.log("Queue processing complete!");
|
package/src/engine.mjs
CHANGED
|
@@ -3,10 +3,60 @@ import { checkScope, scanDiff, redactSecrets } from "./security.mjs";
|
|
|
3
3
|
import { changedFiles, diffBytes, diffText, showFromOrigin, runCmd } from "./git.mjs";
|
|
4
4
|
import { createProvider } from "./provider.mjs";
|
|
5
5
|
import { withBudget, appendLedger, getQueueDir, ensureDir } from "./state.mjs";
|
|
6
|
-
import { readdirSync, readFileSync, renameSync } from "node:fs";
|
|
6
|
+
import { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Validates whether a file in .agent/jules-queue/ is a task file.
|
|
12
|
+
* Filters out README.md and matches TASK-*.md or valid envelope front-matter.
|
|
13
|
+
*/
|
|
14
|
+
export function isTaskFile(fileName, queueDirOrContent = null) {
|
|
15
|
+
if (!fileName || typeof fileName !== "string") return false;
|
|
16
|
+
const lower = fileName.toLowerCase();
|
|
17
|
+
|
|
18
|
+
// Explicitly filter out README.md
|
|
19
|
+
if (lower === "readme.md" || lower.endsWith("/readme.md") || lower.endsWith("\\readme.md")) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!lower.endsWith(".md")) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Matching TASK-*.md or task-*.md
|
|
28
|
+
if (/^task-.*\.md$/i.test(fileName)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check valid envelope front-matter
|
|
33
|
+
if (queueDirOrContent) {
|
|
34
|
+
try {
|
|
35
|
+
let content = queueDirOrContent;
|
|
36
|
+
if (typeof queueDirOrContent === "string" && !queueDirOrContent.includes("\n") && existsSync(queueDirOrContent)) {
|
|
37
|
+
const fullPath = queueDirOrContent.endsWith(fileName) ? queueDirOrContent : join(queueDirOrContent, fileName);
|
|
38
|
+
if (existsSync(fullPath)) {
|
|
39
|
+
content = readFileSync(fullPath, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (typeof content === "string") {
|
|
43
|
+
const trimmed = content.trimStart();
|
|
44
|
+
if (trimmed.startsWith("---")) {
|
|
45
|
+
const endFm = trimmed.indexOf("\n---", 3);
|
|
46
|
+
if (endFm !== -1) {
|
|
47
|
+
const frontMatter = trimmed.slice(3, endFm);
|
|
48
|
+
if (/(?:taskId|envelopeId|baseRef|baseSha|task|title|id|version|capabilities|scope|verify):/i.test(frontMatter)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (_) {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
10
60
|
/**
|
|
11
61
|
* Computes a SHA-256 fingerprint for a failure state (normalized stderr + diff text).
|
|
12
62
|
*/
|
|
@@ -322,7 +372,7 @@ export async function run(opts = {}) {
|
|
|
322
372
|
const completedDir = join(queueDir, "completed");
|
|
323
373
|
ensureDir(completedDir);
|
|
324
374
|
|
|
325
|
-
const files = readdirSync(queueDir).filter((f) => f
|
|
375
|
+
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
326
376
|
const results = [];
|
|
327
377
|
|
|
328
378
|
for (let i = 0; i < files.length; i += concurrency) {
|
package/src/git.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { sep } from "node:path";
|
|
3
|
+
import { journalIntent, journalDone } from "./journal.mjs";
|
|
3
4
|
|
|
4
5
|
export class GateError extends Error {
|
|
5
6
|
constructor(message, opts = {}) {
|
|
@@ -14,8 +15,14 @@ function normalizePath(p) {
|
|
|
14
15
|
return p.split(sep).join("/").replace(/\\/g, "/");
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
19
|
+
const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
|
|
20
|
+
|
|
17
21
|
export function runCmd(command, opts = {}) {
|
|
18
22
|
const cwd = opts.cwd || process.cwd();
|
|
23
|
+
const timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
24
|
+
const maxBuffer = opts.maxBuffer || DEFAULT_MAX_BUFFER;
|
|
25
|
+
|
|
19
26
|
let binary = "";
|
|
20
27
|
let args = [];
|
|
21
28
|
|
|
@@ -40,16 +47,36 @@ export function runCmd(command, opts = {}) {
|
|
|
40
47
|
shell: false,
|
|
41
48
|
stdio: ["ignore", "pipe", "pipe"],
|
|
42
49
|
env: opts.env || process.env,
|
|
50
|
+
timeout,
|
|
51
|
+
maxBuffer,
|
|
43
52
|
});
|
|
44
53
|
return { status: 0, stdout: stdout.trim(), stderr: "" };
|
|
45
54
|
} catch (err) {
|
|
55
|
+
const isTimeout = err.code === "ETIMEDOUT" || (err.signal === "SIGTERM" && err.killed);
|
|
56
|
+
const isNobufs = err.code === "ENOBUFS" || (err.message && err.message.includes("maxBuffer"));
|
|
57
|
+
|
|
58
|
+
const status = err.status || (isTimeout ? 124 : 1);
|
|
59
|
+
let stdout = (err.stdout || "").toString().trim();
|
|
60
|
+
let stderr = (err.stderr || err.message || "").toString().trim();
|
|
61
|
+
|
|
62
|
+
if (isTimeout && !stderr.includes("ETIMEDOUT")) {
|
|
63
|
+
stderr = `Command execution timed out after ${timeout}ms (ETIMEDOUT)${stderr ? "\n" + stderr : ""}`;
|
|
64
|
+
}
|
|
65
|
+
if (isNobufs && !stderr.includes("ENOBUFS")) {
|
|
66
|
+
stderr = `Command output buffer exceeded limit of ${maxBuffer} bytes (ENOBUFS)${stderr ? "\n" + stderr : ""}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
if (opts.ignoreError) {
|
|
47
|
-
return {
|
|
48
|
-
status: err.status || 1,
|
|
49
|
-
stdout: (err.stdout || "").toString().trim(),
|
|
50
|
-
stderr: (err.stderr || "").toString().trim(),
|
|
51
|
-
};
|
|
70
|
+
return { status, stdout, stderr };
|
|
52
71
|
}
|
|
72
|
+
|
|
73
|
+
if (isTimeout) {
|
|
74
|
+
throw new GateError(`Command execution timed out (ETIMEDOUT): ${binary}`, { code: status });
|
|
75
|
+
}
|
|
76
|
+
if (isNobufs) {
|
|
77
|
+
throw new GateError(`Command output buffer exceeded limit (ENOBUFS): ${binary}`, { code: status });
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
throw err;
|
|
54
81
|
}
|
|
55
82
|
}
|
|
@@ -59,20 +86,35 @@ export function runCmd(command, opts = {}) {
|
|
|
59
86
|
*/
|
|
60
87
|
export function git(args = [], opts = {}) {
|
|
61
88
|
const cwd = opts.cwd || process.cwd();
|
|
89
|
+
const timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
90
|
+
const maxBuffer = opts.maxBuffer || DEFAULT_MAX_BUFFER;
|
|
62
91
|
const argArray = Array.isArray(args) ? args : String(args).split(" ").filter(Boolean);
|
|
92
|
+
|
|
63
93
|
try {
|
|
64
94
|
const stdout = execFileSync("git", argArray, {
|
|
65
95
|
cwd,
|
|
66
96
|
encoding: "utf-8",
|
|
67
97
|
shell: false,
|
|
68
98
|
stdio: ["ignore", "pipe", "pipe"],
|
|
99
|
+
timeout,
|
|
100
|
+
maxBuffer,
|
|
69
101
|
});
|
|
70
102
|
return opts.raw ? stdout : stdout.trim();
|
|
71
103
|
} catch (err) {
|
|
72
104
|
if (opts.ignoreError) {
|
|
73
105
|
return "";
|
|
74
106
|
}
|
|
107
|
+
const isTimeout = err.code === "ETIMEDOUT" || (err.signal === "SIGTERM" && err.killed);
|
|
108
|
+
const isNobufs = err.code === "ENOBUFS" || (err.message && err.message.includes("maxBuffer"));
|
|
75
109
|
const stderr = (err.stderr || err.message || "").toString().trim();
|
|
110
|
+
|
|
111
|
+
if (isTimeout) {
|
|
112
|
+
throw new GateError(`Git command timed out [git ${argArray.join(" ")}]: ETIMEDOUT`, { code: 124 });
|
|
113
|
+
}
|
|
114
|
+
if (isNobufs) {
|
|
115
|
+
throw new GateError(`Git command output buffer exceeded limit [git ${argArray.join(" ")}]: ENOBUFS`, { code: 1 });
|
|
116
|
+
}
|
|
117
|
+
|
|
76
118
|
throw new GateError(`Git command failed [git ${argArray.join(" ")}]: ${stderr}`);
|
|
77
119
|
}
|
|
78
120
|
}
|
|
@@ -88,7 +130,7 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
|
88
130
|
stdio: ["ignore", "pipe", "ignore"],
|
|
89
131
|
});
|
|
90
132
|
if (res && res.trim()) {
|
|
91
|
-
return
|
|
133
|
+
return res.trim();
|
|
92
134
|
}
|
|
93
135
|
} catch (_) {}
|
|
94
136
|
}
|
|
@@ -133,8 +175,28 @@ export function showFromOrigin(root = process.cwd(), base = "main", filePath = "
|
|
|
133
175
|
}
|
|
134
176
|
}
|
|
135
177
|
|
|
178
|
+
export function createBranch(root = process.cwd(), branch = "") {
|
|
179
|
+
const opId = journalIntent(root, { type: "create_branch", branch, targetPath: "" });
|
|
180
|
+
try {
|
|
181
|
+
const res = git(["branch", branch], { cwd: root });
|
|
182
|
+
journalDone(root, opId);
|
|
183
|
+
return res;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
journalDone(root, opId);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
136
190
|
export function worktreeAdd(root = process.cwd(), branch = "agent/task", targetDir = "") {
|
|
137
|
-
|
|
191
|
+
const opId = journalIntent(root, { type: "worktree_add", targetPath: targetDir, branch });
|
|
192
|
+
try {
|
|
193
|
+
const res = git(["worktree", "add", targetDir, "-b", branch], { cwd: root });
|
|
194
|
+
journalDone(root, opId);
|
|
195
|
+
return res;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
journalDone(root, opId);
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
138
200
|
}
|
|
139
201
|
|
|
140
202
|
export function worktreeRemove(root = process.cwd(), targetDir = "") {
|
package/src/journal.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { openSync, writeSync, fsyncSync, closeSync, existsSync, readFileSync, unlinkSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getStateDir, getProcessStartTime, isPidAlive, getLockDir } from "./state.mjs";
|
|
5
|
+
import { worktreeRemove, worktreePrune } from "./git.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Appends an intent record to .agent/state/journal.jsonl before a mutating operation.
|
|
9
|
+
* @param {string} root - Project root directory
|
|
10
|
+
* @param {Object|string} op - Operation payload or name
|
|
11
|
+
* @returns {string} opId - Unique operation identifier
|
|
12
|
+
*/
|
|
13
|
+
export function journalIntent(root, op) {
|
|
14
|
+
const stateDir = getStateDir(root);
|
|
15
|
+
const journalPath = join(stateDir, "journal.jsonl");
|
|
16
|
+
|
|
17
|
+
const opObj = typeof op === "string" ? { type: op } : op || {};
|
|
18
|
+
const opId = opObj.opId || `op-${randomUUID()}`;
|
|
19
|
+
const pid = process.pid;
|
|
20
|
+
const processStartTime = getProcessStartTime(pid);
|
|
21
|
+
|
|
22
|
+
const record = {
|
|
23
|
+
opId,
|
|
24
|
+
event: "intent",
|
|
25
|
+
type: opObj.type || opObj.operation || "unknown",
|
|
26
|
+
targetPath: opObj.targetPath || opObj.targetDir || opObj.path || "",
|
|
27
|
+
pid,
|
|
28
|
+
processStartTime,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
...opObj,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const fd = openSync(journalPath, "a");
|
|
34
|
+
try {
|
|
35
|
+
writeSync(fd, JSON.stringify(record) + "\n", "utf-8");
|
|
36
|
+
fsyncSync(fd);
|
|
37
|
+
} finally {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return opId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Appends a journal_done record to .agent/state/journal.jsonl upon operation completion.
|
|
46
|
+
* @param {string} root - Project root directory
|
|
47
|
+
* @param {string} opId - Operation identifier returned by journalIntent
|
|
48
|
+
* @returns {string|null} opId
|
|
49
|
+
*/
|
|
50
|
+
export function journalDone(root, opId) {
|
|
51
|
+
if (!opId) return null;
|
|
52
|
+
const stateDir = getStateDir(root);
|
|
53
|
+
const journalPath = join(stateDir, "journal.jsonl");
|
|
54
|
+
|
|
55
|
+
const record = {
|
|
56
|
+
opId,
|
|
57
|
+
event: "journal_done",
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const fd = openSync(journalPath, "a");
|
|
62
|
+
try {
|
|
63
|
+
writeSync(fd, JSON.stringify(record) + "\n", "utf-8");
|
|
64
|
+
fsyncSync(fd);
|
|
65
|
+
} finally {
|
|
66
|
+
closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return opId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Scans .agent/state/journal.jsonl for uncompleted intents belonging to dead PIDs,
|
|
74
|
+
* prunes orphaned worktrees, removes stale lock files, and marks intents reaped.
|
|
75
|
+
* Idempotent: Subsequent calls result in 0 additional reaps.
|
|
76
|
+
* @param {string} root - Project root directory
|
|
77
|
+
* @returns {{ reapedCount: number, reaped: Array<Object> }}
|
|
78
|
+
*/
|
|
79
|
+
export function reapOrphanedIntents(root) {
|
|
80
|
+
const stateDir = getStateDir(root);
|
|
81
|
+
const journalPath = join(stateDir, "journal.jsonl");
|
|
82
|
+
|
|
83
|
+
if (!existsSync(journalPath)) {
|
|
84
|
+
return { reapedCount: 0, reaped: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let lines = [];
|
|
88
|
+
try {
|
|
89
|
+
const raw = readFileSync(journalPath, "utf-8");
|
|
90
|
+
lines = raw.split("\n").filter(Boolean);
|
|
91
|
+
} catch (_) {
|
|
92
|
+
return { reapedCount: 0, reaped: [] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const intents = new Map();
|
|
96
|
+
const finishedOpIds = new Set();
|
|
97
|
+
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
try {
|
|
100
|
+
const entry = JSON.parse(line);
|
|
101
|
+
if (!entry || !entry.opId) continue;
|
|
102
|
+
if (entry.event === "intent") {
|
|
103
|
+
intents.set(entry.opId, entry);
|
|
104
|
+
} else if (entry.event === "journal_done" || entry.event === "journal_reaped") {
|
|
105
|
+
finishedOpIds.add(entry.opId);
|
|
106
|
+
}
|
|
107
|
+
} catch (_) {
|
|
108
|
+
// Ignore corrupted or partial lines
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const reaped = [];
|
|
113
|
+
|
|
114
|
+
for (const [opId, intent] of intents.entries()) {
|
|
115
|
+
if (finishedOpIds.has(opId)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const alive = isPidAlive(intent.pid, intent.processStartTime);
|
|
120
|
+
if (!alive) {
|
|
121
|
+
// Owner PID is dead or recycled -> reap orphaned intent
|
|
122
|
+
if (intent.targetPath) {
|
|
123
|
+
try {
|
|
124
|
+
worktreeRemove(root, intent.targetPath);
|
|
125
|
+
} catch (_) {}
|
|
126
|
+
try {
|
|
127
|
+
worktreePrune(root);
|
|
128
|
+
} catch (_) {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Clean up stale lock files associated with this PID or targetPath
|
|
132
|
+
try {
|
|
133
|
+
const lockDir = getLockDir(root);
|
|
134
|
+
if (existsSync(lockDir)) {
|
|
135
|
+
const lockFiles = readdirSync(lockDir);
|
|
136
|
+
for (const file of lockFiles) {
|
|
137
|
+
if (file.endsWith(".json")) {
|
|
138
|
+
const lockPath = join(lockDir, file);
|
|
139
|
+
try {
|
|
140
|
+
const lockContent = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
141
|
+
if (
|
|
142
|
+
lockContent.pid === intent.pid ||
|
|
143
|
+
(intent.targetPath && Array.isArray(lockContent.files) && lockContent.files.includes(intent.targetPath))
|
|
144
|
+
) {
|
|
145
|
+
unlinkSync(lockPath);
|
|
146
|
+
}
|
|
147
|
+
} catch (_) {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
|
|
153
|
+
// Record journal_reaped to guarantee idempotency
|
|
154
|
+
const reapedRecord = {
|
|
155
|
+
opId,
|
|
156
|
+
event: "journal_reaped",
|
|
157
|
+
reapedAt: new Date().toISOString(),
|
|
158
|
+
pid: intent.pid,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const fd = openSync(journalPath, "a");
|
|
163
|
+
try {
|
|
164
|
+
writeSync(fd, JSON.stringify(reapedRecord) + "\n", "utf-8");
|
|
165
|
+
fsyncSync(fd);
|
|
166
|
+
} finally {
|
|
167
|
+
closeSync(fd);
|
|
168
|
+
}
|
|
169
|
+
} catch (_) {}
|
|
170
|
+
|
|
171
|
+
reaped.push(intent);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { reapedCount: reaped.length, reaped };
|
|
176
|
+
}
|
package/src/mcp.mjs
CHANGED
|
@@ -3,10 +3,11 @@ import { loadConfig, resolveRoot, detectStack } from "./config.mjs";
|
|
|
3
3
|
import { gate, dispatch } from "./engine.mjs";
|
|
4
4
|
import { classifyRiskTier } from "./risk.mjs";
|
|
5
5
|
import { checkDailyBudget, lockStatus } from "./state.mjs";
|
|
6
|
+
import { reapOrphanedIntents } from "./journal.mjs";
|
|
6
7
|
|
|
7
8
|
export const MCP_SERVER_INFO = {
|
|
8
9
|
name: "jules-orchestrator-kit",
|
|
9
|
-
version: "0.
|
|
10
|
+
version: "0.22.6",
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
|
|
@@ -352,6 +353,8 @@ export function writeMcpFrame(targetOutput, data) {
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
export function startMcpServer(input = process.stdin, output = process.stdout, opts = {}) {
|
|
356
|
+
const root = opts.root || resolveRoot();
|
|
357
|
+
reapOrphanedIntents(root);
|
|
355
358
|
isolateMcpStdout(process.stdout, opts);
|
|
356
359
|
if (output && output !== process.stdout) {
|
|
357
360
|
isolateMcpStdout(output, opts);
|
package/src/state.mjs
CHANGED
|
@@ -292,18 +292,24 @@ export function getLockDir(rootOrOpts = resolveRoot()) {
|
|
|
292
292
|
return dir;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
export function parseProcStat(stat) {
|
|
296
|
+
if (typeof stat !== "string") return null;
|
|
297
|
+
const rpar = stat.lastIndexOf(")");
|
|
298
|
+
if (rpar === -1) return null;
|
|
299
|
+
const rest = stat.slice(rpar + 2).trim().split(/\s+/);
|
|
300
|
+
return rest[19] || null;
|
|
301
|
+
}
|
|
302
|
+
|
|
295
303
|
export function getProcessStartTime(pid) {
|
|
296
|
-
if (!pid
|
|
304
|
+
if (!pid) return null;
|
|
305
|
+
if (typeof pid === "string" && pid.includes(")")) {
|
|
306
|
+
return parseProcStat(pid);
|
|
307
|
+
}
|
|
308
|
+
if (typeof pid !== "number") return null;
|
|
297
309
|
if (process.platform === "linux") {
|
|
298
310
|
try {
|
|
299
311
|
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
300
|
-
|
|
301
|
-
if (rpar !== -1) {
|
|
302
|
-
const rest = stat.slice(rpar + 1).trim().split(/\s+/);
|
|
303
|
-
return rest[19] || null;
|
|
304
|
-
}
|
|
305
|
-
const parts = stat.trim().split(/\s+/);
|
|
306
|
-
return parts[21] || null;
|
|
312
|
+
return parseProcStat(stat);
|
|
307
313
|
} catch (_) {}
|
|
308
314
|
}
|
|
309
315
|
return null;
|