jules-orchestrator-kit 0.29.0 → 0.31.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/README.md +174 -89
- package/bin/agentctl.mjs +205 -6
- package/index.mjs +21 -1
- package/package.json +1 -1
- package/src/config.mjs +18 -2
- package/src/dag-engine.mjs +76 -0
- package/src/dashboard.mjs +2 -2
- package/src/engine.mjs +296 -37
- package/src/envelope.mjs +24 -2
- package/src/mcp.mjs +35 -1
- package/src/ops/checkpoint.mjs +167 -0
- package/src/ops/command-registry.mjs +274 -0
- package/src/ops/doctor-planner.mjs +214 -0
- package/src/ops/doctor-registry.mjs +442 -0
- package/src/ops/ide-scaffold.mjs +139 -0
- package/src/ops/receipts.mjs +171 -0
- package/src/ops/swarm-actions.mjs +156 -0
- package/src/ops/task-actions.mjs +214 -0
- package/src/ops/tdd-generator.mjs +115 -0
- package/src/ops/transaction.mjs +278 -0
- package/src/provider.mjs +164 -8
- package/src/security.mjs +15 -7
- package/src/stack-detector.mjs +15 -5
- package/src/state.mjs +44 -7
- package/src/task-optimizer.mjs +306 -0
- package/src/telemetry.mjs +35 -2
- package/src/tui.mjs +4 -1
- package/src/ux/capabilities.mjs +180 -0
- package/src/ux/diff-viewer.mjs +289 -0
- package/src/ux/key-decoder.mjs +281 -0
- package/src/ux/layout.mjs +195 -0
- package/src/ux/log-viewer.mjs +110 -0
- package/src/ux/palette.mjs +179 -0
- package/src/ux/queue-model.mjs +206 -0
- package/src/ux/renderer.mjs +188 -0
- package/src/ux/swarm-model.mjs +146 -0
- package/src/ux/terminal-session.mjs +215 -0
- package/src/ux/widgets.mjs +323 -0
- package/src/webhook.mjs +120 -0
- package/src/wizard-init.mjs +41 -11
- package/src/wizard-task.mjs +51 -15
package/src/engine.mjs
CHANGED
|
@@ -6,11 +6,26 @@ import { withBudget, appendLedger, getQueueDir, ensureDir, rollbackBudgetReserva
|
|
|
6
6
|
import { sanitizeUntrustedData, buildAgentEnvelope } from "./prompt-guard.mjs";
|
|
7
7
|
import { recordVerifyRun, readVerifyRuns, flakyVerdict } from "./flaky-ledger.mjs";
|
|
8
8
|
import fs, { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
9
|
+
import { join, basename } from "node:path";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
-
import { appendTelemetry } from "./telemetry.mjs";
|
|
11
|
+
import { appendTelemetry as appendTelemetryUnsafe } from "./telemetry.mjs";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { resolveAffectedTests } from "./dag-engine.mjs";
|
|
15
|
+
|
|
16
|
+
export { recordVerifyRun, readVerifyRuns, flakyVerdict, sanitizeUntrustedData, resolveAffectedTests };
|
|
17
|
+
|
|
18
|
+
function appendTelemetry(root, kind, fields = {}) {
|
|
19
|
+
try {
|
|
20
|
+
return appendTelemetryUnsafe(root, kind, fields);
|
|
21
|
+
} catch (_) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isSafeQueueFileName(fileName) {
|
|
27
|
+
return typeof fileName === "string" && fileName.length > 0 && basename(fileName) === fileName && fileName !== "." && fileName !== "..";
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
/**
|
|
16
31
|
* Validates whether a file in .agent/jules-queue/ is a task file.
|
|
@@ -46,14 +61,8 @@ export function isTaskFile(fileName, queueDirOrContent = null) {
|
|
|
46
61
|
}
|
|
47
62
|
if (typeof content === "string") {
|
|
48
63
|
const trimmed = content.trimStart();
|
|
49
|
-
if (trimmed.startsWith("---")) {
|
|
50
|
-
|
|
51
|
-
if (endFm !== -1) {
|
|
52
|
-
const frontMatter = trimmed.slice(3, endFm);
|
|
53
|
-
if (/(?:taskId|envelopeId|baseRef|baseSha|task|title|id|version|capabilities|scope|verify):/i.test(frontMatter)) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
64
|
+
if (trimmed.includes("JULES_TASK_ENVELOPE") || trimmed.includes("# Task ID:") || trimmed.startsWith("---")) {
|
|
65
|
+
return true;
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
} catch (_) {}
|
|
@@ -134,8 +143,11 @@ export async function gate(opts = {}) {
|
|
|
134
143
|
trustedScope = normalizeScope(parsed);
|
|
135
144
|
if (parsed.verify || parsed.test_cmd || parsed.build_cmd) {
|
|
136
145
|
trustedVerify = {
|
|
146
|
+
setup: parsed.verify?.setup || config.verify.setup,
|
|
137
147
|
test: parsed.verify?.test || parsed.test_cmd || config.verify.test,
|
|
148
|
+
teardown: parsed.verify?.teardown || config.verify.teardown,
|
|
138
149
|
build: parsed.verify?.build || parsed.build_cmd || config.verify.build,
|
|
150
|
+
timeoutMs: parsed.verify?.timeoutMs || parsed.verify?.timeout_ms || config.verify.timeoutMs,
|
|
139
151
|
};
|
|
140
152
|
}
|
|
141
153
|
} catch (_) {}
|
|
@@ -207,34 +219,79 @@ export async function gate(opts = {}) {
|
|
|
207
219
|
let flakyVerdictResult = null;
|
|
208
220
|
|
|
209
221
|
if (testCmd) {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
const setupCmd = trustedVerify.setup;
|
|
223
|
+
const teardownCmd = trustedVerify.teardown;
|
|
224
|
+
const verifyTimeout = trustedVerify.timeoutMs;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (setupCmd) {
|
|
228
|
+
const setupRes = runCmd(setupCmd, { cwd: root, ignoreError: true, env: testEnv, timeout: verifyTimeout });
|
|
229
|
+
if (setupRes.status !== 0) {
|
|
230
|
+
testResult = {
|
|
231
|
+
ok: false,
|
|
232
|
+
status: setupRes.status,
|
|
233
|
+
stdout: redactSecrets(setupRes.stdout || ""),
|
|
234
|
+
stderr: redactSecrets(setupRes.stderr || ""),
|
|
235
|
+
command: setupCmd,
|
|
236
|
+
phase: "setup",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (testResult.ok) {
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
const res = runCmd(testCmd, { cwd: root, ignoreError: true, env: testEnv, timeout: verifyTimeout });
|
|
244
|
+
const durationMs = Date.now() - startTime;
|
|
245
|
+
testResult = {
|
|
246
|
+
ok: res.status === 0,
|
|
247
|
+
status: res.status,
|
|
248
|
+
stdout: redactSecrets(res.stdout || ""),
|
|
249
|
+
stderr: redactSecrets(res.stderr || ""),
|
|
250
|
+
command: testCmd,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const fingerprint = !testResult.ok ? fingerprintFailureState(testResult, root) : null;
|
|
254
|
+
recordVerifyRun(root, testCmd, testResult.ok, fingerprint, durationMs);
|
|
255
|
+
|
|
256
|
+
if (!testResult.ok) {
|
|
257
|
+
const runs = readVerifyRuns(root, testCmd);
|
|
258
|
+
flakyVerdictResult = flakyVerdict(runs);
|
|
259
|
+
if (flakyVerdictResult.verdict === "QUARANTINED") {
|
|
260
|
+
phases.push({ phase: "verify", ok: false, testResult, buildResult, flakyVerdict: flakyVerdictResult });
|
|
261
|
+
appendTelemetry(root, "gate_phase", { phase: "verify", ok: false, quarantined: true });
|
|
262
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 8 });
|
|
263
|
+
return { ok: false, code: 8, phases, flakyVerdict: flakyVerdictResult };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
if (teardownCmd) {
|
|
269
|
+
try {
|
|
270
|
+
runCmd(teardownCmd, { cwd: root, ignoreError: true, env: testEnv });
|
|
271
|
+
} catch (_) {}
|
|
226
272
|
}
|
|
227
273
|
}
|
|
228
274
|
}
|
|
229
275
|
|
|
230
276
|
if (testResult.ok && buildCmd) {
|
|
231
277
|
const res = runCmd(buildCmd, { cwd: root, ignoreError: true, env: testEnv });
|
|
232
|
-
buildResult = {
|
|
278
|
+
buildResult = {
|
|
279
|
+
ok: res.status === 0,
|
|
280
|
+
status: res.status,
|
|
281
|
+
stdout: redactSecrets(res.stdout || ""),
|
|
282
|
+
stderr: redactSecrets(res.stderr || ""),
|
|
283
|
+
command: buildCmd,
|
|
284
|
+
};
|
|
233
285
|
}
|
|
234
286
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
287
|
+
let serverResult = { ok: true, status: 0 };
|
|
288
|
+
if (testResult.ok && buildResult.ok && trustedVerify.server && trustedVerify.server.command) {
|
|
289
|
+
serverResult = await probeDevServer(trustedVerify.server, root);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const verifyOk = testResult.ok && buildResult.ok && serverResult.ok;
|
|
293
|
+
const failingCmd = !testResult.ok ? testResult : !buildResult.ok ? buildResult : !serverResult.ok ? serverResult : null;
|
|
294
|
+
phases.push({ phase: "verify", ok: verifyOk, testResult, buildResult, serverResult });
|
|
238
295
|
appendTelemetry(root, "gate_phase", { phase: "verify", ok: verifyOk });
|
|
239
296
|
if (progressBus && progressToken) {
|
|
240
297
|
progressBus.reportProgress(progressToken, 100, 100, "Phase 4/4: Automated Test & Build Verification complete");
|
|
@@ -348,7 +405,7 @@ export async function repair(failure, opts = {}) {
|
|
|
348
405
|
);
|
|
349
406
|
} catch (err) {
|
|
350
407
|
if (err instanceof ProviderRateLimitError || err instanceof ProviderUnavailableError) {
|
|
351
|
-
if (err.reservationId) {
|
|
408
|
+
if (err.reservationId && !err.budgetReservationRolledBack) {
|
|
352
409
|
rollbackBudgetReservation(root, err.reservationId);
|
|
353
410
|
}
|
|
354
411
|
const retryAfterMs = err.retryAfterMs || 60000;
|
|
@@ -479,7 +536,10 @@ Stderr:
|
|
|
479
536
|
${cleanStderr}`;
|
|
480
537
|
}
|
|
481
538
|
|
|
482
|
-
export async function dispatch(task, opts = {}) {
|
|
539
|
+
export async function dispatch(task = {}, opts = {}) {
|
|
540
|
+
if (!task || typeof task !== "object") {
|
|
541
|
+
throw new TypeError("Dispatch task must be an object");
|
|
542
|
+
}
|
|
483
543
|
const root = opts.config?._root || opts.root || process.cwd();
|
|
484
544
|
const config = opts.config || loadConfig(root);
|
|
485
545
|
const provider = createProvider(config.provider, config);
|
|
@@ -509,7 +569,7 @@ export async function dispatch(task, opts = {}) {
|
|
|
509
569
|
);
|
|
510
570
|
} catch (err) {
|
|
511
571
|
if (err instanceof ProviderRateLimitError || err instanceof ProviderUnavailableError) {
|
|
512
|
-
if (err.reservationId) {
|
|
572
|
+
if (err.reservationId && !err.budgetReservationRolledBack) {
|
|
513
573
|
rollbackBudgetReservation(root, err.reservationId);
|
|
514
574
|
}
|
|
515
575
|
const retryAfterMs = err.retryAfterMs || 60000;
|
|
@@ -540,7 +600,8 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
540
600
|
|
|
541
601
|
const root = options.root || process.cwd();
|
|
542
602
|
const config = options.config || loadConfig(root);
|
|
543
|
-
const
|
|
603
|
+
const requestedConcurrency = Number(options.concurrency ?? config.limits.concurrency ?? 1);
|
|
604
|
+
const concurrency = Number.isInteger(requestedConcurrency) && requestedConcurrency > 0 ? requestedConcurrency : 1;
|
|
544
605
|
const isDry = options.dryRun !== undefined ? options.dryRun : (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
|
|
545
606
|
const queueDir = getQueueDir(root);
|
|
546
607
|
const completedDir = join(queueDir, "completed");
|
|
@@ -559,9 +620,12 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
559
620
|
const batch = filesToProcess.slice(i, i + concurrency);
|
|
560
621
|
await Promise.all(
|
|
561
622
|
batch.map(async (fileOrItem) => {
|
|
562
|
-
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem
|
|
563
|
-
const srcPath = join(queueDir, fileName);
|
|
623
|
+
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem?.id || fileOrItem?.file || fileOrItem?.title || "task";
|
|
564
624
|
try {
|
|
625
|
+
if (!isSafeQueueFileName(fileName)) {
|
|
626
|
+
throw new Error(`Unsafe queue task path: ${fileName}`);
|
|
627
|
+
}
|
|
628
|
+
const srcPath = join(queueDir, fileName);
|
|
565
629
|
let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
|
|
566
630
|
let title = typeof fileOrItem === "object" && fileOrItem.title ? fileOrItem.title : fileName;
|
|
567
631
|
if (!prompt && existsSync(srcPath)) {
|
|
@@ -575,6 +639,9 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
575
639
|
} else {
|
|
576
640
|
const dstPath = join(completedDir, fileName);
|
|
577
641
|
if (existsSync(srcPath)) {
|
|
642
|
+
if (existsSync(dstPath)) {
|
|
643
|
+
throw new Error(`Completed task destination already exists: ${fileName}`);
|
|
644
|
+
}
|
|
578
645
|
renameSync(srcPath, dstPath);
|
|
579
646
|
}
|
|
580
647
|
appendLedger({ event: "task_completed", file: fileName, session }, root);
|
|
@@ -589,3 +656,195 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
589
656
|
|
|
590
657
|
return { processed: results.length, results };
|
|
591
658
|
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Synthesizes a structured, evidence-backed PR description body for GitHub PR creation.
|
|
662
|
+
* @param {object} session - Session or dispatch result
|
|
663
|
+
* @param {object} gateResult - Gate evaluation result
|
|
664
|
+
* @param {object} [options]
|
|
665
|
+
* @returns {string} Markdown PR description body
|
|
666
|
+
*/
|
|
667
|
+
export function synthesizePrDescription(session = {}, gateResult = {}, options = {}) {
|
|
668
|
+
const sessionId = session.id || session.name || "jules-session";
|
|
669
|
+
const attemptsCount = session.attempts?.length || session._warmAttempts || 1;
|
|
670
|
+
const maxAttempts = options.maxAttempts || 3;
|
|
671
|
+
const durationMs = options.durationMs || session.durationMs || 0;
|
|
672
|
+
const durationSec = (durationMs / 1000).toFixed(1);
|
|
673
|
+
const isWarm = Boolean(session.resumed || session._warmResumed);
|
|
674
|
+
|
|
675
|
+
const phases = gateResult.phases || [];
|
|
676
|
+
const scopePhase = phases.find((p) => p.phase === "scope") || { ok: true, violations: [] };
|
|
677
|
+
const payloadPhase = phases.find((p) => p.phase === "payload") || { ok: true, bytes: 0, limitBytes: 76800 };
|
|
678
|
+
const secretPhase = phases.find((p) => p.phase === "secrets") || { ok: true, findings: [] };
|
|
679
|
+
|
|
680
|
+
const kbDiff = ((payloadPhase.bytes || 0) / 1024).toFixed(1);
|
|
681
|
+
const kbLimit = ((payloadPhase.limitBytes || 76800) / 1024).toFixed(0);
|
|
682
|
+
|
|
683
|
+
const modifiedFiles = options.modifiedFiles || [];
|
|
684
|
+
const affectedTests = resolveAffectedTests(modifiedFiles, options);
|
|
685
|
+
|
|
686
|
+
const prBody = `## 🚀 Autonomous Jules Agent Execution Evidence
|
|
687
|
+
|
|
688
|
+
### ⏱️ OODA Verification Trace
|
|
689
|
+
- **Session ID:** \`${sessionId}\`
|
|
690
|
+
- **OODA Turns:** \`${attemptsCount}/${maxAttempts}\`
|
|
691
|
+
- **Warm Resumption:** ${isWarm ? "✅ Active Context Stream" : "Cold Start"}
|
|
692
|
+
- **Execution Latency:** \`${durationSec}s\`
|
|
693
|
+
|
|
694
|
+
### 🛡️ Zero-Trust Security Audit Matrix
|
|
695
|
+
- **Scope Guard:** ${scopePhase.ok ? "✅ PASS (0 protected path violations)" : "❌ FAIL"}
|
|
696
|
+
- **Diff Payload Governor:** ${payloadPhase.ok ? `✅ PASS (${kbDiff} KB / ${kbLimit} KB limit)` : "❌ EXCEEDED"}
|
|
697
|
+
- **Secret Scanner:** ${secretPhase.ok ? "✅ PASS (0 high-entropy leaks detected)" : "❌ LEAKS FOUND"}
|
|
698
|
+
|
|
699
|
+
### 🧪 Terminal Verification Evidence
|
|
700
|
+
\`\`\`text
|
|
701
|
+
${options.testOutput || "All verification tests passed with Exit Code 0."}
|
|
702
|
+
\`\`\`
|
|
703
|
+
|
|
704
|
+
### 📂 AST Impact & Dependency Graph
|
|
705
|
+
${
|
|
706
|
+
affectedTests === null
|
|
707
|
+
? "- **Impact Scope:** ⚠️ Global contract change detected (`package.json`/`tsconfig`/`schema`). Full test suite executed."
|
|
708
|
+
: affectedTests.length > 0
|
|
709
|
+
? `- **Affected Test Suites (${affectedTests.length}):**\n` + affectedTests.map((t) => ` - \`${t}\``).join("\n")
|
|
710
|
+
: "- **Impact Scope:** Isolated leaf implementation change."
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
*Generated automatically by [jules-orchestrator-kit](https://github.com/FullThrottle83/jules-orchestrator-kit)*
|
|
715
|
+
`;
|
|
716
|
+
|
|
717
|
+
return prBody;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Live Dev Server & SSR Hydration Smoke Prober.
|
|
722
|
+
* Spawns the server in a detached process group, polls the URL via globalThis.fetch,
|
|
723
|
+
* intercepts SSR hydration errors, and cleanly kills the process group in a finally block.
|
|
724
|
+
* @param {object} serverConfig
|
|
725
|
+
* @param {string} serverConfig.command
|
|
726
|
+
* @param {string} [serverConfig.url="http://localhost:3000"]
|
|
727
|
+
* @param {number} [serverConfig.timeoutMs=15000]
|
|
728
|
+
* @param {string} root
|
|
729
|
+
* @returns {Promise<object>} Probe result { ok: boolean, status: number, error: string, logs: string }
|
|
730
|
+
*/
|
|
731
|
+
export async function probeDevServer(serverConfig = {}, root = process.cwd()) {
|
|
732
|
+
if (!serverConfig || !serverConfig.command) {
|
|
733
|
+
return { ok: true, skipped: true, reason: "No server command configured" };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const cmd = serverConfig.command;
|
|
737
|
+
const url = serverConfig.url || "http://localhost:3000";
|
|
738
|
+
const timeoutMs = Number(serverConfig.timeoutMs || 15000);
|
|
739
|
+
|
|
740
|
+
const isWin = process.platform === "win32";
|
|
741
|
+
const shellBin = isWin ? (process.env.ComSpec || "cmd.exe") : "/bin/sh";
|
|
742
|
+
const shellArgs = isWin ? ["/d", "/s", "/c", cmd] : ["-c", cmd];
|
|
743
|
+
|
|
744
|
+
let child;
|
|
745
|
+
let stdoutLogs = "";
|
|
746
|
+
let stderrLogs = "";
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
child = spawn(shellBin, shellArgs, {
|
|
750
|
+
cwd: root,
|
|
751
|
+
detached: true,
|
|
752
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
753
|
+
env: process.env,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
if (child.stdout) {
|
|
757
|
+
child.stdout.on("data", (chunk) => {
|
|
758
|
+
stdoutLogs += chunk.toString("utf-8");
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (child.stderr) {
|
|
762
|
+
child.stderr.on("data", (chunk) => {
|
|
763
|
+
stderrLogs += chunk.toString("utf-8");
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const startTime = Date.now();
|
|
768
|
+
let pollResponse = null;
|
|
769
|
+
let pollError = null;
|
|
770
|
+
|
|
771
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
772
|
+
if (child.exitCode !== null) {
|
|
773
|
+
return {
|
|
774
|
+
ok: false,
|
|
775
|
+
status: child.exitCode,
|
|
776
|
+
error: `Dev server process exited prematurely with code ${child.exitCode}`,
|
|
777
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
783
|
+
const bodyText = await res.text();
|
|
784
|
+
pollResponse = { status: res.status, bodyText, ok: res.ok };
|
|
785
|
+
break;
|
|
786
|
+
} catch (err) {
|
|
787
|
+
pollError = err;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (!pollResponse) {
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
status: 504,
|
|
797
|
+
error: `Dev server probe timed out after ${timeoutMs}ms attempting to reach ${url}. ${pollError ? pollError.message : ""}`,
|
|
798
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const combinedLogs = (stderrLogs + "\n" + pollResponse.bodyText).toLowerCase();
|
|
803
|
+
const hydrationPanics = [
|
|
804
|
+
"hydration failed",
|
|
805
|
+
"text content did not match",
|
|
806
|
+
"unhandled runtime error",
|
|
807
|
+
"minified react error #418",
|
|
808
|
+
"minified react error #423",
|
|
809
|
+
"minified react error #425",
|
|
810
|
+
];
|
|
811
|
+
|
|
812
|
+
const detectedPanic = hydrationPanics.find((panic) => combinedLogs.includes(panic));
|
|
813
|
+
if (detectedPanic) {
|
|
814
|
+
return {
|
|
815
|
+
ok: false,
|
|
816
|
+
status: 500,
|
|
817
|
+
error: `SSR Hydration Smoke Probe Failure: Detected '${detectedPanic}' error in server response or stderr.`,
|
|
818
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!pollResponse.ok) {
|
|
823
|
+
return {
|
|
824
|
+
ok: false,
|
|
825
|
+
status: pollResponse.status,
|
|
826
|
+
error: `Dev server responded with HTTP status ${pollResponse.status}`,
|
|
827
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
ok: true,
|
|
833
|
+
status: pollResponse.status,
|
|
834
|
+
url,
|
|
835
|
+
logs: stdoutLogs.slice(-500),
|
|
836
|
+
};
|
|
837
|
+
} finally {
|
|
838
|
+
if (child && child.pid) {
|
|
839
|
+
try {
|
|
840
|
+
if (isWin) {
|
|
841
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
842
|
+
} else {
|
|
843
|
+
process.kill(-child.pid, "SIGTERM");
|
|
844
|
+
}
|
|
845
|
+
} catch (_) {}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
|
package/src/envelope.mjs
CHANGED
|
@@ -2,7 +2,7 @@ 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";
|
|
5
|
-
import {
|
|
5
|
+
import { resolve, relative, isAbsolute } from "node:path";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Validates a Task Envelope before dispatch or during CI triage.
|
|
@@ -67,7 +67,12 @@ export function validateEnvelope(envelope = {}, opts = {}) {
|
|
|
67
67
|
if (Array.isArray(envelope.referenced_paths)) {
|
|
68
68
|
for (const relPath of envelope.referenced_paths) {
|
|
69
69
|
if (typeof relPath !== "string") continue;
|
|
70
|
-
const absPath =
|
|
70
|
+
const absPath = resolve(root, relPath);
|
|
71
|
+
const rootRelative = relative(resolve(root), absPath);
|
|
72
|
+
if (isAbsolute(rootRelative) || rootRelative === ".." || rootRelative.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
73
|
+
errors.push(`Premise failure: referenced path '${relPath}' escapes repository root.`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
71
76
|
let existsInGit = false;
|
|
72
77
|
if (resolvedBase) {
|
|
73
78
|
const catRes = runCmd(["git", "cat-file", "-e", `${resolvedBase}:${relPath}`], { cwd: root, ignoreError: true });
|
|
@@ -103,3 +108,20 @@ export function validateEnvelope(envelope = {}, opts = {}) {
|
|
|
103
108
|
warnings,
|
|
104
109
|
};
|
|
105
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse JSON metadata payload embedded in HTML comment JULES_TASK_ENVELOPE.
|
|
114
|
+
* @param {string} content
|
|
115
|
+
* @returns {Record<string, any> | null}
|
|
116
|
+
*/
|
|
117
|
+
export function parseEnvelopeHeader(content) {
|
|
118
|
+
if (!content) return null;
|
|
119
|
+
const match = String(content).match(/<!--\s*JULES_TASK_ENVELOPE:\s*({[\s\S]*?})\s*-->/);
|
|
120
|
+
if (!match) return null;
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(match[1]);
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
package/src/mcp.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { ProgressBus } from "./mcp-progress.mjs";
|
|
|
9
9
|
|
|
10
10
|
export const MCP_SERVER_INFO = {
|
|
11
11
|
name: "jules-orchestrator-kit",
|
|
12
|
-
version: "0.29.
|
|
12
|
+
version: "0.29.1",
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
|
|
@@ -148,6 +148,19 @@ export const MCP_TOOLS = [
|
|
|
148
148
|
},
|
|
149
149
|
},
|
|
150
150
|
},
|
|
151
|
+
{
|
|
152
|
+
name: "optimize_jules_prompt",
|
|
153
|
+
description: "Evaluate task prompt falsifiability, check static path validity, and synthesize optimized task envelope.",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
prompt: { type: "string", description: "Raw task prompt instructions to analyze or optimize" },
|
|
158
|
+
fix: { type: "boolean", description: "If true, synthesizes and returns an optimized task envelope" },
|
|
159
|
+
verifyCmd: { type: "string", description: "Optional explicit verification command" },
|
|
160
|
+
},
|
|
161
|
+
required: ["prompt"],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
151
164
|
];
|
|
152
165
|
|
|
153
166
|
export async function handleMcpRequest(request, opts = {}) {
|
|
@@ -291,6 +304,27 @@ export async function handleMcpRequest(request, opts = {}) {
|
|
|
291
304
|
};
|
|
292
305
|
}
|
|
293
306
|
|
|
307
|
+
if (toolName === "optimize_jules_prompt") {
|
|
308
|
+
if (!args || typeof args.prompt !== "string") {
|
|
309
|
+
return {
|
|
310
|
+
jsonrpc: "2.0",
|
|
311
|
+
id,
|
|
312
|
+
error: { code: -32602, message: "Invalid parameters: 'prompt' must be a string" },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const { scorePromptFalsifiability, optimizeTaskPrompt } = await import("./task-optimizer.mjs");
|
|
316
|
+
const res = args.fix
|
|
317
|
+
? optimizeTaskPrompt(args.prompt, { rootDir: root, verifyCmd: args.verifyCmd })
|
|
318
|
+
: scorePromptFalsifiability(args.prompt, { rootDir: root, verifyCmd: args.verifyCmd });
|
|
319
|
+
return {
|
|
320
|
+
jsonrpc: "2.0",
|
|
321
|
+
id,
|
|
322
|
+
result: {
|
|
323
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
294
328
|
return {
|
|
295
329
|
jsonrpc: "2.0",
|
|
296
330
|
id,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { git, changedFiles, diffText } from "../git.mjs";
|
|
4
|
+
import { resolveRoot } from "../config.mjs";
|
|
5
|
+
|
|
6
|
+
export class CheckpointError extends Error {
|
|
7
|
+
constructor(message, opts = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "CheckpointError";
|
|
10
|
+
this.code = opts.code || 1;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensures checkpoint storage directory exists.
|
|
16
|
+
*/
|
|
17
|
+
export function getCheckpointDir(root = resolveRoot()) {
|
|
18
|
+
const dir = join(root, ".agent", "state", "checkpoints");
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an atomic pre-flight snapshot of the repository state before an agent task runs.
|
|
27
|
+
* @param {string} sessionId
|
|
28
|
+
* @param {object} options
|
|
29
|
+
* @returns {object} Checkpoint metadata
|
|
30
|
+
*/
|
|
31
|
+
export function createCheckpoint(sessionId = `session-${Date.now()}`, options = {}) {
|
|
32
|
+
const root = options.root || resolveRoot();
|
|
33
|
+
const dir = getCheckpointDir(root);
|
|
34
|
+
|
|
35
|
+
let headSha = "";
|
|
36
|
+
try {
|
|
37
|
+
headSha = git(["rev-parse", "HEAD"], { cwd: root, ignoreError: true }).trim();
|
|
38
|
+
} catch (_) {}
|
|
39
|
+
|
|
40
|
+
let branch = "";
|
|
41
|
+
try {
|
|
42
|
+
branch = git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root, ignoreError: true }).trim();
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
|
|
45
|
+
const uncommittedFiles = changedFiles(root, branch || "main", "working-tree");
|
|
46
|
+
const diffContent = diffText(root, branch || "main", "working-tree");
|
|
47
|
+
|
|
48
|
+
const snapshot = {
|
|
49
|
+
version: 1,
|
|
50
|
+
id: sessionId,
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
headSha,
|
|
53
|
+
branch,
|
|
54
|
+
uncommittedFiles,
|
|
55
|
+
diffContent,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const snapshotPath = join(dir, `${sessionId}.json`);
|
|
59
|
+
writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
60
|
+
|
|
61
|
+
// Keep last 10 checkpoints
|
|
62
|
+
pruneCheckpoints(root, 10);
|
|
63
|
+
|
|
64
|
+
return snapshot;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Restores working tree and git state to a previously saved checkpoint.
|
|
69
|
+
* @param {string} sessionId or '--latest'
|
|
70
|
+
* @param {object} options
|
|
71
|
+
* @returns {object} Restore result summary
|
|
72
|
+
*/
|
|
73
|
+
export function restoreCheckpoint(sessionId = "--latest", options = {}) {
|
|
74
|
+
const root = options.root || resolveRoot();
|
|
75
|
+
const dir = getCheckpointDir(root);
|
|
76
|
+
|
|
77
|
+
let targetId = sessionId;
|
|
78
|
+
if (!targetId || targetId === "--latest" || targetId === "latest") {
|
|
79
|
+
const list = listCheckpoints(root);
|
|
80
|
+
if (list.length === 0) {
|
|
81
|
+
throw new CheckpointError("No checkpoints found to restore.");
|
|
82
|
+
}
|
|
83
|
+
targetId = list[0].id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const snapshotFile = join(dir, `${targetId}.json`);
|
|
87
|
+
if (!existsSync(snapshotFile)) {
|
|
88
|
+
throw new CheckpointError(`Checkpoint snapshot file not found for session '${targetId}'.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let snapshot;
|
|
92
|
+
try {
|
|
93
|
+
snapshot = JSON.parse(readFileSync(snapshotFile, "utf-8"));
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new CheckpointError(`Corrupted checkpoint file '${targetId}.json': ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (snapshot.headSha) {
|
|
99
|
+
try {
|
|
100
|
+
git(["reset", "--hard", snapshot.headSha], { cwd: root, ignoreError: true });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw new CheckpointError(`Failed git reset --hard ${snapshot.headSha}: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Clean untracked files generated during session
|
|
107
|
+
try {
|
|
108
|
+
git(["clean", "-fd"], { cwd: root, ignoreError: true });
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
id: snapshot.id,
|
|
114
|
+
headSha: snapshot.headSha,
|
|
115
|
+
restoredAt: new Date().toISOString(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Lists all active checkpoints ordered by timestamp descending.
|
|
121
|
+
*/
|
|
122
|
+
export function listCheckpoints(root = resolveRoot()) {
|
|
123
|
+
const dir = getCheckpointDir(root);
|
|
124
|
+
if (!existsSync(dir)) return [];
|
|
125
|
+
|
|
126
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
127
|
+
const items = [];
|
|
128
|
+
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const filePath = join(dir, file);
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
133
|
+
items.push({
|
|
134
|
+
id: data.id || file.replace(/\.json$/, ""),
|
|
135
|
+
timestamp: data.timestamp || new Date(statSync(filePath).mtimeMs).toISOString(),
|
|
136
|
+
headSha: data.headSha || "",
|
|
137
|
+
branch: data.branch || "",
|
|
138
|
+
filePath,
|
|
139
|
+
});
|
|
140
|
+
} catch (_) {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
144
|
+
return items;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Prunes checkpoints keeping only the most recent N sessions.
|
|
149
|
+
*/
|
|
150
|
+
export function pruneCheckpoints(root = resolveRoot(), maxRetention = 10) {
|
|
151
|
+
const list = listCheckpoints(root);
|
|
152
|
+
if (list.length <= maxRetention) return 0;
|
|
153
|
+
|
|
154
|
+
const toRemove = list.slice(maxRetention);
|
|
155
|
+
let prunedCount = 0;
|
|
156
|
+
|
|
157
|
+
for (const item of toRemove) {
|
|
158
|
+
try {
|
|
159
|
+
if (existsSync(item.filePath)) {
|
|
160
|
+
rmSync(item.filePath, { force: true });
|
|
161
|
+
prunedCount++;
|
|
162
|
+
}
|
|
163
|
+
} catch (_) {}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return prunedCount;
|
|
167
|
+
}
|