jules-orchestrator-kit 0.29.1 → 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 +169 -86
- package/bin/agentctl.mjs +202 -3
- package/index.mjs +20 -0
- package/package.json +1 -1
- package/src/config.mjs +18 -2
- package/src/dag-engine.mjs +76 -0
- package/src/engine.mjs +294 -29
- package/src/envelope.mjs +24 -2
- package/src/mcp.mjs +34 -0
- 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 +14 -6
- 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 +2 -6
- 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-task.mjs +6 -0
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -196,9 +196,19 @@ export function detectStack(projectRoot = process.cwd()) {
|
|
|
196
196
|
return detectPolyglotStack(projectRoot);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
export function resolveVerify(root = process.cwd()) {
|
|
199
|
+
export function resolveVerify(root = process.cwd(), userVerify = {}) {
|
|
200
200
|
const s = detectStack(root);
|
|
201
|
-
return {
|
|
201
|
+
return {
|
|
202
|
+
setup: userVerify.setup ?? s.setupCmd ?? "",
|
|
203
|
+
test: userVerify.test ?? s.testCmd ?? "",
|
|
204
|
+
teardown: userVerify.teardown ?? s.teardownCmd ?? "",
|
|
205
|
+
build: userVerify.build ?? s.buildCmd ?? "",
|
|
206
|
+
server: userVerify.server ? {
|
|
207
|
+
command: userVerify.server.command || "",
|
|
208
|
+
url: userVerify.server.url || "http://localhost:3000",
|
|
209
|
+
timeoutMs: Number(userVerify.server.timeoutMs || 15000),
|
|
210
|
+
} : null,
|
|
211
|
+
};
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
export const TIER_PRESETS = {
|
|
@@ -247,8 +257,11 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
247
257
|
}
|
|
248
258
|
}
|
|
249
259
|
|
|
260
|
+
const setupCmd = parsed.setup_cmd || parsed.verify?.setup || "";
|
|
250
261
|
const testCmd = parsed.test_cmd || parsed.verify?.test || "";
|
|
262
|
+
const teardownCmd = parsed.teardown_cmd || parsed.verify?.teardown || "";
|
|
251
263
|
const buildCmd = parsed.build_cmd || parsed.verify?.build || "";
|
|
264
|
+
const verifyTimeoutMs = parsed.verify?.timeoutMs ?? parsed.verify?.timeout_ms ?? 60000;
|
|
252
265
|
const autoVerify = resolveVerify(root);
|
|
253
266
|
|
|
254
267
|
const activeTier = String(process.env.JULES_TIER || parsed.tier || "ultra").toLowerCase();
|
|
@@ -275,8 +288,11 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
275
288
|
provider: parsed.provider || DEFAULTS.provider,
|
|
276
289
|
tier: activeTier,
|
|
277
290
|
verify: {
|
|
291
|
+
setup: setupCmd || autoVerify.setup || "",
|
|
278
292
|
test: testCmd || autoVerify.test,
|
|
293
|
+
teardown: teardownCmd || autoVerify.teardown || "",
|
|
279
294
|
build: buildCmd || autoVerify.build,
|
|
295
|
+
timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 60000,
|
|
280
296
|
},
|
|
281
297
|
scope: normalizeScope(parsed),
|
|
282
298
|
limits: {
|
package/src/dag-engine.mjs
CHANGED
|
@@ -330,3 +330,79 @@ export class DagExecutor {
|
|
|
330
330
|
});
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Global Contract Trigger List:
|
|
336
|
+
* Files matching these patterns are considered global structural contracts.
|
|
337
|
+
* If ANY modified file matches these patterns, selective test execution MUST yield to full test suite execution (returns null).
|
|
338
|
+
*/
|
|
339
|
+
export const GLOBAL_CONTRACT_PATTERNS = [
|
|
340
|
+
/^package(-lock)?\.json$/,
|
|
341
|
+
/^pnpm-lock\.yaml$/,
|
|
342
|
+
/^yarn\.lock$/,
|
|
343
|
+
/^tsconfig.*\.json$/,
|
|
344
|
+
/^schema\./,
|
|
345
|
+
/\.d\.ts$/,
|
|
346
|
+
/^\.env/,
|
|
347
|
+
/^\.agent\//,
|
|
348
|
+
/^Cargo\.(toml|lock)$/,
|
|
349
|
+
/^go\.(mod|sum)$/,
|
|
350
|
+
/^composer\.(json|lock)$/,
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Checks if a relative file path matches global contract triggers.
|
|
355
|
+
*/
|
|
356
|
+
export function isGlobalContractFile(filePath = "") {
|
|
357
|
+
if (typeof filePath !== "string") return false;
|
|
358
|
+
const normalized = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
359
|
+
const baseName = normalized.split("/").pop();
|
|
360
|
+
return GLOBAL_CONTRACT_PATTERNS.some((pattern) => pattern.test(normalized) || pattern.test(baseName));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Resolves affected test files based on modified files and static import analysis.
|
|
365
|
+
* Returns null if full test suite should be executed (due to global contract change or unknown dependency),
|
|
366
|
+
* or an array of affected test file paths.
|
|
367
|
+
*/
|
|
368
|
+
export function resolveAffectedTests(modifiedFiles = [], options = {}) {
|
|
369
|
+
if (!Array.isArray(modifiedFiles) || modifiedFiles.length === 0) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 1. Check Global Contract Trigger List
|
|
374
|
+
for (const file of modifiedFiles) {
|
|
375
|
+
if (isGlobalContractFile(file)) {
|
|
376
|
+
return null; // Force full test suite execution
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const knownTestFiles = Array.isArray(options.knownTestFiles) ? options.knownTestFiles : [];
|
|
381
|
+
const testSuffixRegex = options.testSuffixRegex || /\.(test|spec)\.(m?[jt]sx?|py|rs|go)$/i;
|
|
382
|
+
|
|
383
|
+
const affected = new Set();
|
|
384
|
+
|
|
385
|
+
for (const file of modifiedFiles) {
|
|
386
|
+
if (typeof file !== "string") continue;
|
|
387
|
+
const normalized = file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
388
|
+
|
|
389
|
+
// If the modified file itself is a test file, add it directly
|
|
390
|
+
if (testSuffixRegex.test(normalized)) {
|
|
391
|
+
affected.add(normalized);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Match known test files that import or reference this file/module
|
|
396
|
+
const baseNameNoExt = normalized.split("/").pop().replace(/\.[^/.]+$/, "");
|
|
397
|
+
for (const testFile of knownTestFiles) {
|
|
398
|
+
if (typeof testFile !== "string") continue;
|
|
399
|
+
const normTest = testFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
400
|
+
if (normTest.includes(baseNameNoExt)) {
|
|
401
|
+
affected.add(normTest);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return affected.size > 0 ? Array.from(affected) : null;
|
|
407
|
+
}
|
|
408
|
+
|
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.
|
|
@@ -128,8 +143,11 @@ export async function gate(opts = {}) {
|
|
|
128
143
|
trustedScope = normalizeScope(parsed);
|
|
129
144
|
if (parsed.verify || parsed.test_cmd || parsed.build_cmd) {
|
|
130
145
|
trustedVerify = {
|
|
146
|
+
setup: parsed.verify?.setup || config.verify.setup,
|
|
131
147
|
test: parsed.verify?.test || parsed.test_cmd || config.verify.test,
|
|
148
|
+
teardown: parsed.verify?.teardown || config.verify.teardown,
|
|
132
149
|
build: parsed.verify?.build || parsed.build_cmd || config.verify.build,
|
|
150
|
+
timeoutMs: parsed.verify?.timeoutMs || parsed.verify?.timeout_ms || config.verify.timeoutMs,
|
|
133
151
|
};
|
|
134
152
|
}
|
|
135
153
|
} catch (_) {}
|
|
@@ -201,34 +219,79 @@ export async function gate(opts = {}) {
|
|
|
201
219
|
let flakyVerdictResult = null;
|
|
202
220
|
|
|
203
221
|
if (testCmd) {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 (_) {}
|
|
220
272
|
}
|
|
221
273
|
}
|
|
222
274
|
}
|
|
223
275
|
|
|
224
276
|
if (testResult.ok && buildCmd) {
|
|
225
277
|
const res = runCmd(buildCmd, { cwd: root, ignoreError: true, env: testEnv });
|
|
226
|
-
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
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
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);
|
|
227
290
|
}
|
|
228
291
|
|
|
229
|
-
const verifyOk = testResult.ok && buildResult.ok;
|
|
230
|
-
const failingCmd = !testResult.ok ? testResult : !buildResult.ok ? buildResult : null;
|
|
231
|
-
phases.push({ phase: "verify", ok: verifyOk, testResult, buildResult });
|
|
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 });
|
|
232
295
|
appendTelemetry(root, "gate_phase", { phase: "verify", ok: verifyOk });
|
|
233
296
|
if (progressBus && progressToken) {
|
|
234
297
|
progressBus.reportProgress(progressToken, 100, 100, "Phase 4/4: Automated Test & Build Verification complete");
|
|
@@ -342,7 +405,7 @@ export async function repair(failure, opts = {}) {
|
|
|
342
405
|
);
|
|
343
406
|
} catch (err) {
|
|
344
407
|
if (err instanceof ProviderRateLimitError || err instanceof ProviderUnavailableError) {
|
|
345
|
-
if (err.reservationId) {
|
|
408
|
+
if (err.reservationId && !err.budgetReservationRolledBack) {
|
|
346
409
|
rollbackBudgetReservation(root, err.reservationId);
|
|
347
410
|
}
|
|
348
411
|
const retryAfterMs = err.retryAfterMs || 60000;
|
|
@@ -473,7 +536,10 @@ Stderr:
|
|
|
473
536
|
${cleanStderr}`;
|
|
474
537
|
}
|
|
475
538
|
|
|
476
|
-
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
|
+
}
|
|
477
543
|
const root = opts.config?._root || opts.root || process.cwd();
|
|
478
544
|
const config = opts.config || loadConfig(root);
|
|
479
545
|
const provider = createProvider(config.provider, config);
|
|
@@ -503,7 +569,7 @@ export async function dispatch(task, opts = {}) {
|
|
|
503
569
|
);
|
|
504
570
|
} catch (err) {
|
|
505
571
|
if (err instanceof ProviderRateLimitError || err instanceof ProviderUnavailableError) {
|
|
506
|
-
if (err.reservationId) {
|
|
572
|
+
if (err.reservationId && !err.budgetReservationRolledBack) {
|
|
507
573
|
rollbackBudgetReservation(root, err.reservationId);
|
|
508
574
|
}
|
|
509
575
|
const retryAfterMs = err.retryAfterMs || 60000;
|
|
@@ -534,7 +600,8 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
534
600
|
|
|
535
601
|
const root = options.root || process.cwd();
|
|
536
602
|
const config = options.config || loadConfig(root);
|
|
537
|
-
const
|
|
603
|
+
const requestedConcurrency = Number(options.concurrency ?? config.limits.concurrency ?? 1);
|
|
604
|
+
const concurrency = Number.isInteger(requestedConcurrency) && requestedConcurrency > 0 ? requestedConcurrency : 1;
|
|
538
605
|
const isDry = options.dryRun !== undefined ? options.dryRun : (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
|
|
539
606
|
const queueDir = getQueueDir(root);
|
|
540
607
|
const completedDir = join(queueDir, "completed");
|
|
@@ -553,9 +620,12 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
553
620
|
const batch = filesToProcess.slice(i, i + concurrency);
|
|
554
621
|
await Promise.all(
|
|
555
622
|
batch.map(async (fileOrItem) => {
|
|
556
|
-
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem
|
|
557
|
-
const srcPath = join(queueDir, fileName);
|
|
623
|
+
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem?.id || fileOrItem?.file || fileOrItem?.title || "task";
|
|
558
624
|
try {
|
|
625
|
+
if (!isSafeQueueFileName(fileName)) {
|
|
626
|
+
throw new Error(`Unsafe queue task path: ${fileName}`);
|
|
627
|
+
}
|
|
628
|
+
const srcPath = join(queueDir, fileName);
|
|
559
629
|
let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
|
|
560
630
|
let title = typeof fileOrItem === "object" && fileOrItem.title ? fileOrItem.title : fileName;
|
|
561
631
|
if (!prompt && existsSync(srcPath)) {
|
|
@@ -569,6 +639,9 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
569
639
|
} else {
|
|
570
640
|
const dstPath = join(completedDir, fileName);
|
|
571
641
|
if (existsSync(srcPath)) {
|
|
642
|
+
if (existsSync(dstPath)) {
|
|
643
|
+
throw new Error(`Completed task destination already exists: ${fileName}`);
|
|
644
|
+
}
|
|
572
645
|
renameSync(srcPath, dstPath);
|
|
573
646
|
}
|
|
574
647
|
appendLedger({ event: "task_completed", file: fileName, session }, root);
|
|
@@ -583,3 +656,195 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
583
656
|
|
|
584
657
|
return { processed: results.length, results };
|
|
585
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
|
@@ -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,
|