jules-orchestrator-kit 0.22.5 → 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 CHANGED
@@ -7,13 +7,14 @@ import { loadConfig, resolveRoot, detectStack } from "../src/config.mjs";
7
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.5 — Universal Agent Orchestrator & Safety Gatekeeper
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.5");
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) {
@@ -223,7 +225,7 @@ async function main() {
223
225
  }
224
226
 
225
227
  case "doctor": {
226
- console.log(`\nšŸ” agentctl System Diagnostics (v0.22.5)`);
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
@@ -50,6 +50,7 @@ export { checkAssetIntegrity } from "./src/asset_integrity.mjs";
50
50
  export { classifyRiskTier, RISK_TIERS } from "./src/risk.mjs";
51
51
  export { checkRulesBudget } from "./src/rules_budget.mjs";
52
52
  export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
53
+ export { journalIntent, journalDone, reapOrphanedIntents } from "./src/journal.mjs";
53
54
 
54
55
  // Legacy SDK shims for backward compatibility
55
56
  export { resolveProjectCommands, resolveWorkspaceExecutionBoundary } from "./scripts/command-resolver.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.22.5",
3
+ "version": "0.22.6",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
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 = {}) {
@@ -174,8 +175,28 @@ export function showFromOrigin(root = process.cwd(), base = "main", filePath = "
174
175
  }
175
176
  }
176
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
+
177
190
  export function worktreeAdd(root = process.cwd(), branch = "agent/task", targetDir = "") {
178
- return git(["worktree", "add", targetDir, "-b", branch], { cwd: root });
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
+ }
179
200
  }
180
201
 
181
202
  export function worktreeRemove(root = process.cwd(), targetDir = "") {
@@ -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.20.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);