jules-orchestrator-kit 0.22.3 → 0.22.4

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
@@ -13,7 +13,7 @@ const command = args[0];
13
13
 
14
14
  function printHelp() {
15
15
  console.log(`
16
- šŸš€ agentctl v0.22.3 — Universal Agent Orchestrator & Safety Gatekeeper
16
+ šŸš€ agentctl v0.22.4 — Universal Agent Orchestrator & Safety Gatekeeper
17
17
 
18
18
  Usage: agentctl <command> [options]
19
19
 
@@ -43,7 +43,7 @@ async function main() {
43
43
  }
44
44
 
45
45
  if (command === "version" || command === "--version" || command === "-v") {
46
- console.log("agentctl v0.22.1");
46
+ console.log("agentctl v0.22.4");
47
47
  process.exit(0);
48
48
  }
49
49
 
@@ -223,7 +223,7 @@ async function main() {
223
223
  }
224
224
 
225
225
  case "doctor": {
226
- console.log(`\nšŸ” agentctl System Diagnostics (v0.22.3)`);
226
+ console.log(`\nšŸ” agentctl System Diagnostics (v0.22.4)`);
227
227
  console.log(`--------------------------------------------------`);
228
228
  console.log(` Project Root : ${root}`);
229
229
  console.log(` Config File : ${config._file || "None (Using defaults)"}`);
package/index.mjs CHANGED
@@ -48,6 +48,7 @@ export {
48
48
  export { checkAssetIntegrity } from "./src/asset_integrity.mjs";
49
49
  export { classifyRiskTier, RISK_TIERS } from "./src/risk.mjs";
50
50
  export { checkRulesBudget } from "./src/rules_budget.mjs";
51
+ export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
51
52
 
52
53
  // Legacy SDK shims for backward compatibility
53
54
  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.3",
3
+ "version": "0.22.4",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,274 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+
4
+ /**
5
+ * Custom error thrown when a circular dependency is detected in the DAG.
6
+ */
7
+ export class DagCycleError extends Error {
8
+ constructor(cyclePath) {
9
+ const pathStr = Array.isArray(cyclePath) ? cyclePath.join(" -> ") : String(cyclePath);
10
+ super(`Circular dependency detected: ${pathStr}`);
11
+ this.name = "DagCycleError";
12
+ this.cyclePath = cyclePath;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Zero-Dependency Task DAG Executor with cycle detection and interface fingerprinting.
18
+ */
19
+ export class DagExecutor {
20
+ constructor({ concurrency = 4 } = {}) {
21
+ this.concurrency = concurrency;
22
+ this.tasks = new Map();
23
+ }
24
+
25
+ /**
26
+ * Registers a task into the DAG.
27
+ * @param {Object} task
28
+ * @param {string} task.id
29
+ * @param {string[]} [task.dependsOn=[]]
30
+ * @param {string[]} [task.outputs=[]]
31
+ * @param {Function} [task.runner]
32
+ */
33
+ addTask({ id, dependsOn = [], outputs = [], runner }) {
34
+ if (!id || typeof id !== "string") {
35
+ throw new Error("Task id must be a non-empty string");
36
+ }
37
+ if (this.tasks.has(id)) {
38
+ throw new Error(`Task with id '${id}' already exists`);
39
+ }
40
+ this.tasks.set(id, {
41
+ id,
42
+ dependsOn: Array.isArray(dependsOn) ? [...dependsOn] : [],
43
+ outputs: Array.isArray(outputs) ? [...outputs] : [],
44
+ runner: runner || (async () => {}),
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Validates the DAG, resolves dependencies using Kahn's algorithm, and checks for cycles.
50
+ * Throws DagCycleError if a cycle is detected.
51
+ */
52
+ validateDag() {
53
+ const totalNodes = this.tasks.size;
54
+ if (totalNodes === 0) return { executionOrder: [], dependents: new Map() };
55
+
56
+ const inDegree = new Map();
57
+ const dependents = new Map();
58
+
59
+ for (const id of this.tasks.keys()) {
60
+ inDegree.set(id, 0);
61
+ dependents.set(id, []);
62
+ }
63
+
64
+ for (const [id, task] of this.tasks.entries()) {
65
+ for (const depId of task.dependsOn) {
66
+ if (!this.tasks.has(depId)) {
67
+ throw new Error(`Task '${id}' depends on missing task '${depId}'`);
68
+ }
69
+ inDegree.set(id, inDegree.get(id) + 1);
70
+ dependents.get(depId).push(id);
71
+ }
72
+ }
73
+
74
+ const queue = [];
75
+ for (const [id, deg] of inDegree.entries()) {
76
+ if (deg === 0) {
77
+ queue.push(id);
78
+ }
79
+ }
80
+ queue.sort();
81
+
82
+ const executionOrder = [];
83
+ let processedNodes = 0;
84
+ const currentInDegree = new Map(inDegree);
85
+
86
+ while (queue.length > 0) {
87
+ queue.sort();
88
+ const node = queue.shift();
89
+ executionOrder.push(node);
90
+ processedNodes++;
91
+
92
+ for (const neighbor of dependents.get(node)) {
93
+ const newDeg = currentInDegree.get(neighbor) - 1;
94
+ currentInDegree.set(neighbor, newDeg);
95
+ if (newDeg === 0) {
96
+ queue.push(neighbor);
97
+ }
98
+ }
99
+ }
100
+
101
+ if (processedNodes !== totalNodes) {
102
+ const cyclePath = this._findCyclePath();
103
+ throw new DagCycleError(cyclePath);
104
+ }
105
+
106
+ return { executionOrder, dependents };
107
+ }
108
+
109
+ /**
110
+ * Finds the exact circular path sequence in the task dependency graph.
111
+ * @private
112
+ */
113
+ _findCyclePath() {
114
+ const visited = new Set();
115
+ const stack = [];
116
+ const stackSet = new Set();
117
+
118
+ const dfs = (nodeId) => {
119
+ visited.add(nodeId);
120
+ stack.push(nodeId);
121
+ stackSet.add(nodeId);
122
+
123
+ const task = this.tasks.get(nodeId);
124
+ if (task) {
125
+ for (const depId of task.dependsOn) {
126
+ if (!this.tasks.has(depId)) continue;
127
+ if (!visited.has(depId)) {
128
+ const res = dfs(depId);
129
+ if (res) return res;
130
+ } else if (stackSet.has(depId)) {
131
+ const cycleStartIdx = stack.indexOf(depId);
132
+ const rawCycle = stack.slice(cycleStartIdx);
133
+ rawCycle.push(depId);
134
+ return rawCycle;
135
+ }
136
+ }
137
+ }
138
+
139
+ stack.pop();
140
+ stackSet.delete(nodeId);
141
+ return null;
142
+ };
143
+
144
+ // Sort keys lexicographically for deterministic cycle path detection
145
+ const sortedKeys = Array.from(this.tasks.keys()).sort();
146
+ for (const id of sortedKeys) {
147
+ if (!visited.has(id)) {
148
+ const cycle = dfs(id);
149
+ if (cycle) {
150
+ return cycle.reverse();
151
+ }
152
+ }
153
+ }
154
+
155
+ return sortedKeys;
156
+ }
157
+
158
+ /**
159
+ * Computes SHA-256 hash of a file path using node:crypto.
160
+ * @private
161
+ */
162
+ _computeFileHash(filePath) {
163
+ if (!existsSync(filePath)) {
164
+ return createHash("sha256").update("").digest("hex");
165
+ }
166
+ const content = readFileSync(filePath);
167
+ return createHash("sha256").update(content).digest("hex");
168
+ }
169
+
170
+ /**
171
+ * Executes tasks in topological order with parallel concurrency and interface fingerprinting.
172
+ * @param {Object} [options]
173
+ * @param {number} [options.concurrency]
174
+ */
175
+ async execute({ concurrency = this.concurrency } = {}) {
176
+ const { dependents } = this.validateDag();
177
+
178
+ const completed = new Set();
179
+ const running = new Set();
180
+ const fingerprints = new Map();
181
+ const executionHistory = [];
182
+
183
+ const inDegree = new Map();
184
+ for (const [id, task] of this.tasks.entries()) {
185
+ inDegree.set(id, task.dependsOn.length);
186
+ }
187
+
188
+ return new Promise((resolve, reject) => {
189
+ let isSettled = false;
190
+
191
+ const fail = (err) => {
192
+ if (!isSettled) {
193
+ isSettled = true;
194
+ reject(err);
195
+ }
196
+ };
197
+
198
+ const checkAndSchedule = async () => {
199
+ if (isSettled) return;
200
+
201
+ if (completed.size === this.tasks.size) {
202
+ isSettled = true;
203
+ return resolve({ executionHistory, fingerprints });
204
+ }
205
+
206
+ const readyTasks = [];
207
+ for (const [id, deg] of inDegree.entries()) {
208
+ if (deg === 0 && !completed.has(id) && !running.has(id)) {
209
+ readyTasks.push(id);
210
+ }
211
+ }
212
+
213
+ readyTasks.sort();
214
+
215
+ while (running.size < concurrency && readyTasks.length > 0) {
216
+ const taskId = readyTasks.shift();
217
+ const task = this.tasks.get(taskId);
218
+
219
+ // Verification Gate & Interface Fingerprint Check
220
+ try {
221
+ for (const depId of task.dependsOn) {
222
+ const depTask = this.tasks.get(depId);
223
+ for (const outputFile of depTask.outputs) {
224
+ const expectedHash = fingerprints.get(outputFile);
225
+ const actualHash = this._computeFileHash(outputFile);
226
+ if (expectedHash !== actualHash) {
227
+ throw new Error(
228
+ `Interface fingerprint mismatch for output '${outputFile}' from dependency '${depId}'`
229
+ );
230
+ }
231
+ }
232
+ }
233
+ } catch (gateErr) {
234
+ return fail(gateErr);
235
+ }
236
+
237
+ running.add(taskId);
238
+
239
+ (async () => {
240
+ try {
241
+ executionHistory.push(taskId);
242
+ await task.runner({
243
+ id: task.id,
244
+ dependsOn: task.dependsOn,
245
+ outputs: task.outputs,
246
+ fingerprints,
247
+ });
248
+
249
+ // Post-task Interface Fingerprinting
250
+ for (const outputFile of task.outputs) {
251
+ const hash = this._computeFileHash(outputFile);
252
+ fingerprints.set(outputFile, hash);
253
+ }
254
+
255
+ running.delete(taskId);
256
+ completed.add(taskId);
257
+
258
+ for (const depNode of dependents.get(taskId)) {
259
+ inDegree.set(depNode, inDegree.get(depNode) - 1);
260
+ }
261
+
262
+ checkAndSchedule();
263
+ } catch (err) {
264
+ running.delete(taskId);
265
+ fail(err);
266
+ }
267
+ })();
268
+ }
269
+ };
270
+
271
+ checkAndSchedule();
272
+ });
273
+ }
274
+ }