jules-orchestrator-kit 0.22.3 ā 0.22.5
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 +6 -6
- package/index.mjs +4 -2
- package/package.json +1 -1
- package/scripts/jules-queue-runner.mjs +2 -1
- package/src/dag-engine.mjs +274 -0
- package/src/engine.mjs +52 -2
- package/src/git.mjs +47 -6
- package/src/state.mjs +14 -8
package/bin/agentctl.mjs
CHANGED
|
@@ -4,7 +4,7 @@ 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
10
|
|
|
@@ -13,7 +13,7 @@ const command = args[0];
|
|
|
13
13
|
|
|
14
14
|
function printHelp() {
|
|
15
15
|
console.log(`
|
|
16
|
-
š agentctl v0.22.
|
|
16
|
+
š agentctl v0.22.5 ā 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.
|
|
46
|
+
console.log("agentctl v0.22.5");
|
|
47
47
|
process.exit(0);
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -150,7 +150,7 @@ async function main() {
|
|
|
150
150
|
|
|
151
151
|
case "queue": {
|
|
152
152
|
const queueDir = getQueueDir(root);
|
|
153
|
-
const files = readdirSync(queueDir).filter((f) => f
|
|
153
|
+
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
154
154
|
console.log(` Found ${files.length} queued task(s) in .agent/queue/`);
|
|
155
155
|
if (files.length > 0) {
|
|
156
156
|
const tasks = files.map((f) => ({
|
|
@@ -168,7 +168,7 @@ async function main() {
|
|
|
168
168
|
case "swarm": {
|
|
169
169
|
console.log("š Running Swarm Orchestrator...");
|
|
170
170
|
const queueDir = getQueueDir(root);
|
|
171
|
-
const files = readdirSync(queueDir).filter((f) => f
|
|
171
|
+
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
172
172
|
if (files.length === 0) {
|
|
173
173
|
console.log("No pending tasks found for swarm.");
|
|
174
174
|
process.exit(0);
|
|
@@ -223,7 +223,7 @@ async function main() {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
case "doctor": {
|
|
226
|
-
console.log(`\nš agentctl System Diagnostics (v0.22.
|
|
226
|
+
console.log(`\nš agentctl System Diagnostics (v0.22.5)`);
|
|
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
|
@@ -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,
|
|
@@ -48,6 +49,7 @@ export {
|
|
|
48
49
|
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";
|
|
52
|
+
export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
|
|
51
53
|
|
|
52
54
|
// Legacy SDK shims for backward compatibility
|
|
53
55
|
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!");
|
|
@@ -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
|
+
}
|
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
|
@@ -14,8 +14,14 @@ function normalizePath(p) {
|
|
|
14
14
|
return p.split(sep).join("/").replace(/\\/g, "/");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
18
|
+
const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
|
|
19
|
+
|
|
17
20
|
export function runCmd(command, opts = {}) {
|
|
18
21
|
const cwd = opts.cwd || process.cwd();
|
|
22
|
+
const timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
23
|
+
const maxBuffer = opts.maxBuffer || DEFAULT_MAX_BUFFER;
|
|
24
|
+
|
|
19
25
|
let binary = "";
|
|
20
26
|
let args = [];
|
|
21
27
|
|
|
@@ -40,16 +46,36 @@ export function runCmd(command, opts = {}) {
|
|
|
40
46
|
shell: false,
|
|
41
47
|
stdio: ["ignore", "pipe", "pipe"],
|
|
42
48
|
env: opts.env || process.env,
|
|
49
|
+
timeout,
|
|
50
|
+
maxBuffer,
|
|
43
51
|
});
|
|
44
52
|
return { status: 0, stdout: stdout.trim(), stderr: "" };
|
|
45
53
|
} catch (err) {
|
|
54
|
+
const isTimeout = err.code === "ETIMEDOUT" || (err.signal === "SIGTERM" && err.killed);
|
|
55
|
+
const isNobufs = err.code === "ENOBUFS" || (err.message && err.message.includes("maxBuffer"));
|
|
56
|
+
|
|
57
|
+
const status = err.status || (isTimeout ? 124 : 1);
|
|
58
|
+
let stdout = (err.stdout || "").toString().trim();
|
|
59
|
+
let stderr = (err.stderr || err.message || "").toString().trim();
|
|
60
|
+
|
|
61
|
+
if (isTimeout && !stderr.includes("ETIMEDOUT")) {
|
|
62
|
+
stderr = `Command execution timed out after ${timeout}ms (ETIMEDOUT)${stderr ? "\n" + stderr : ""}`;
|
|
63
|
+
}
|
|
64
|
+
if (isNobufs && !stderr.includes("ENOBUFS")) {
|
|
65
|
+
stderr = `Command output buffer exceeded limit of ${maxBuffer} bytes (ENOBUFS)${stderr ? "\n" + stderr : ""}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
if (opts.ignoreError) {
|
|
47
|
-
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
69
|
+
return { status, stdout, stderr };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isTimeout) {
|
|
73
|
+
throw new GateError(`Command execution timed out (ETIMEDOUT): ${binary}`, { code: status });
|
|
74
|
+
}
|
|
75
|
+
if (isNobufs) {
|
|
76
|
+
throw new GateError(`Command output buffer exceeded limit (ENOBUFS): ${binary}`, { code: status });
|
|
52
77
|
}
|
|
78
|
+
|
|
53
79
|
throw err;
|
|
54
80
|
}
|
|
55
81
|
}
|
|
@@ -59,20 +85,35 @@ export function runCmd(command, opts = {}) {
|
|
|
59
85
|
*/
|
|
60
86
|
export function git(args = [], opts = {}) {
|
|
61
87
|
const cwd = opts.cwd || process.cwd();
|
|
88
|
+
const timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
89
|
+
const maxBuffer = opts.maxBuffer || DEFAULT_MAX_BUFFER;
|
|
62
90
|
const argArray = Array.isArray(args) ? args : String(args).split(" ").filter(Boolean);
|
|
91
|
+
|
|
63
92
|
try {
|
|
64
93
|
const stdout = execFileSync("git", argArray, {
|
|
65
94
|
cwd,
|
|
66
95
|
encoding: "utf-8",
|
|
67
96
|
shell: false,
|
|
68
97
|
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
timeout,
|
|
99
|
+
maxBuffer,
|
|
69
100
|
});
|
|
70
101
|
return opts.raw ? stdout : stdout.trim();
|
|
71
102
|
} catch (err) {
|
|
72
103
|
if (opts.ignoreError) {
|
|
73
104
|
return "";
|
|
74
105
|
}
|
|
106
|
+
const isTimeout = err.code === "ETIMEDOUT" || (err.signal === "SIGTERM" && err.killed);
|
|
107
|
+
const isNobufs = err.code === "ENOBUFS" || (err.message && err.message.includes("maxBuffer"));
|
|
75
108
|
const stderr = (err.stderr || err.message || "").toString().trim();
|
|
109
|
+
|
|
110
|
+
if (isTimeout) {
|
|
111
|
+
throw new GateError(`Git command timed out [git ${argArray.join(" ")}]: ETIMEDOUT`, { code: 124 });
|
|
112
|
+
}
|
|
113
|
+
if (isNobufs) {
|
|
114
|
+
throw new GateError(`Git command output buffer exceeded limit [git ${argArray.join(" ")}]: ENOBUFS`, { code: 1 });
|
|
115
|
+
}
|
|
116
|
+
|
|
76
117
|
throw new GateError(`Git command failed [git ${argArray.join(" ")}]: ${stderr}`);
|
|
77
118
|
}
|
|
78
119
|
}
|
|
@@ -88,7 +129,7 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
|
88
129
|
stdio: ["ignore", "pipe", "ignore"],
|
|
89
130
|
});
|
|
90
131
|
if (res && res.trim()) {
|
|
91
|
-
return
|
|
132
|
+
return res.trim();
|
|
92
133
|
}
|
|
93
134
|
} catch (_) {}
|
|
94
135
|
}
|
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;
|