jules-orchestrator-kit 0.22.9 ā 0.24.0
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 +3 -3
- package/bin/init.js +2 -3
- package/index.mjs +3 -1
- package/package.json +3 -4
- package/scripts/jules-queue-runner.mjs +3 -22
- package/scripts/utils.mjs +65 -0
- package/src/dag-engine.mjs +23 -1
- package/src/engine.mjs +139 -20
- package/src/envelope.mjs +3 -3
- package/src/flaky-ledger.mjs +0 -2
- package/src/git.mjs +1 -6
- package/src/mcp-progress.mjs +158 -0
- package/src/mcp.mjs +36 -2
- package/src/merge-verify.mjs +7 -7
- package/src/security.mjs +3 -1
- package/src/state.mjs +4 -2
- package/src/telemetry.mjs +311 -0
- package/scripts/jules-cleanup.mjs +0 -49
- package/scripts/jules-swarm.mjs +0 -38
- package/scripts/lock-manager.mjs +0 -22
package/bin/agentctl.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const command = args[0];
|
|
|
14
14
|
|
|
15
15
|
function printHelp() {
|
|
16
16
|
console.log(`
|
|
17
|
-
š agentctl v0.
|
|
17
|
+
š agentctl v0.24.0 ā Universal Agent Orchestrator & Safety Gatekeeper
|
|
18
18
|
|
|
19
19
|
Usage: agentctl <command> [options]
|
|
20
20
|
|
|
@@ -44,7 +44,7 @@ async function main() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
47
|
-
console.log("agentctl v0.
|
|
47
|
+
console.log("agentctl v0.24.0");
|
|
48
48
|
process.exit(0);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -226,7 +226,7 @@ async function main() {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
case "doctor": {
|
|
229
|
-
console.log(`\nš agentctl System Diagnostics (v0.
|
|
229
|
+
console.log(`\nš agentctl System Diagnostics (v0.24.0)`);
|
|
230
230
|
console.log(`--------------------------------------------------`);
|
|
231
231
|
console.log(` Project Root : ${root}`);
|
|
232
232
|
console.log(` Config File : ${config._file || "None (Using defaults)"}`);
|
package/bin/init.js
CHANGED
|
@@ -248,13 +248,12 @@ if (fs.existsSync(targetPkgPath) && targetDir !== kitRoot) {
|
|
|
248
248
|
let updated = false;
|
|
249
249
|
const julesScripts = {
|
|
250
250
|
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
251
|
-
"jules:queue": "node
|
|
251
|
+
"jules:queue": "node bin/agentctl.mjs queue",
|
|
252
252
|
"jules:create": "node scripts/jules-create.mjs",
|
|
253
253
|
"jules:status": "node scripts/jules-status.mjs",
|
|
254
254
|
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
255
|
-
"jules:cleanup": "node scripts/jules-cleanup.mjs",
|
|
256
255
|
"jules:scan": "node scripts/jules-scan-todos.mjs",
|
|
257
|
-
"jules:swarm": "node
|
|
256
|
+
"jules:swarm": "node bin/agentctl.mjs swarm",
|
|
258
257
|
"jules:nightly": "node scripts/jules-nightly.mjs"
|
|
259
258
|
};
|
|
260
259
|
|
package/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Google Jules Orchestrator Kit - Node.js SDK (v0.
|
|
2
|
+
* Google Jules Orchestrator Kit - Node.js SDK (v0.24.0)
|
|
3
3
|
*
|
|
4
4
|
* Exposes core orchestrator functions for programmatically driving agent tasks,
|
|
5
5
|
* security auditing, repo gating, and state operations.
|
|
@@ -58,3 +58,5 @@ export { runSelfAudit, runPreflightSandbox } from "./scripts/jules-self-audit.mj
|
|
|
58
58
|
export { scanCodebaseForTodos, runScanner } from "./scripts/jules-scan-todos.mjs";
|
|
59
59
|
export { getDynamicGuardrails, dispatchTask } from "./scripts/jules-dispatch.mjs";
|
|
60
60
|
export { classifyQueueFailure } from "./scripts/jules-queue-runner.mjs";
|
|
61
|
+
export { extractPrUrls, auditSessions, buildSyncManifest, pushReservationManifest } from "./scripts/utils.mjs";
|
|
62
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Orchestration kit for running Google Jules autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,13 +42,12 @@
|
|
|
42
42
|
"init": "node bin/init.js",
|
|
43
43
|
"test": "node --test test/*.test.mjs",
|
|
44
44
|
"jules:dispatch": "node scripts/jules-dispatch.mjs",
|
|
45
|
-
"jules:queue": "node
|
|
45
|
+
"jules:queue": "node bin/agentctl.mjs queue",
|
|
46
46
|
"jules:create": "node scripts/jules-create.mjs",
|
|
47
47
|
"jules:status": "node scripts/jules-status.mjs",
|
|
48
48
|
"jules:audit": "node scripts/jules-self-audit.mjs",
|
|
49
|
-
"jules:cleanup": "node scripts/jules-cleanup.mjs",
|
|
50
49
|
"jules:scan": "node scripts/jules-scan-todos.mjs",
|
|
51
|
-
"jules:swarm": "node
|
|
50
|
+
"jules:swarm": "node bin/agentctl.mjs swarm",
|
|
52
51
|
"jules:merge-swarm": "node scripts/jules-merge-swarm.mjs",
|
|
53
52
|
"jules:patch": "node scripts/jules-patch.mjs",
|
|
54
53
|
"jules:nightly": "node scripts/jules-nightly.mjs",
|
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
* Backward compatibility shim for jules-queue-runner.mjs in v0.9.0.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { isTaskFile } from "../src/engine.mjs";
|
|
9
|
-
import { readdirSync, renameSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
7
|
+
import { run } from "../src/engine.mjs";
|
|
11
8
|
|
|
12
9
|
export function classifyQueueFailure(err) {
|
|
13
10
|
const msg = String(err?.message || err || "");
|
|
@@ -19,25 +16,9 @@ export function classifyQueueFailure(err) {
|
|
|
19
16
|
|
|
20
17
|
if (process.argv[1] && process.argv[1].endsWith("jules-queue-runner.mjs")) {
|
|
21
18
|
const root = process.env.JULES_PROJECT_ROOT || process.cwd();
|
|
22
|
-
const
|
|
23
|
-
const files = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
24
|
-
|
|
25
|
-
if (files.length === 0) {
|
|
26
|
-
console.log("Queue processing complete!");
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const completedDir = join(queueDir, "completed");
|
|
31
|
-
ensureDir(completedDir);
|
|
32
|
-
|
|
33
|
-
for (const file of files) {
|
|
34
|
-
const src = join(queueDir, file);
|
|
35
|
-
const dst = join(completedDir, file);
|
|
36
|
-
try {
|
|
37
|
-
renameSync(src, dst);
|
|
38
|
-
} catch (_) {}
|
|
39
|
-
}
|
|
19
|
+
const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
|
|
40
20
|
|
|
21
|
+
await run({ root, dryRun: isDry });
|
|
41
22
|
console.log("Queue processing complete!");
|
|
42
23
|
process.exit(0);
|
|
43
24
|
}
|
package/scripts/utils.mjs
CHANGED
|
@@ -159,3 +159,68 @@ export function acquireBudgetLock(_root) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export function releaseBudgetLock(_root) {}
|
|
162
|
+
|
|
163
|
+
export function extractPrUrls(outputs = []) {
|
|
164
|
+
const prs = [];
|
|
165
|
+
const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
|
|
166
|
+
for (const item of outputs) {
|
|
167
|
+
const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
|
|
168
|
+
const matches = text.match(regex);
|
|
169
|
+
if (matches) {
|
|
170
|
+
for (const m of matches) {
|
|
171
|
+
if (!prs.includes(m)) prs.push(m);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return prs;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function auditSessions(sessions = [], opts = {}) {
|
|
179
|
+
const staleHoursThreshold = opts.staleHoursThreshold || 24;
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const merged = [];
|
|
182
|
+
const active = [];
|
|
183
|
+
const stale = [];
|
|
184
|
+
|
|
185
|
+
for (const s of sessions) {
|
|
186
|
+
if (s.state === "MERGED") {
|
|
187
|
+
merged.push(s);
|
|
188
|
+
} else {
|
|
189
|
+
const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
|
|
190
|
+
const ageHours = (now - updatedMs) / (1000 * 60 * 60);
|
|
191
|
+
if (ageHours > staleHoursThreshold) {
|
|
192
|
+
stale.push(s);
|
|
193
|
+
} else {
|
|
194
|
+
active.push(s);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { merged, active, stale };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function buildSyncManifest(tasks = []) {
|
|
203
|
+
const reservations = tasks.map((t) => ({ id: t.id, title: t.title, scope: t.scope }));
|
|
204
|
+
return {
|
|
205
|
+
version: 1,
|
|
206
|
+
totalTasks: tasks.length,
|
|
207
|
+
reservations,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function pushReservationManifest(manifest, projectRoot = process.cwd()) {
|
|
212
|
+
const isDry = process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1";
|
|
213
|
+
const isRemote = process.env.JULES_SWARM_REMOTE_PUSH === "true";
|
|
214
|
+
|
|
215
|
+
const agentDir = join(projectRoot, ".agent");
|
|
216
|
+
if (!existsSync(agentDir)) {
|
|
217
|
+
try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
|
|
218
|
+
}
|
|
219
|
+
const manifestPath = join(agentDir, "sync-manifest.json");
|
|
220
|
+
try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
|
|
221
|
+
|
|
222
|
+
if (isDry) return { status: "DRY_RUN", path: manifestPath };
|
|
223
|
+
if (isRemote) return { status: "PUSHED", path: manifestPath };
|
|
224
|
+
return { status: "SAVED_LOCAL", path: manifestPath };
|
|
225
|
+
}
|
|
226
|
+
|
package/src/dag-engine.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { appendTelemetry } from "./telemetry.mjs";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Custom error thrown when a circular dependency is detected in the DAG.
|
|
@@ -200,8 +201,17 @@ export class DagExecutor {
|
|
|
200
201
|
* @param {Object} [options]
|
|
201
202
|
* @param {number} [options.concurrency]
|
|
202
203
|
* @param {number} [options.taskTimeout]
|
|
204
|
+
* @param {string} [options.root]
|
|
205
|
+
* @param {Object} [options.progressBus]
|
|
206
|
+
* @param {string} [options.progressToken]
|
|
203
207
|
*/
|
|
204
|
-
async execute({
|
|
208
|
+
async execute({
|
|
209
|
+
concurrency = this.concurrency,
|
|
210
|
+
taskTimeout = this.taskTimeout,
|
|
211
|
+
root,
|
|
212
|
+
progressBus,
|
|
213
|
+
progressToken,
|
|
214
|
+
} = {}) {
|
|
205
215
|
this.isExecuting = true;
|
|
206
216
|
const { dependents } = this.validateDag();
|
|
207
217
|
|
|
@@ -265,6 +275,7 @@ export class DagExecutor {
|
|
|
265
275
|
}
|
|
266
276
|
|
|
267
277
|
running.add(taskId);
|
|
278
|
+
if (root) appendTelemetry(root, "dag_task_started", { taskId });
|
|
268
279
|
|
|
269
280
|
(async () => {
|
|
270
281
|
try {
|
|
@@ -291,6 +302,17 @@ export class DagExecutor {
|
|
|
291
302
|
running.delete(taskId);
|
|
292
303
|
completed.add(taskId);
|
|
293
304
|
|
|
305
|
+
if (root) appendTelemetry(root, "dag_task_completed", { taskId });
|
|
306
|
+
if (progressBus && progressToken) {
|
|
307
|
+
const pct = Math.round((completed.size / this.tasks.size) * 100);
|
|
308
|
+
progressBus.reportProgress(
|
|
309
|
+
progressToken,
|
|
310
|
+
pct,
|
|
311
|
+
100,
|
|
312
|
+
`Task ${taskId} completed (${completed.size}/${this.tasks.size})`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
294
316
|
for (const depNode of dependents.get(taskId)) {
|
|
295
317
|
inDegree.set(depNode, inDegree.get(depNode) - 1);
|
|
296
318
|
}
|
package/src/engine.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { recordVerifyRun, readVerifyRuns, flakyVerdict } from "./flaky-ledger.mj
|
|
|
8
8
|
import { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
import { appendTelemetry } from "./telemetry.mjs";
|
|
11
12
|
|
|
12
13
|
export { recordVerifyRun, readVerifyRuns, flakyVerdict, sanitizeUntrustedData };
|
|
13
14
|
|
|
@@ -100,8 +101,12 @@ export async function gate(opts = {}) {
|
|
|
100
101
|
const root = opts.root || process.cwd();
|
|
101
102
|
const config = opts.config || loadConfig(root);
|
|
102
103
|
const base = opts.base || config.baseBranch || "main";
|
|
104
|
+
const progressBus = opts.progressBus;
|
|
105
|
+
const progressToken = opts.progressToken;
|
|
103
106
|
const phases = [];
|
|
104
107
|
|
|
108
|
+
appendTelemetry(root, "gate_started", { base });
|
|
109
|
+
|
|
105
110
|
let files = [];
|
|
106
111
|
let bytes = 0;
|
|
107
112
|
let diffStr = "";
|
|
@@ -112,6 +117,7 @@ export async function gate(opts = {}) {
|
|
|
112
117
|
diffStr = diffText(root, base);
|
|
113
118
|
} catch (err) {
|
|
114
119
|
phases.push({ phase: "git_resolution", ok: false, error: err.message });
|
|
120
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 1, error: err.message });
|
|
115
121
|
return { ok: false, code: 1, phases, error: err.message };
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -142,7 +148,13 @@ export async function gate(opts = {}) {
|
|
|
142
148
|
});
|
|
143
149
|
|
|
144
150
|
phases.push({ phase: "scope", ok: scopeResult.ok, violations: scopeResult.violations });
|
|
151
|
+
appendTelemetry(root, "gate_phase", { phase: "scope", ok: scopeResult.ok });
|
|
152
|
+
if (progressBus && progressToken) {
|
|
153
|
+
progressBus.reportProgress(progressToken, 25, 100, "Phase 1/4: Scope Guard verification complete");
|
|
154
|
+
}
|
|
155
|
+
|
|
145
156
|
if (!scopeResult.ok) {
|
|
157
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 3 });
|
|
146
158
|
return { ok: false, code: 3, phases };
|
|
147
159
|
}
|
|
148
160
|
|
|
@@ -150,14 +162,26 @@ export async function gate(opts = {}) {
|
|
|
150
162
|
const limitBytes = (config.limits.diffKb || 75) * 1024;
|
|
151
163
|
const payloadOk = bytes <= limitBytes;
|
|
152
164
|
phases.push({ phase: "payload", ok: payloadOk, bytes, limitBytes });
|
|
165
|
+
appendTelemetry(root, "gate_phase", { phase: "payload", ok: payloadOk });
|
|
166
|
+
if (progressBus && progressToken) {
|
|
167
|
+
progressBus.reportProgress(progressToken, 50, 100, "Phase 2/4: Diff Payload Governor check complete");
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
if (!payloadOk) {
|
|
171
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 5 });
|
|
154
172
|
return { ok: false, code: 5, phases };
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
// Phase 3: Diff Secret Scanner
|
|
158
176
|
const secretResult = scanDiff(diffStr);
|
|
159
177
|
phases.push({ phase: "secrets", ok: secretResult.ok, findings: secretResult.findings });
|
|
178
|
+
appendTelemetry(root, "gate_phase", { phase: "secrets", ok: secretResult.ok });
|
|
179
|
+
if (progressBus && progressToken) {
|
|
180
|
+
progressBus.reportProgress(progressToken, 75, 100, "Phase 3/4: Diff Secret Scanner check complete");
|
|
181
|
+
}
|
|
182
|
+
|
|
160
183
|
if (!secretResult.ok) {
|
|
184
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 6 });
|
|
161
185
|
return { ok: false, code: 6, phases };
|
|
162
186
|
}
|
|
163
187
|
|
|
@@ -195,6 +219,8 @@ export async function gate(opts = {}) {
|
|
|
195
219
|
flakyVerdictResult = flakyVerdict(runs);
|
|
196
220
|
if (flakyVerdictResult.verdict === "QUARANTINED") {
|
|
197
221
|
phases.push({ phase: "verify", ok: false, testResult, buildResult, flakyVerdict: flakyVerdictResult });
|
|
222
|
+
appendTelemetry(root, "gate_phase", { phase: "verify", ok: false, quarantined: true });
|
|
223
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 8 });
|
|
198
224
|
return { ok: false, code: 8, phases, flakyVerdict: flakyVerdictResult };
|
|
199
225
|
}
|
|
200
226
|
}
|
|
@@ -208,16 +234,25 @@ export async function gate(opts = {}) {
|
|
|
208
234
|
const verifyOk = testResult.ok && buildResult.ok;
|
|
209
235
|
const failingCmd = !testResult.ok ? testResult : !buildResult.ok ? buildResult : null;
|
|
210
236
|
phases.push({ phase: "verify", ok: verifyOk, testResult, buildResult });
|
|
237
|
+
appendTelemetry(root, "gate_phase", { phase: "verify", ok: verifyOk });
|
|
238
|
+
if (progressBus && progressToken) {
|
|
239
|
+
progressBus.reportProgress(progressToken, 100, 100, "Phase 4/4: Automated Test & Build Verification complete");
|
|
240
|
+
}
|
|
211
241
|
|
|
212
242
|
if (!verifyOk && opts.fix) {
|
|
213
|
-
const repairs = await repair(failingCmd, { config, root, signal: opts.signal });
|
|
243
|
+
const repairs = await repair(failingCmd, { config, root, signal: opts.signal, progressBus, progressToken });
|
|
244
|
+
const finalOk = repairs.ok;
|
|
245
|
+
const finalCode = repairs.ok ? 0 : 4;
|
|
246
|
+
appendTelemetry(root, "gate_finished", { ok: finalOk, code: finalCode, repaired: true });
|
|
214
247
|
if (repairs.ok) {
|
|
215
248
|
return { ok: true, code: 0, phases, repairs };
|
|
216
249
|
}
|
|
217
250
|
return { ok: false, code: 4, phases, repairs };
|
|
218
251
|
}
|
|
219
252
|
|
|
220
|
-
|
|
253
|
+
const code = verifyOk ? 0 : 4;
|
|
254
|
+
appendTelemetry(root, "gate_finished", { ok: verifyOk, code });
|
|
255
|
+
return { ok: verifyOk, code, phases };
|
|
221
256
|
}
|
|
222
257
|
|
|
223
258
|
/**
|
|
@@ -280,6 +315,8 @@ export async function repair(failure, opts = {}) {
|
|
|
280
315
|
const config = opts.config || loadConfig(root);
|
|
281
316
|
const maxRetries = config.limits.repairAttempts || 3;
|
|
282
317
|
const provider = opts.provider || createProvider(config.provider, config);
|
|
318
|
+
const progressBus = opts.progressBus;
|
|
319
|
+
const progressToken = opts.progressToken;
|
|
283
320
|
const attempts = [];
|
|
284
321
|
const breaker = opts.circuitBreaker || new OODACircuitBreaker({
|
|
285
322
|
windowSize: Math.max(maxRetries + 1, 6),
|
|
@@ -292,6 +329,10 @@ export async function repair(failure, opts = {}) {
|
|
|
292
329
|
breaker.observe(initialFingerprint);
|
|
293
330
|
|
|
294
331
|
for (let n = 1; n <= maxRetries; n++) {
|
|
332
|
+
if (progressBus && progressToken) {
|
|
333
|
+
progressBus.reportProgress(progressToken, Math.round((n / maxRetries) * 100), 100, `OODA Repair attempt ${n}/${maxRetries}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
295
336
|
const repairPrompt = buildRepairPrompt(currentFailure, n, config);
|
|
296
337
|
let session;
|
|
297
338
|
try {
|
|
@@ -306,13 +347,25 @@ export async function repair(failure, opts = {}) {
|
|
|
306
347
|
);
|
|
307
348
|
} catch (err) {
|
|
308
349
|
attempts.push({ n, ok: false, error: err.message });
|
|
350
|
+
appendTelemetry(root, "ooda_repair_attempt", { attempt: n, ok: false, error: err.message });
|
|
309
351
|
break;
|
|
310
352
|
}
|
|
311
353
|
|
|
312
354
|
attempts.push({ n, session, ok: true });
|
|
355
|
+
appendTelemetry(root, "ooda_repair_attempt", { attempt: n, ok: true });
|
|
356
|
+
|
|
357
|
+
// Poll async provider for terminal session state before executing re-verification gates
|
|
358
|
+
if (provider && session) {
|
|
359
|
+
await pollSessionState(provider, session, {
|
|
360
|
+
root,
|
|
361
|
+
dryRun: opts.dryRun,
|
|
362
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
363
|
+
maxPollAttempts: opts.maxPollAttempts,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
313
366
|
|
|
314
367
|
// Re-verify after repair attempt
|
|
315
|
-
const gateRes = await gate({ root, config, fix: false });
|
|
368
|
+
const gateRes = await gate({ root, config, fix: false, progressBus, progressToken });
|
|
316
369
|
if (gateRes.ok) {
|
|
317
370
|
breaker.reset();
|
|
318
371
|
return { ok: true, attempts, finalStatus: "PASSED" };
|
|
@@ -336,6 +389,50 @@ export async function repair(failure, opts = {}) {
|
|
|
336
389
|
return { ok: false, attempts, finalStatus: "OODA_EXHAUSTED" };
|
|
337
390
|
}
|
|
338
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Polls an async provider for terminal session state (COMPLETED / FAILED) before re-verification.
|
|
394
|
+
*/
|
|
395
|
+
export async function pollSessionState(provider, session, opts = {}) {
|
|
396
|
+
if (!session || !session.id) return { status: "COMPLETED" };
|
|
397
|
+
const initialStatus = String(session.status || session.state || "").toUpperCase();
|
|
398
|
+
if (initialStatus === "COMPLETED" || initialStatus === "FAILED" || opts.dryRun || session.id === "dry-run-session-id") {
|
|
399
|
+
return { status: initialStatus || "COMPLETED" };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const maxAttempts = opts.maxPollAttempts || 30;
|
|
403
|
+
const pollIntervalMs = opts.pollIntervalMs || 1000;
|
|
404
|
+
const timeoutMs = opts.pollTimeoutMs || 300000;
|
|
405
|
+
const startTime = Date.now();
|
|
406
|
+
|
|
407
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
408
|
+
if (Date.now() - startTime > timeoutMs) break;
|
|
409
|
+
|
|
410
|
+
let currentSession = null;
|
|
411
|
+
if (provider && typeof provider.getSession === "function") {
|
|
412
|
+
try {
|
|
413
|
+
currentSession = await provider.getSession(session.id, opts);
|
|
414
|
+
} catch (_) {}
|
|
415
|
+
} else if (typeof opts.pollFn === "function") {
|
|
416
|
+
try {
|
|
417
|
+
currentSession = await opts.pollFn(session.id);
|
|
418
|
+
} catch (_) {}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (currentSession) {
|
|
422
|
+
const status = String(currentSession.status || currentSession.state || "").toUpperCase();
|
|
423
|
+
if (status === "COMPLETED" || status === "FAILED") {
|
|
424
|
+
return { ...currentSession, status };
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { ...session, status: String(session.status || "COMPLETED").toUpperCase() };
|
|
434
|
+
}
|
|
435
|
+
|
|
339
436
|
function buildRepairPrompt(failure, attempt, _config) {
|
|
340
437
|
const cleanStderr = redactSecrets(failure.stderr || failure.stdout || "Unknown Error");
|
|
341
438
|
|
|
@@ -392,32 +489,54 @@ export async function dispatch(task, opts = {}) {
|
|
|
392
489
|
);
|
|
393
490
|
}
|
|
394
491
|
|
|
395
|
-
export async function run(opts = {}) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
492
|
+
export async function run(tasksOrOpts = {}, opts = {}) {
|
|
493
|
+
let tasks = null;
|
|
494
|
+
let options = opts;
|
|
495
|
+
if (Array.isArray(tasksOrOpts)) {
|
|
496
|
+
tasks = tasksOrOpts;
|
|
497
|
+
options = opts || {};
|
|
498
|
+
} else if (tasksOrOpts && typeof tasksOrOpts === "object") {
|
|
499
|
+
options = tasksOrOpts;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const root = options.root || process.cwd();
|
|
503
|
+
const config = options.config || loadConfig(root);
|
|
504
|
+
const concurrency = options.concurrency || config.limits.concurrency || 1;
|
|
505
|
+
const isDry = options.dryRun !== undefined ? options.dryRun : (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
|
|
399
506
|
const queueDir = getQueueDir(root);
|
|
400
507
|
const completedDir = join(queueDir, "completed");
|
|
401
508
|
ensureDir(completedDir);
|
|
402
509
|
|
|
403
|
-
|
|
510
|
+
let filesToProcess = [];
|
|
511
|
+
if (Array.isArray(tasks)) {
|
|
512
|
+
filesToProcess = tasks.map((t) => (typeof t === "string" ? t : t.id || t.file || t.title));
|
|
513
|
+
} else {
|
|
514
|
+
filesToProcess = readdirSync(queueDir).filter((f) => isTaskFile(f, queueDir));
|
|
515
|
+
}
|
|
516
|
+
|
|
404
517
|
const results = [];
|
|
405
518
|
|
|
406
|
-
for (let i = 0; i <
|
|
407
|
-
const batch =
|
|
519
|
+
for (let i = 0; i < filesToProcess.length; i += concurrency) {
|
|
520
|
+
const batch = filesToProcess.slice(i, i + concurrency);
|
|
408
521
|
await Promise.all(
|
|
409
|
-
batch.map(async (
|
|
410
|
-
const
|
|
522
|
+
batch.map(async (fileOrItem) => {
|
|
523
|
+
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem.id || fileOrItem.file;
|
|
524
|
+
const srcPath = join(queueDir, fileName);
|
|
411
525
|
try {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
526
|
+
let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
|
|
527
|
+
if (!prompt && existsSync(srcPath)) {
|
|
528
|
+
prompt = readFileSync(srcPath, "utf-8");
|
|
529
|
+
}
|
|
530
|
+
const task = { title: fileName, prompt };
|
|
531
|
+
const session = await dispatch(task, { root, config, dryRun: isDry });
|
|
532
|
+
const dstPath = join(completedDir, fileName);
|
|
533
|
+
if (existsSync(srcPath)) {
|
|
534
|
+
renameSync(srcPath, dstPath);
|
|
535
|
+
}
|
|
536
|
+
appendLedger({ event: "task_completed", file: fileName, session }, root);
|
|
537
|
+
results.push({ file: fileName, ok: true, session });
|
|
419
538
|
} catch (err) {
|
|
420
|
-
results.push({ file, ok: false, error: err.message });
|
|
539
|
+
results.push({ file: fileName, ok: false, error: err.message });
|
|
421
540
|
}
|
|
422
541
|
})
|
|
423
542
|
);
|
package/src/envelope.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { git, resolveBase } from "./git.mjs";
|
|
1
|
+
import { git, runCmd, resolveBase } from "./git.mjs";
|
|
2
2
|
import { checkScope } from "./security.mjs";
|
|
3
3
|
import { normalizeScope } from "./config.mjs";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -70,8 +70,8 @@ export function validateEnvelope(envelope = {}, opts = {}) {
|
|
|
70
70
|
const absPath = join(root, relPath);
|
|
71
71
|
let existsInGit = false;
|
|
72
72
|
if (resolvedBase) {
|
|
73
|
-
const catRes =
|
|
74
|
-
existsInGit =
|
|
73
|
+
const catRes = runCmd(["git", "cat-file", "-e", `${resolvedBase}:${relPath}`], { cwd: root, ignoreError: true });
|
|
74
|
+
existsInGit = catRes.status === 0;
|
|
75
75
|
}
|
|
76
76
|
const existsOnDisk = existsSync(absPath);
|
|
77
77
|
if (!existsInGit && !existsOnDisk) {
|
package/src/flaky-ledger.mjs
CHANGED
package/src/git.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizePath } from "./config.mjs";
|
|
3
3
|
import { journalIntent, journalDone } from "./journal.mjs";
|
|
4
4
|
|
|
5
5
|
export const NET_GUARD_PRELOAD_URL = new URL("./preload-net-guard.mjs", import.meta.url).href;
|
|
@@ -13,11 +13,6 @@ export class GateError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function normalizePath(p) {
|
|
17
|
-
if (!p || typeof p !== "string") return "";
|
|
18
|
-
return p.split(sep).join("/").replace(/\\/g, "/");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
22
17
|
const DEFAULT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
|
|
23
18
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { writeMcpFrame } from "./mcp.mjs";
|
|
2
|
+
|
|
3
|
+
export const MAX_PROGRESS_MESSAGE_LENGTH = 240;
|
|
4
|
+
export const DEFAULT_PROGRESS_COALESCE_MS = 150;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MCP Progress & Event Streaming Bus with 150ms window coalescing and backpressure safety.
|
|
8
|
+
*/
|
|
9
|
+
export class ProgressBus {
|
|
10
|
+
/**
|
|
11
|
+
* @param {import("node:stream").Writable} [output=process.stdout]
|
|
12
|
+
* @param {Object} [opts={}]
|
|
13
|
+
* @param {number} [opts.coalesceMs=150]
|
|
14
|
+
*/
|
|
15
|
+
constructor(output = process.stdout, opts = {}) {
|
|
16
|
+
this.output = output || process.stdout;
|
|
17
|
+
this.coalesceMs = opts.coalesceMs ?? DEFAULT_PROGRESS_COALESCE_MS;
|
|
18
|
+
this.pending = new Map(); // token -> { timer, latestPayload }
|
|
19
|
+
this.queue = [];
|
|
20
|
+
this.isWriting = false;
|
|
21
|
+
this.isDraining = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Internal queue flusher with stream backpressure handling.
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
async _flushQueue() {
|
|
29
|
+
this.isWriting = true;
|
|
30
|
+
while (this.queue.length > 0) {
|
|
31
|
+
if (this.isDraining) {
|
|
32
|
+
await new Promise((resolve) => {
|
|
33
|
+
if (!this.output || typeof this.output.once !== "function") {
|
|
34
|
+
return resolve();
|
|
35
|
+
}
|
|
36
|
+
this.output.once("drain", resolve);
|
|
37
|
+
});
|
|
38
|
+
this.isDraining = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const frame = this.queue.shift();
|
|
42
|
+
const ok = writeMcpFrame(this.output, frame);
|
|
43
|
+
if (!ok) {
|
|
44
|
+
this.isDraining = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.isWriting = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sends a framed JSON-RPC payload to output via authorized writeMcpFrame.
|
|
52
|
+
* @param {Object} payload
|
|
53
|
+
*/
|
|
54
|
+
async sendFrame(payload) {
|
|
55
|
+
const frameStr = JSON.stringify(payload) + "\n";
|
|
56
|
+
this.queue.push(frameStr);
|
|
57
|
+
if (!this.isWriting) {
|
|
58
|
+
await this._flushQueue();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reports progress for an active progressToken.
|
|
64
|
+
* Coalesces rapid intermediate updates within a 150ms window (latest-wins).
|
|
65
|
+
*
|
|
66
|
+
* @param {string} progressToken
|
|
67
|
+
* @param {number} progress
|
|
68
|
+
* @param {number|null} [total=null]
|
|
69
|
+
* @param {string|null} [message=null]
|
|
70
|
+
*/
|
|
71
|
+
reportProgress(progressToken, progress, total = null, message = null) {
|
|
72
|
+
if (!progressToken) return;
|
|
73
|
+
|
|
74
|
+
let cappedMessage = undefined;
|
|
75
|
+
if (message !== null && message !== undefined) {
|
|
76
|
+
const msgStr = String(message);
|
|
77
|
+
cappedMessage =
|
|
78
|
+
msgStr.length > MAX_PROGRESS_MESSAGE_LENGTH
|
|
79
|
+
? msgStr.slice(0, MAX_PROGRESS_MESSAGE_LENGTH)
|
|
80
|
+
: msgStr;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const params = {
|
|
84
|
+
progressToken,
|
|
85
|
+
progress,
|
|
86
|
+
};
|
|
87
|
+
if (total !== null && total !== undefined) params.total = total;
|
|
88
|
+
if (cappedMessage !== undefined) params.message = cappedMessage;
|
|
89
|
+
|
|
90
|
+
const payload = {
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
method: "notifications/progress",
|
|
93
|
+
params,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const isComplete = total !== null && total !== undefined && progress >= total;
|
|
97
|
+
const existing = this.pending.get(progressToken);
|
|
98
|
+
|
|
99
|
+
if (isComplete) {
|
|
100
|
+
if (existing?.timer) clearTimeout(existing.timer);
|
|
101
|
+
this.pending.delete(progressToken);
|
|
102
|
+
this.sendFrame(payload);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!existing) {
|
|
107
|
+
// First update in window: send immediately
|
|
108
|
+
this.sendFrame(payload);
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
const current = this.pending.get(progressToken);
|
|
111
|
+
if (current?.latestPayload) {
|
|
112
|
+
this.sendFrame(current.latestPayload);
|
|
113
|
+
}
|
|
114
|
+
this.pending.delete(progressToken);
|
|
115
|
+
}, this.coalesceMs);
|
|
116
|
+
|
|
117
|
+
this.pending.set(progressToken, { timer, latestPayload: null });
|
|
118
|
+
} else {
|
|
119
|
+
// Intermediate update: save latest payload
|
|
120
|
+
existing.latestPayload = payload;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Emits an MCP log notification frame (notifications/message).
|
|
126
|
+
* Never calls raw console.log.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} [level="info"] "info" | "warning" | "error" | "debug"
|
|
129
|
+
* @param {string|Object} [data=""]
|
|
130
|
+
* @param {string} [logger="jules-orchestrator"]
|
|
131
|
+
*/
|
|
132
|
+
log(level = "info", data = "", logger = "jules-orchestrator") {
|
|
133
|
+
const payload = {
|
|
134
|
+
jsonrpc: "2.0",
|
|
135
|
+
method: "notifications/message",
|
|
136
|
+
params: {
|
|
137
|
+
level,
|
|
138
|
+
logger,
|
|
139
|
+
data: typeof data === "string" ? data : JSON.stringify(data),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
return this.sendFrame(payload);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Flushes all pending progress timers and ensures output queue is completely drained.
|
|
147
|
+
*/
|
|
148
|
+
async flush() {
|
|
149
|
+
for (const [token, item] of this.pending.entries()) {
|
|
150
|
+
if (item.timer) clearTimeout(item.timer);
|
|
151
|
+
if (item.latestPayload) {
|
|
152
|
+
this.sendFrame(item.latestPayload);
|
|
153
|
+
}
|
|
154
|
+
this.pending.delete(token);
|
|
155
|
+
}
|
|
156
|
+
await this._flushQueue();
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/mcp.mjs
CHANGED
|
@@ -4,10 +4,12 @@ import { gate, dispatch } from "./engine.mjs";
|
|
|
4
4
|
import { classifyRiskTier } from "./risk.mjs";
|
|
5
5
|
import { checkDailyBudget, lockStatus } from "./state.mjs";
|
|
6
6
|
import { reapOrphanedIntents, reapStaleMutexDirs } from "./journal.mjs";
|
|
7
|
+
import { readTelemetry } from "./telemetry.mjs";
|
|
8
|
+
import { ProgressBus } from "./mcp-progress.mjs";
|
|
7
9
|
|
|
8
10
|
export const MCP_SERVER_INFO = {
|
|
9
11
|
name: "jules-orchestrator-kit",
|
|
10
|
-
version: "0.
|
|
12
|
+
version: "0.23.0",
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
|
|
@@ -136,6 +138,16 @@ export const MCP_TOOLS = [
|
|
|
136
138
|
properties: {},
|
|
137
139
|
},
|
|
138
140
|
},
|
|
141
|
+
{
|
|
142
|
+
name: "telemetry_tail",
|
|
143
|
+
description: "Query the last N telemetry events from the orchestrator telemetry spine.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
limit: { type: "number", description: "Number of telemetry events to retrieve (default 50, max 500)" },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
139
151
|
];
|
|
140
152
|
|
|
141
153
|
export async function handleMcpRequest(request, opts = {}) {
|
|
@@ -260,6 +272,25 @@ export async function handleMcpRequest(request, opts = {}) {
|
|
|
260
272
|
};
|
|
261
273
|
}
|
|
262
274
|
|
|
275
|
+
if (toolName === "telemetry_tail") {
|
|
276
|
+
if (args.limit !== undefined && (typeof args.limit !== "number" || args.limit < 1)) {
|
|
277
|
+
return {
|
|
278
|
+
jsonrpc: "2.0",
|
|
279
|
+
id,
|
|
280
|
+
error: { code: -32602, message: "Invalid parameters: 'limit' must be a positive number" },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const limit = Math.min(args.limit || 50, 500);
|
|
284
|
+
const events = readTelemetry(root, limit);
|
|
285
|
+
return {
|
|
286
|
+
jsonrpc: "2.0",
|
|
287
|
+
id,
|
|
288
|
+
result: {
|
|
289
|
+
content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
263
294
|
return {
|
|
264
295
|
jsonrpc: "2.0",
|
|
265
296
|
id,
|
|
@@ -361,11 +392,14 @@ export function startMcpServer(input = process.stdin, output = process.stdout, o
|
|
|
361
392
|
isolateMcpStdout(output, opts);
|
|
362
393
|
}
|
|
363
394
|
|
|
395
|
+
const progressBus = opts.progressBus || new ProgressBus(output, opts);
|
|
364
396
|
const decoder = new McpFrameDecoder({ maxFrameSize: opts.maxFrameSize || MAX_MCP_FRAME_SIZE });
|
|
365
397
|
|
|
366
398
|
decoder.on("data", async (request) => {
|
|
367
399
|
try {
|
|
368
|
-
const
|
|
400
|
+
const progressToken = request?.params?._meta?.progressToken || request?.params?.progressToken;
|
|
401
|
+
const requestOpts = { ...opts, progressBus, progressToken };
|
|
402
|
+
const response = await handleMcpRequest(request, requestOpts);
|
|
369
403
|
if (response !== null && response !== undefined) {
|
|
370
404
|
writeMcpFrame(output, JSON.stringify(response) + "\n");
|
|
371
405
|
}
|
package/src/merge-verify.mjs
CHANGED
|
@@ -31,14 +31,14 @@ export function mergeVerifyChain(mergedText, fileName, root = process.cwd()) {
|
|
|
31
31
|
tool = "python3 -m py_compile";
|
|
32
32
|
} else if (ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts") {
|
|
33
33
|
const hasTsConfig = existsSync(join(root, "tsconfig.json"));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const localTsc = join(root, "node_modules", ".bin", "tsc");
|
|
35
|
+
const hasLocalTsc = existsSync(localTsc);
|
|
36
|
+
if (hasTsConfig && hasLocalTsc) {
|
|
37
|
+
cmd = localTsc;
|
|
38
|
+
args = ["--noEmit", tempFilePath];
|
|
37
39
|
tool = "tsc --noEmit";
|
|
38
40
|
} else {
|
|
39
|
-
|
|
40
|
-
args = ["--check", tempFilePath];
|
|
41
|
-
tool = "node --check";
|
|
41
|
+
return { ok: true, tool: "ts-skipped-no-tsconfig" };
|
|
42
42
|
}
|
|
43
43
|
} else {
|
|
44
44
|
// JS, MJS, CJS or default
|
|
@@ -47,7 +47,7 @@ export function mergeVerifyChain(mergedText, fileName, root = process.cwd()) {
|
|
|
47
47
|
tool = "node --check";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
50
|
+
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: 120000 });
|
|
51
51
|
const ok = res.status === 0;
|
|
52
52
|
|
|
53
53
|
return {
|
package/src/security.mjs
CHANGED
|
@@ -64,7 +64,9 @@ export function safeAtomicWrite(filePath, content, options = {}) {
|
|
|
64
64
|
const fd = openSync(tempFile, "wx", mode);
|
|
65
65
|
try {
|
|
66
66
|
writeSync(fd, content, null, encoding);
|
|
67
|
-
|
|
67
|
+
if (options.sync !== false) {
|
|
68
|
+
fsyncSync(fd);
|
|
69
|
+
}
|
|
68
70
|
} finally {
|
|
69
71
|
closeSync(fd);
|
|
70
72
|
}
|
package/src/state.mjs
CHANGED
|
@@ -349,8 +349,9 @@ export function isPidAlive(pid, expectedStartTime = null) {
|
|
|
349
349
|
return true;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot()) {
|
|
353
|
-
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
352
|
+
export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot(), opts = {}) {
|
|
353
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : (rootOrOpts?.root || resolveRoot());
|
|
354
|
+
const branch = (typeof rootOrOpts === "object" && rootOrOpts?.branch) || opts?.branch || process.env.JULES_BRANCH || process.env.BRANCH_NAME || "";
|
|
354
355
|
const lockDir = getLockDir(root);
|
|
355
356
|
const lockFile = join(lockDir, `${taskId}.json`);
|
|
356
357
|
|
|
@@ -375,6 +376,7 @@ export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveR
|
|
|
375
376
|
const payload = {
|
|
376
377
|
agent: agentName,
|
|
377
378
|
taskId,
|
|
379
|
+
branch,
|
|
378
380
|
files,
|
|
379
381
|
pid: process.pid,
|
|
380
382
|
processStartTime: startTime,
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import {
|
|
2
|
+
openSync,
|
|
3
|
+
writeSync,
|
|
4
|
+
readSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
statSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { resolveRoot } from "./config.mjs";
|
|
14
|
+
import { getStateDir, withVfsMutex } from "./state.mjs";
|
|
15
|
+
import { safeAtomicWrite } from "./security.mjs";
|
|
16
|
+
|
|
17
|
+
export const MAX_TELEMETRY_SEGMENT_BYTES = 8 * 1024 * 1024; // 8 MB log segment ceiling
|
|
18
|
+
export const TELEMETRY_GENESIS_HASH = "0".repeat(64);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reads up to 64 KB from EOF of target log segment to extract true tail hash.
|
|
22
|
+
*/
|
|
23
|
+
function readTailHash(filePath, bufferSize = 64 * 1024) {
|
|
24
|
+
if (!existsSync(filePath)) return null;
|
|
25
|
+
try {
|
|
26
|
+
const stat = statSync(filePath);
|
|
27
|
+
if (stat.size === 0) return null;
|
|
28
|
+
const readSize = Math.min(stat.size, bufferSize);
|
|
29
|
+
const position = stat.size - readSize;
|
|
30
|
+
const fd = openSync(filePath, "r");
|
|
31
|
+
const buf = Buffer.alloc(readSize);
|
|
32
|
+
try {
|
|
33
|
+
readSync(fd, buf, 0, readSize, position);
|
|
34
|
+
} finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
const content = buf.toString("utf-8");
|
|
38
|
+
const lines = content.split("\n").filter(Boolean);
|
|
39
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
40
|
+
try {
|
|
41
|
+
const obj = JSON.parse(lines[i]);
|
|
42
|
+
if (obj && typeof obj.hash === "string") {
|
|
43
|
+
return obj.hash;
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
}
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns telemetry file paths for a given date string sorted by segment index.
|
|
53
|
+
*/
|
|
54
|
+
function getTelemetrySegmentFiles(stateDir, dateStr) {
|
|
55
|
+
if (!existsSync(stateDir)) return [];
|
|
56
|
+
const prefix = `telemetry-${dateStr}`;
|
|
57
|
+
const files = readdirSync(stateDir).filter(
|
|
58
|
+
(f) => f.startsWith(prefix) && f.endsWith(".jsonl")
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return files
|
|
62
|
+
.map((name) => {
|
|
63
|
+
let index = 0;
|
|
64
|
+
if (name !== `${prefix}.jsonl`) {
|
|
65
|
+
const match = name.match(/telemetry-[0-9]{4}-[0-9]{2}-[0-9]{2}-([0-9]+)\.jsonl$/);
|
|
66
|
+
if (match) index = parseInt(match[1], 10);
|
|
67
|
+
}
|
|
68
|
+
return { name, path: join(stateDir, name), index };
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => a.index - b.index);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Performs a cold scan of telemetry jsonl files to recover the last valid hash and active segment index.
|
|
75
|
+
*/
|
|
76
|
+
function coldScanTelemetry(stateDir, dateStr) {
|
|
77
|
+
const segments = getTelemetrySegmentFiles(stateDir, dateStr);
|
|
78
|
+
if (segments.length === 0) {
|
|
79
|
+
return { hash: TELEMETRY_GENESIS_HASH, segment: 0 };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
83
|
+
const seg = segments[i];
|
|
84
|
+
if (existsSync(seg.path)) {
|
|
85
|
+
const tailHash = readTailHash(seg.path);
|
|
86
|
+
if (tailHash) {
|
|
87
|
+
return { hash: tailHash, segment: seg.index };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { hash: TELEMETRY_GENESIS_HASH, segment: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Appends a structured telemetry event to .agent/state/telemetry-<date>.jsonl with SHA-256 hash chaining.
|
|
97
|
+
* Uses .head atomic cache with true tail reconciliation for O(1) constant-time appends.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} [rootOrOpts]
|
|
100
|
+
* @param {string} kind
|
|
101
|
+
* @param {Object} [fields={}]
|
|
102
|
+
* @returns {Object} Appended telemetry record
|
|
103
|
+
*/
|
|
104
|
+
export function appendTelemetry(rootOrOpts = resolveRoot(), kind = "event", fields = {}) {
|
|
105
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
106
|
+
const stateDir = getStateDir(root);
|
|
107
|
+
const mutexDir = join(stateDir, ".telemetry.mutex");
|
|
108
|
+
|
|
109
|
+
return withVfsMutex(mutexDir, () => {
|
|
110
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
111
|
+
const headPath = join(stateDir, `telemetry-${dateStr}.head`);
|
|
112
|
+
|
|
113
|
+
let prevHash = TELEMETRY_GENESIS_HASH;
|
|
114
|
+
let activeSegmentIndex = 0;
|
|
115
|
+
let headValid = false;
|
|
116
|
+
|
|
117
|
+
if (existsSync(headPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const headContent = readFileSync(headPath, "utf-8");
|
|
120
|
+
const headObj = JSON.parse(headContent);
|
|
121
|
+
if (headObj && typeof headObj.hash === "string") {
|
|
122
|
+
prevHash = headObj.hash;
|
|
123
|
+
activeSegmentIndex = typeof headObj.segment === "number" ? headObj.segment : 0;
|
|
124
|
+
headValid = true;
|
|
125
|
+
}
|
|
126
|
+
} catch (_) {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let activeFileName =
|
|
130
|
+
activeSegmentIndex === 0
|
|
131
|
+
? `telemetry-${dateStr}.jsonl`
|
|
132
|
+
: `telemetry-${dateStr}-${activeSegmentIndex}.jsonl`;
|
|
133
|
+
let activeFilePath = join(stateDir, activeFileName);
|
|
134
|
+
|
|
135
|
+
// Reconcile .head hash against true log tail (reading last 64 KB from EOF)
|
|
136
|
+
const tailHash = readTailHash(activeFilePath);
|
|
137
|
+
if (tailHash) {
|
|
138
|
+
if (tailHash !== prevHash) {
|
|
139
|
+
prevHash = tailHash;
|
|
140
|
+
}
|
|
141
|
+
headValid = true;
|
|
142
|
+
} else if (!headValid) {
|
|
143
|
+
const recovered = coldScanTelemetry(stateDir, dateStr);
|
|
144
|
+
prevHash = recovered.hash;
|
|
145
|
+
activeSegmentIndex = recovered.segment;
|
|
146
|
+
activeFileName =
|
|
147
|
+
activeSegmentIndex === 0
|
|
148
|
+
? `telemetry-${dateStr}.jsonl`
|
|
149
|
+
: `telemetry-${dateStr}-${activeSegmentIndex}.jsonl`;
|
|
150
|
+
activeFilePath = join(stateDir, activeFileName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (existsSync(activeFilePath)) {
|
|
154
|
+
try {
|
|
155
|
+
const stat = statSync(activeFilePath);
|
|
156
|
+
if (stat.size >= MAX_TELEMETRY_SEGMENT_BYTES) {
|
|
157
|
+
activeSegmentIndex += 1;
|
|
158
|
+
activeFileName = `telemetry-${dateStr}-${activeSegmentIndex}.jsonl`;
|
|
159
|
+
activeFilePath = join(stateDir, activeFileName);
|
|
160
|
+
}
|
|
161
|
+
} catch (_) {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const ts = new Date().toISOString();
|
|
165
|
+
const payloadWithoutHash = { v: 1, ts, kind, ...fields, prevHash };
|
|
166
|
+
const hash = createHash("sha256").update(JSON.stringify(payloadWithoutHash)).digest("hex");
|
|
167
|
+
const entry = { ...payloadWithoutHash, hash };
|
|
168
|
+
|
|
169
|
+
const fd = openSync(activeFilePath, "a");
|
|
170
|
+
try {
|
|
171
|
+
writeSync(fd, JSON.stringify(entry) + "\n", "utf-8");
|
|
172
|
+
} finally {
|
|
173
|
+
closeSync(fd);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
safeAtomicWrite(
|
|
177
|
+
headPath,
|
|
178
|
+
JSON.stringify({ v: 1, hash, segment: activeSegmentIndex, ts }),
|
|
179
|
+
{ sync: false }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return entry;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Reads telemetry records across segments for a target date (or current date if omitted).
|
|
188
|
+
* Returns array of telemetry objects in chronological order.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} [rootOrOpts]
|
|
191
|
+
* @param {number} [limit=50]
|
|
192
|
+
* @param {string} [targetDateStr]
|
|
193
|
+
* @returns {Array<Object>}
|
|
194
|
+
*/
|
|
195
|
+
export function readTelemetry(rootOrOpts = resolveRoot(), limit = 50, targetDateStr = null) {
|
|
196
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
197
|
+
const stateDir = getStateDir(root);
|
|
198
|
+
const dateStr = targetDateStr || new Date().toISOString().split("T")[0];
|
|
199
|
+
|
|
200
|
+
const segments = getTelemetrySegmentFiles(stateDir, dateStr);
|
|
201
|
+
if (segments.length === 0) return [];
|
|
202
|
+
|
|
203
|
+
const allEntries = [];
|
|
204
|
+
for (const seg of segments) {
|
|
205
|
+
if (existsSync(seg.path)) {
|
|
206
|
+
try {
|
|
207
|
+
const raw = readFileSync(seg.path, "utf-8");
|
|
208
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
try {
|
|
211
|
+
const entry = JSON.parse(line);
|
|
212
|
+
allEntries.push(Object.freeze(entry));
|
|
213
|
+
} catch (_) {}
|
|
214
|
+
}
|
|
215
|
+
} catch (_) {}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (allEntries.length <= limit) {
|
|
220
|
+
return allEntries;
|
|
221
|
+
}
|
|
222
|
+
return allEntries.slice(allEntries.length - limit);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Verifies the cryptographic SHA-256 hash-chain integrity of telemetry records for a date.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} [rootOrOpts]
|
|
229
|
+
* @param {string} [targetDateStr]
|
|
230
|
+
* @returns {Object}
|
|
231
|
+
*/
|
|
232
|
+
export function verifyTelemetryIntegrity(rootOrOpts = resolveRoot(), targetDateStr = null) {
|
|
233
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
234
|
+
const stateDir = getStateDir(root);
|
|
235
|
+
const dateStr = targetDateStr || new Date().toISOString().split("T")[0];
|
|
236
|
+
|
|
237
|
+
const segments = getTelemetrySegmentFiles(stateDir, dateStr);
|
|
238
|
+
if (segments.length === 0) {
|
|
239
|
+
return { ok: true, count: 0, lastHash: TELEMETRY_GENESIS_HASH };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let expectedPrevHash = TELEMETRY_GENESIS_HASH;
|
|
243
|
+
let totalCount = 0;
|
|
244
|
+
|
|
245
|
+
for (const seg of segments) {
|
|
246
|
+
if (!existsSync(seg.path)) continue;
|
|
247
|
+
try {
|
|
248
|
+
const raw = readFileSync(seg.path, "utf-8");
|
|
249
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
250
|
+
for (let i = 0; i < lines.length; i++) {
|
|
251
|
+
totalCount++;
|
|
252
|
+
let obj;
|
|
253
|
+
try {
|
|
254
|
+
obj = JSON.parse(lines[i]);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
return { ok: false, segment: seg.name, line: i + 1, error: "TORN_WRITE_CORRUPTION", detail: err.message };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!obj.hash || !obj.prevHash) {
|
|
260
|
+
return { ok: false, segment: seg.name, line: i + 1, error: "MISSING_HASH_FIELDS" };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (obj.prevHash !== expectedPrevHash) {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
segment: seg.name,
|
|
267
|
+
line: i + 1,
|
|
268
|
+
error: "BROKEN_PREV_HASH",
|
|
269
|
+
expected: expectedPrevHash,
|
|
270
|
+
actual: obj.prevHash,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { hash, ...rest } = obj;
|
|
275
|
+
const recomputed = createHash("sha256").update(JSON.stringify(rest)).digest("hex");
|
|
276
|
+
if (recomputed !== hash) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
segment: seg.name,
|
|
280
|
+
line: i + 1,
|
|
281
|
+
error: "CORRUPTED_ENTRY_HASH",
|
|
282
|
+
expected: recomputed,
|
|
283
|
+
actual: hash,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
expectedPrevHash = hash;
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
return { ok: false, count: totalCount, error: err.message };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const headPath = join(stateDir, `telemetry-${dateStr}.head`);
|
|
294
|
+
if (existsSync(headPath)) {
|
|
295
|
+
try {
|
|
296
|
+
const headObj = JSON.parse(readFileSync(headPath, "utf-8"));
|
|
297
|
+
if (headObj.hash !== expectedPrevHash) {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
error: "HEAD_HASH_MISMATCH",
|
|
301
|
+
expected: expectedPrevHash,
|
|
302
|
+
actual: headObj.hash,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
} catch (_) {
|
|
306
|
+
return { ok: false, error: "CORRUPTED_HEAD_FILE" };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { ok: true, count: totalCount, lastHash: expectedPrevHash };
|
|
311
|
+
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Backward compatibility shim for jules-cleanup.mjs in v0.9.0.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export function extractPrUrls(outputs = []) {
|
|
8
|
-
const prs = [];
|
|
9
|
-
const regex = /https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
|
|
10
|
-
for (const item of outputs) {
|
|
11
|
-
const text = typeof item === "string" ? item : item?.link || JSON.stringify(item);
|
|
12
|
-
const matches = text.match(regex);
|
|
13
|
-
if (matches) {
|
|
14
|
-
for (const m of matches) {
|
|
15
|
-
if (!prs.includes(m)) prs.push(m);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return prs;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function auditSessions(sessions = [], opts = {}) {
|
|
23
|
-
const staleHoursThreshold = opts.staleHoursThreshold || 24;
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
const merged = [];
|
|
26
|
-
const active = [];
|
|
27
|
-
const stale = [];
|
|
28
|
-
|
|
29
|
-
for (const s of sessions) {
|
|
30
|
-
if (s.state === "MERGED") {
|
|
31
|
-
merged.push(s);
|
|
32
|
-
} else {
|
|
33
|
-
const updatedMs = s.updateTime ? new Date(s.updateTime).getTime() : now;
|
|
34
|
-
const ageHours = (now - updatedMs) / (1000 * 60 * 60);
|
|
35
|
-
if (ageHours > staleHoursThreshold) {
|
|
36
|
-
stale.push(s);
|
|
37
|
-
} else {
|
|
38
|
-
active.push(s);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { merged, active, stale };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (process.argv[1] && process.argv[1].endsWith("jules-cleanup.mjs")) {
|
|
47
|
-
console.log("[Shim] Cleanup audit completed.");
|
|
48
|
-
process.exit(0);
|
|
49
|
-
}
|
package/scripts/jules-swarm.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Backward compatibility shim for jules-swarm.mjs in v0.9.0.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
|
|
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
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
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";
|
|
22
|
-
|
|
23
|
-
const agentDir = join(projectRoot, ".agent");
|
|
24
|
-
if (!existsSync(agentDir)) {
|
|
25
|
-
try { mkdirSync(agentDir, { recursive: true }); } catch (_) {}
|
|
26
|
-
}
|
|
27
|
-
const manifestPath = join(agentDir, "sync-manifest.json");
|
|
28
|
-
try { writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); } catch (_) {}
|
|
29
|
-
|
|
30
|
-
if (isDry) return { status: "DRY_RUN", path: manifestPath };
|
|
31
|
-
if (isRemote) return { status: "PUSHED", path: manifestPath };
|
|
32
|
-
return { status: "SAVED_LOCAL", path: manifestPath };
|
|
33
|
-
}
|
|
34
|
-
|
|
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);
|
|
38
|
-
}
|
package/scripts/lock-manager.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backward compatibility shim for lock-manager.mjs in v0.9.0.
|
|
3
|
-
* Delegates to src/state.mjs.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { acquireLock, releaseLock, lockStatus } from "../src/state.mjs";
|
|
7
|
-
|
|
8
|
-
export function acquire(agent, taskId, filePaths = [], opts = {}) {
|
|
9
|
-
return acquireLock(agent, taskId, filePaths, opts);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function release(agent, taskId, opts = {}) {
|
|
13
|
-
return releaseLock(agent, taskId, opts);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function status(opts = {}) {
|
|
17
|
-
return lockStatus(opts);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function cleanup() {
|
|
21
|
-
return { removed: 0 };
|
|
22
|
-
}
|