jules-orchestrator-kit 0.29.1 → 0.32.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 +267 -4
- package/bin/mcp-server.mjs +0 -0
- package/index.mjs +21 -1
- package/package.json +1 -1
- package/scripts/stale-base-check.mjs +2 -2
- package/src/config.mjs +18 -2
- package/src/dag-engine.mjs +76 -0
- package/src/engine.mjs +331 -32
- package/src/envelope.mjs +32 -2
- package/src/execution_envelope.mjs +9 -1
- package/src/git.mjs +41 -0
- package/src/mcp.mjs +34 -0
- package/src/memory.mjs +148 -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/prompt-guard.mjs +8 -1
- package/src/provider.mjs +164 -8
- package/src/remediation.mjs +164 -0
- package/src/security.mjs +14 -6
- package/src/stack-detector.mjs +15 -5
- package/src/state.mjs +71 -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/src/engine.mjs
CHANGED
|
@@ -2,15 +2,32 @@ import { loadConfig, parseYaml, normalizeScope } from "./config.mjs";
|
|
|
2
2
|
import { checkScope, scanDiff, redactSecrets } from "./security.mjs";
|
|
3
3
|
import { changedFiles, diffBytes, diffText, showFromOrigin, runCmd } from "./git.mjs";
|
|
4
4
|
import { createProvider, ProviderRateLimitError, ProviderUnavailableError } from "./provider.mjs";
|
|
5
|
-
import { withBudget, appendLedger, getQueueDir, ensureDir, rollbackBudgetReservation } from "./state.mjs";
|
|
5
|
+
import { withBudget, appendLedger, getQueueDir, ensureDir, rollbackBudgetReservation, isConcurrencyGroupLocked } from "./state.mjs";
|
|
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
|
+
import { recordRemediation, queryRemediations, harvestFailureRecord, hydrateMemory } from "./remediation.mjs";
|
|
16
|
+
|
|
17
|
+
export { recordVerifyRun, readVerifyRuns, flakyVerdict, sanitizeUntrustedData, resolveAffectedTests, recordRemediation, queryRemediations, harvestFailureRecord, hydrateMemory, isConcurrencyGroupLocked };
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
function appendTelemetry(root, kind, fields = {}) {
|
|
21
|
+
try {
|
|
22
|
+
return appendTelemetryUnsafe(root, kind, fields);
|
|
23
|
+
} catch (_) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isSafeQueueFileName(fileName) {
|
|
29
|
+
return typeof fileName === "string" && fileName.length > 0 && basename(fileName) === fileName && fileName !== "." && fileName !== "..";
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
33
|
* Validates whether a file in .agent/jules-queue/ is a task file.
|
|
@@ -128,8 +145,11 @@ export async function gate(opts = {}) {
|
|
|
128
145
|
trustedScope = normalizeScope(parsed);
|
|
129
146
|
if (parsed.verify || parsed.test_cmd || parsed.build_cmd) {
|
|
130
147
|
trustedVerify = {
|
|
148
|
+
setup: parsed.verify?.setup || config.verify.setup,
|
|
131
149
|
test: parsed.verify?.test || parsed.test_cmd || config.verify.test,
|
|
150
|
+
teardown: parsed.verify?.teardown || config.verify.teardown,
|
|
132
151
|
build: parsed.verify?.build || parsed.build_cmd || config.verify.build,
|
|
152
|
+
timeoutMs: parsed.verify?.timeoutMs || parsed.verify?.timeout_ms || config.verify.timeoutMs,
|
|
133
153
|
};
|
|
134
154
|
}
|
|
135
155
|
} catch (_) {}
|
|
@@ -201,34 +221,79 @@ export async function gate(opts = {}) {
|
|
|
201
221
|
let flakyVerdictResult = null;
|
|
202
222
|
|
|
203
223
|
if (testCmd) {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
const setupCmd = trustedVerify.setup;
|
|
225
|
+
const teardownCmd = trustedVerify.teardown;
|
|
226
|
+
const verifyTimeout = trustedVerify.timeoutMs;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
if (setupCmd) {
|
|
230
|
+
const setupRes = runCmd(setupCmd, { cwd: root, ignoreError: true, env: testEnv, timeout: verifyTimeout });
|
|
231
|
+
if (setupRes.status !== 0) {
|
|
232
|
+
testResult = {
|
|
233
|
+
ok: false,
|
|
234
|
+
status: setupRes.status,
|
|
235
|
+
stdout: redactSecrets(setupRes.stdout || ""),
|
|
236
|
+
stderr: redactSecrets(setupRes.stderr || ""),
|
|
237
|
+
command: setupCmd,
|
|
238
|
+
phase: "setup",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (testResult.ok) {
|
|
244
|
+
const startTime = Date.now();
|
|
245
|
+
const res = runCmd(testCmd, { cwd: root, ignoreError: true, env: testEnv, timeout: verifyTimeout });
|
|
246
|
+
const durationMs = Date.now() - startTime;
|
|
247
|
+
testResult = {
|
|
248
|
+
ok: res.status === 0,
|
|
249
|
+
status: res.status,
|
|
250
|
+
stdout: redactSecrets(res.stdout || ""),
|
|
251
|
+
stderr: redactSecrets(res.stderr || ""),
|
|
252
|
+
command: testCmd,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const fingerprint = !testResult.ok ? fingerprintFailureState(testResult, root) : null;
|
|
256
|
+
recordVerifyRun(root, testCmd, testResult.ok, fingerprint, durationMs);
|
|
257
|
+
|
|
258
|
+
if (!testResult.ok) {
|
|
259
|
+
const runs = readVerifyRuns(root, testCmd);
|
|
260
|
+
flakyVerdictResult = flakyVerdict(runs);
|
|
261
|
+
if (flakyVerdictResult.verdict === "QUARANTINED") {
|
|
262
|
+
phases.push({ phase: "verify", ok: false, testResult, buildResult, flakyVerdict: flakyVerdictResult });
|
|
263
|
+
appendTelemetry(root, "gate_phase", { phase: "verify", ok: false, quarantined: true });
|
|
264
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 8 });
|
|
265
|
+
return { ok: false, code: 8, phases, flakyVerdict: flakyVerdictResult };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
if (teardownCmd) {
|
|
271
|
+
try {
|
|
272
|
+
runCmd(teardownCmd, { cwd: root, ignoreError: true, env: testEnv });
|
|
273
|
+
} catch (_) {}
|
|
220
274
|
}
|
|
221
275
|
}
|
|
222
276
|
}
|
|
223
277
|
|
|
224
278
|
if (testResult.ok && buildCmd) {
|
|
225
279
|
const res = runCmd(buildCmd, { cwd: root, ignoreError: true, env: testEnv });
|
|
226
|
-
buildResult = {
|
|
280
|
+
buildResult = {
|
|
281
|
+
ok: res.status === 0,
|
|
282
|
+
status: res.status,
|
|
283
|
+
stdout: redactSecrets(res.stdout || ""),
|
|
284
|
+
stderr: redactSecrets(res.stderr || ""),
|
|
285
|
+
command: buildCmd,
|
|
286
|
+
};
|
|
227
287
|
}
|
|
228
288
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
289
|
+
let serverResult = { ok: true, status: 0 };
|
|
290
|
+
if (testResult.ok && buildResult.ok && trustedVerify.server && trustedVerify.server.command) {
|
|
291
|
+
serverResult = await probeDevServer(trustedVerify.server, root);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const verifyOk = testResult.ok && buildResult.ok && serverResult.ok;
|
|
295
|
+
const failingCmd = !testResult.ok ? testResult : !buildResult.ok ? buildResult : !serverResult.ok ? serverResult : null;
|
|
296
|
+
phases.push({ phase: "verify", ok: verifyOk, testResult, buildResult, serverResult });
|
|
232
297
|
appendTelemetry(root, "gate_phase", { phase: "verify", ok: verifyOk });
|
|
233
298
|
if (progressBus && progressToken) {
|
|
234
299
|
progressBus.reportProgress(progressToken, 100, 100, "Phase 4/4: Automated Test & Build Verification complete");
|
|
@@ -342,7 +407,7 @@ export async function repair(failure, opts = {}) {
|
|
|
342
407
|
);
|
|
343
408
|
} catch (err) {
|
|
344
409
|
if (err instanceof ProviderRateLimitError || err instanceof ProviderUnavailableError) {
|
|
345
|
-
if (err.reservationId) {
|
|
410
|
+
if (err.reservationId && !err.budgetReservationRolledBack) {
|
|
346
411
|
rollbackBudgetReservation(root, err.reservationId);
|
|
347
412
|
}
|
|
348
413
|
const retryAfterMs = err.retryAfterMs || 60000;
|
|
@@ -381,6 +446,12 @@ export async function repair(failure, opts = {}) {
|
|
|
381
446
|
const gateRes = await gate({ root, config, fix: false, progressBus, progressToken });
|
|
382
447
|
if (gateRes.ok) {
|
|
383
448
|
breaker.reset();
|
|
449
|
+
recordRemediation(root, {
|
|
450
|
+
fingerprint: initialFingerprint,
|
|
451
|
+
symptom: currentFailure.stderr || currentFailure.message || "Failure resolved by OODA repair",
|
|
452
|
+
remediationHint: `Resolved on repair attempt #${n}`,
|
|
453
|
+
targetFiles: currentFailure.targetFiles || [],
|
|
454
|
+
});
|
|
384
455
|
return { ok: true, attempts, finalStatus: "PASSED" };
|
|
385
456
|
}
|
|
386
457
|
|
|
@@ -389,6 +460,12 @@ export async function repair(failure, opts = {}) {
|
|
|
389
460
|
|
|
390
461
|
const check = breaker.observe(currentFingerprint);
|
|
391
462
|
if (check.tripped) {
|
|
463
|
+
harvestFailureRecord(root, {
|
|
464
|
+
fingerprint: currentFingerprint,
|
|
465
|
+
symptom: currentFailure.stderr || currentFailure.message || "Deterministic Regression",
|
|
466
|
+
remediationHint: `Identical failure state fingerprint (${currentFingerprint}) observed during attempt #${n}`,
|
|
467
|
+
targetFiles: currentFailure.targetFiles || [],
|
|
468
|
+
});
|
|
392
469
|
return {
|
|
393
470
|
ok: false,
|
|
394
471
|
attempts,
|
|
@@ -399,7 +476,15 @@ export async function repair(failure, opts = {}) {
|
|
|
399
476
|
}
|
|
400
477
|
}
|
|
401
478
|
|
|
479
|
+
harvestFailureRecord(root, {
|
|
480
|
+
fingerprint: initialFingerprint,
|
|
481
|
+
symptom: currentFailure.stderr || currentFailure.message || "OODA Repair Exhausted",
|
|
482
|
+
remediationHint: "OODA repair attempts exhausted without clean verification pass.",
|
|
483
|
+
targetFiles: currentFailure.targetFiles || [],
|
|
484
|
+
});
|
|
485
|
+
|
|
402
486
|
return { ok: false, attempts, finalStatus: "OODA_EXHAUSTED" };
|
|
487
|
+
|
|
403
488
|
}
|
|
404
489
|
|
|
405
490
|
/**
|
|
@@ -473,7 +558,10 @@ Stderr:
|
|
|
473
558
|
${cleanStderr}`;
|
|
474
559
|
}
|
|
475
560
|
|
|
476
|
-
export async function dispatch(task, opts = {}) {
|
|
561
|
+
export async function dispatch(task = {}, opts = {}) {
|
|
562
|
+
if (!task || typeof task !== "object") {
|
|
563
|
+
throw new TypeError("Dispatch task must be an object");
|
|
564
|
+
}
|
|
477
565
|
const root = opts.config?._root || opts.root || process.cwd();
|
|
478
566
|
const config = opts.config || loadConfig(root);
|
|
479
567
|
const provider = createProvider(config.provider, config);
|
|
@@ -487,11 +575,15 @@ export async function dispatch(task, opts = {}) {
|
|
|
487
575
|
// Redact secrets in prompt before dispatching
|
|
488
576
|
const cleanPrompt = redactSecrets(task.prompt || "");
|
|
489
577
|
|
|
490
|
-
// Wire Prompt Guard & Envelope to wrap raw task arguments
|
|
578
|
+
// Wire Prompt Guard, Memory Hydration & Envelope to wrap raw task arguments
|
|
491
579
|
const taskInstructions = task.taskInstructions || cleanPrompt || task.title || "Autonomous Task Execution";
|
|
492
580
|
const untrustedData = Array.isArray(task.untrustedData) ? task.untrustedData : [];
|
|
493
581
|
const systemPolicy = task.systemPolicy || config.systemPolicy || "";
|
|
494
|
-
|
|
582
|
+
|
|
583
|
+
const targetFiles = Array.isArray(task.targetFiles) ? task.targetFiles : (Array.isArray(task.referenced_paths) ? task.referenced_paths : []);
|
|
584
|
+
const learnedRemediations = task.learnedRemediations || hydrateMemory(root, { targetFiles, fingerprint: task.fingerprint || "" });
|
|
585
|
+
|
|
586
|
+
const envelopedPrompt = buildAgentEnvelope(systemPolicy, taskInstructions, untrustedData, { learnedRemediations });
|
|
495
587
|
|
|
496
588
|
const cleanTask = { ...task, prompt: envelopedPrompt };
|
|
497
589
|
|
|
@@ -503,7 +595,7 @@ export async function dispatch(task, opts = {}) {
|
|
|
503
595
|
);
|
|
504
596
|
} catch (err) {
|
|
505
597
|
if (err instanceof ProviderRateLimitError || err instanceof ProviderUnavailableError) {
|
|
506
|
-
if (err.reservationId) {
|
|
598
|
+
if (err.reservationId && !err.budgetReservationRolledBack) {
|
|
507
599
|
rollbackBudgetReservation(root, err.reservationId);
|
|
508
600
|
}
|
|
509
601
|
const retryAfterMs = err.retryAfterMs || 60000;
|
|
@@ -534,7 +626,8 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
534
626
|
|
|
535
627
|
const root = options.root || process.cwd();
|
|
536
628
|
const config = options.config || loadConfig(root);
|
|
537
|
-
const
|
|
629
|
+
const requestedConcurrency = Number(options.concurrency ?? config.limits.concurrency ?? 1);
|
|
630
|
+
const concurrency = Number.isInteger(requestedConcurrency) && requestedConcurrency > 0 ? requestedConcurrency : 1;
|
|
538
631
|
const isDry = options.dryRun !== undefined ? options.dryRun : (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
|
|
539
632
|
const queueDir = getQueueDir(root);
|
|
540
633
|
const completedDir = join(queueDir, "completed");
|
|
@@ -553,15 +646,25 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
553
646
|
const batch = filesToProcess.slice(i, i + concurrency);
|
|
554
647
|
await Promise.all(
|
|
555
648
|
batch.map(async (fileOrItem) => {
|
|
556
|
-
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem
|
|
557
|
-
const srcPath = join(queueDir, fileName);
|
|
649
|
+
const fileName = typeof fileOrItem === "string" ? fileOrItem : fileOrItem?.id || fileOrItem?.file || fileOrItem?.title || "task";
|
|
558
650
|
try {
|
|
651
|
+
if (!isSafeQueueFileName(fileName)) {
|
|
652
|
+
throw new Error(`Unsafe queue task path: ${fileName}`);
|
|
653
|
+
}
|
|
654
|
+
const srcPath = join(queueDir, fileName);
|
|
559
655
|
let prompt = typeof fileOrItem === "object" && fileOrItem.prompt ? fileOrItem.prompt : "";
|
|
560
656
|
let title = typeof fileOrItem === "object" && fileOrItem.title ? fileOrItem.title : fileName;
|
|
561
657
|
if (!prompt && existsSync(srcPath)) {
|
|
562
658
|
prompt = await fs.promises.readFile(srcPath, "utf-8");
|
|
563
659
|
}
|
|
564
660
|
const task = typeof fileOrItem === "object" ? { ...fileOrItem, title, prompt } : { title, prompt };
|
|
661
|
+
|
|
662
|
+
const groupName = task.concurrency_group || task.concurrencyGroup || "";
|
|
663
|
+
if (groupName && isConcurrencyGroupLocked(groupName, root, task.id || fileName)) {
|
|
664
|
+
results.push({ file: fileName, ok: false, status: "SKIPPED_CONCURRENCY_GROUP_LOCKED", reason: `Concurrency group '${groupName}' is active` });
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
565
668
|
const session = await dispatch(task, { root, config, dryRun: isDry });
|
|
566
669
|
|
|
567
670
|
if (session && session.ok === false) {
|
|
@@ -569,6 +672,9 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
569
672
|
} else {
|
|
570
673
|
const dstPath = join(completedDir, fileName);
|
|
571
674
|
if (existsSync(srcPath)) {
|
|
675
|
+
if (existsSync(dstPath)) {
|
|
676
|
+
throw new Error(`Completed task destination already exists: ${fileName}`);
|
|
677
|
+
}
|
|
572
678
|
renameSync(srcPath, dstPath);
|
|
573
679
|
}
|
|
574
680
|
appendLedger({ event: "task_completed", file: fileName, session }, root);
|
|
@@ -581,5 +687,198 @@ export async function run(tasksOrOpts = {}, opts = {}) {
|
|
|
581
687
|
);
|
|
582
688
|
}
|
|
583
689
|
|
|
690
|
+
|
|
584
691
|
return { processed: results.length, results };
|
|
585
692
|
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Synthesizes a structured, evidence-backed PR description body for GitHub PR creation.
|
|
696
|
+
* @param {object} session - Session or dispatch result
|
|
697
|
+
* @param {object} gateResult - Gate evaluation result
|
|
698
|
+
* @param {object} [options]
|
|
699
|
+
* @returns {string} Markdown PR description body
|
|
700
|
+
*/
|
|
701
|
+
export function synthesizePrDescription(session = {}, gateResult = {}, options = {}) {
|
|
702
|
+
const sessionId = session.id || session.name || "jules-session";
|
|
703
|
+
const attemptsCount = session.attempts?.length || session._warmAttempts || 1;
|
|
704
|
+
const maxAttempts = options.maxAttempts || 3;
|
|
705
|
+
const durationMs = options.durationMs || session.durationMs || 0;
|
|
706
|
+
const durationSec = (durationMs / 1000).toFixed(1);
|
|
707
|
+
const isWarm = Boolean(session.resumed || session._warmResumed);
|
|
708
|
+
|
|
709
|
+
const phases = gateResult.phases || [];
|
|
710
|
+
const scopePhase = phases.find((p) => p.phase === "scope") || { ok: true, violations: [] };
|
|
711
|
+
const payloadPhase = phases.find((p) => p.phase === "payload") || { ok: true, bytes: 0, limitBytes: 76800 };
|
|
712
|
+
const secretPhase = phases.find((p) => p.phase === "secrets") || { ok: true, findings: [] };
|
|
713
|
+
|
|
714
|
+
const kbDiff = ((payloadPhase.bytes || 0) / 1024).toFixed(1);
|
|
715
|
+
const kbLimit = ((payloadPhase.limitBytes || 76800) / 1024).toFixed(0);
|
|
716
|
+
|
|
717
|
+
const modifiedFiles = options.modifiedFiles || [];
|
|
718
|
+
const affectedTests = resolveAffectedTests(modifiedFiles, options);
|
|
719
|
+
|
|
720
|
+
const prBody = `## 🚀 Autonomous Jules Agent Execution Evidence
|
|
721
|
+
|
|
722
|
+
### ⏱️ OODA Verification Trace
|
|
723
|
+
- **Session ID:** \`${sessionId}\`
|
|
724
|
+
- **OODA Turns:** \`${attemptsCount}/${maxAttempts}\`
|
|
725
|
+
- **Warm Resumption:** ${isWarm ? "✅ Active Context Stream" : "Cold Start"}
|
|
726
|
+
- **Execution Latency:** \`${durationSec}s\`
|
|
727
|
+
|
|
728
|
+
### 🛡️ Zero-Trust Security Audit Matrix
|
|
729
|
+
- **Scope Guard:** ${scopePhase.ok ? "✅ PASS (0 protected path violations)" : "❌ FAIL"}
|
|
730
|
+
- **Diff Payload Governor:** ${payloadPhase.ok ? `✅ PASS (${kbDiff} KB / ${kbLimit} KB limit)` : "❌ EXCEEDED"}
|
|
731
|
+
- **Secret Scanner:** ${secretPhase.ok ? "✅ PASS (0 high-entropy leaks detected)" : "❌ LEAKS FOUND"}
|
|
732
|
+
|
|
733
|
+
### 🧪 Terminal Verification Evidence
|
|
734
|
+
\`\`\`text
|
|
735
|
+
${options.testOutput || "All verification tests passed with Exit Code 0."}
|
|
736
|
+
\`\`\`
|
|
737
|
+
|
|
738
|
+
### 📂 AST Impact & Dependency Graph
|
|
739
|
+
${
|
|
740
|
+
affectedTests === null
|
|
741
|
+
? "- **Impact Scope:** ⚠️ Global contract change detected (`package.json`/`tsconfig`/`schema`). Full test suite executed."
|
|
742
|
+
: affectedTests.length > 0
|
|
743
|
+
? `- **Affected Test Suites (${affectedTests.length}):**\n` + affectedTests.map((t) => ` - \`${t}\``).join("\n")
|
|
744
|
+
: "- **Impact Scope:** Isolated leaf implementation change."
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
*Generated automatically by [jules-orchestrator-kit](https://github.com/FullThrottle83/jules-orchestrator-kit)*
|
|
749
|
+
`;
|
|
750
|
+
|
|
751
|
+
return prBody;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Live Dev Server & SSR Hydration Smoke Prober.
|
|
756
|
+
* Spawns the server in a detached process group, polls the URL via globalThis.fetch,
|
|
757
|
+
* intercepts SSR hydration errors, and cleanly kills the process group in a finally block.
|
|
758
|
+
* @param {object} serverConfig
|
|
759
|
+
* @param {string} serverConfig.command
|
|
760
|
+
* @param {string} [serverConfig.url="http://localhost:3000"]
|
|
761
|
+
* @param {number} [serverConfig.timeoutMs=15000]
|
|
762
|
+
* @param {string} root
|
|
763
|
+
* @returns {Promise<object>} Probe result { ok: boolean, status: number, error: string, logs: string }
|
|
764
|
+
*/
|
|
765
|
+
export async function probeDevServer(serverConfig = {}, root = process.cwd()) {
|
|
766
|
+
if (!serverConfig || !serverConfig.command) {
|
|
767
|
+
return { ok: true, skipped: true, reason: "No server command configured" };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const cmd = serverConfig.command;
|
|
771
|
+
const url = serverConfig.url || "http://localhost:3000";
|
|
772
|
+
const timeoutMs = Number(serverConfig.timeoutMs || 15000);
|
|
773
|
+
|
|
774
|
+
const isWin = process.platform === "win32";
|
|
775
|
+
const shellBin = isWin ? (process.env.ComSpec || "cmd.exe") : "/bin/sh";
|
|
776
|
+
const shellArgs = isWin ? ["/d", "/s", "/c", cmd] : ["-c", cmd];
|
|
777
|
+
|
|
778
|
+
let child;
|
|
779
|
+
let stdoutLogs = "";
|
|
780
|
+
let stderrLogs = "";
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
child = spawn(shellBin, shellArgs, {
|
|
784
|
+
cwd: root,
|
|
785
|
+
detached: true,
|
|
786
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
787
|
+
env: process.env,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
if (child.stdout) {
|
|
791
|
+
child.stdout.on("data", (chunk) => {
|
|
792
|
+
stdoutLogs += chunk.toString("utf-8");
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
if (child.stderr) {
|
|
796
|
+
child.stderr.on("data", (chunk) => {
|
|
797
|
+
stderrLogs += chunk.toString("utf-8");
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const startTime = Date.now();
|
|
802
|
+
let pollResponse = null;
|
|
803
|
+
let pollError = null;
|
|
804
|
+
|
|
805
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
806
|
+
if (child.exitCode !== null) {
|
|
807
|
+
return {
|
|
808
|
+
ok: false,
|
|
809
|
+
status: child.exitCode,
|
|
810
|
+
error: `Dev server process exited prematurely with code ${child.exitCode}`,
|
|
811
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
817
|
+
const bodyText = await res.text();
|
|
818
|
+
pollResponse = { status: res.status, bodyText, ok: res.ok };
|
|
819
|
+
break;
|
|
820
|
+
} catch (err) {
|
|
821
|
+
pollError = err;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!pollResponse) {
|
|
828
|
+
return {
|
|
829
|
+
ok: false,
|
|
830
|
+
status: 504,
|
|
831
|
+
error: `Dev server probe timed out after ${timeoutMs}ms attempting to reach ${url}. ${pollError ? pollError.message : ""}`,
|
|
832
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const combinedLogs = (stderrLogs + "\n" + pollResponse.bodyText).toLowerCase();
|
|
837
|
+
const hydrationPanics = [
|
|
838
|
+
"hydration failed",
|
|
839
|
+
"text content did not match",
|
|
840
|
+
"unhandled runtime error",
|
|
841
|
+
"minified react error #418",
|
|
842
|
+
"minified react error #423",
|
|
843
|
+
"minified react error #425",
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
const detectedPanic = hydrationPanics.find((panic) => combinedLogs.includes(panic));
|
|
847
|
+
if (detectedPanic) {
|
|
848
|
+
return {
|
|
849
|
+
ok: false,
|
|
850
|
+
status: 500,
|
|
851
|
+
error: `SSR Hydration Smoke Probe Failure: Detected '${detectedPanic}' error in server response or stderr.`,
|
|
852
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!pollResponse.ok) {
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
status: pollResponse.status,
|
|
860
|
+
error: `Dev server responded with HTTP status ${pollResponse.status}`,
|
|
861
|
+
logs: (stderrLogs || stdoutLogs).slice(-2000),
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
ok: true,
|
|
867
|
+
status: pollResponse.status,
|
|
868
|
+
url,
|
|
869
|
+
logs: stdoutLogs.slice(-500),
|
|
870
|
+
};
|
|
871
|
+
} finally {
|
|
872
|
+
if (child && child.pid) {
|
|
873
|
+
try {
|
|
874
|
+
if (isWin) {
|
|
875
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
876
|
+
} else {
|
|
877
|
+
process.kill(-child.pid, "SIGTERM");
|
|
878
|
+
}
|
|
879
|
+
} catch (_) {}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
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 });
|
|
@@ -96,10 +101,35 @@ export function validateEnvelope(envelope = {}, opts = {}) {
|
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
// 6. Concurrency Group Check
|
|
105
|
+
if (envelope.concurrency_group !== undefined) {
|
|
106
|
+
if (typeof envelope.concurrency_group !== "string" || !envelope.concurrency_group.trim()) {
|
|
107
|
+
errors.push("concurrency_group must be a non-empty string when provided.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
return {
|
|
112
|
+
|
|
100
113
|
ok: errors.length === 0,
|
|
101
114
|
code: errors.length === 0 ? 0 : 1,
|
|
102
115
|
errors,
|
|
103
116
|
warnings,
|
|
104
117
|
};
|
|
105
118
|
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse JSON metadata payload embedded in HTML comment JULES_TASK_ENVELOPE.
|
|
122
|
+
* @param {string} content
|
|
123
|
+
* @returns {Record<string, any> | null}
|
|
124
|
+
*/
|
|
125
|
+
export function parseEnvelopeHeader(content) {
|
|
126
|
+
if (!content) return null;
|
|
127
|
+
const match = String(content).match(/<!--\s*JULES_TASK_ENVELOPE:\s*({[\s\S]*?})\s*-->/);
|
|
128
|
+
if (!match) return null;
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(match[1]);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { resolveRoot } from "./config.mjs";
|
|
3
3
|
import { resolveBase } from "./git.mjs";
|
|
4
4
|
import { classifyRiskTier } from "./risk.mjs";
|
|
5
|
+
import { queryRemediations } from "./remediation.mjs";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Generates a SHA-256 hash of the execution envelope payload.
|
|
@@ -19,6 +20,7 @@ export function hashExecutionEnvelope(envelope) {
|
|
|
19
20
|
riskTier: envelope.riskTier,
|
|
20
21
|
budgetReservationId: envelope.budgetReservationId,
|
|
21
22
|
capabilities: envelope.capabilities,
|
|
23
|
+
remediations: envelope.remediations || [],
|
|
22
24
|
createdAt: envelope.createdAt,
|
|
23
25
|
};
|
|
24
26
|
const sortedKeys = Object.keys(rawPayload).sort();
|
|
@@ -45,11 +47,12 @@ export function freezeExecutionEnvelope(envelope) {
|
|
|
45
47
|
}),
|
|
46
48
|
verify: Object.freeze({ ...(envelope.verify || {}) }),
|
|
47
49
|
capabilities: Object.freeze([...(envelope.capabilities || [])]),
|
|
50
|
+
remediations: Object.freeze([...(envelope.remediations || [])]),
|
|
48
51
|
});
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/**
|
|
52
|
-
* Creates an immutable Capability-Bounded Execution Envelope (CBEE).
|
|
55
|
+
* Creates an immutable Capability-Bounded Execution Envelope (CBEE) with pre-flight remediation hydration.
|
|
53
56
|
*/
|
|
54
57
|
export function createExecutionEnvelope(task = {}, opts = {}) {
|
|
55
58
|
const root = opts.root || resolveRoot();
|
|
@@ -66,6 +69,9 @@ export function createExecutionEnvelope(task = {}, opts = {}) {
|
|
|
66
69
|
const files = task.files || opts.files || [];
|
|
67
70
|
const riskMeta = classifyRiskTier(files, { diffLines: opts.diffLines || 0 });
|
|
68
71
|
|
|
72
|
+
const taskFingerprint = task.fingerprint || opts.fingerprint || null;
|
|
73
|
+
const remediations = queryRemediations(root, { targetFiles: files, fingerprint: taskFingerprint, limit: 3 });
|
|
74
|
+
|
|
69
75
|
const rawEnvelope = {
|
|
70
76
|
id: envelopeId,
|
|
71
77
|
taskId,
|
|
@@ -84,6 +90,7 @@ export function createExecutionEnvelope(task = {}, opts = {}) {
|
|
|
84
90
|
riskTier: riskMeta.tier,
|
|
85
91
|
budgetReservationId: opts.reservationId || null,
|
|
86
92
|
capabilities: opts.capabilities || ["git:read", "git:diff", "test:run"],
|
|
93
|
+
remediations,
|
|
87
94
|
createdAt: new Date().toISOString(),
|
|
88
95
|
};
|
|
89
96
|
|
|
@@ -101,3 +108,4 @@ export function verifyExecutionEnvelope(envelope) {
|
|
|
101
108
|
const expectedHash = hashExecutionEnvelope(envelope);
|
|
102
109
|
return envelope.hash === expectedHash;
|
|
103
110
|
}
|
|
111
|
+
|
package/src/git.mjs
CHANGED
|
@@ -131,6 +131,30 @@ export function git(args = [], opts = {}) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export function ensureBaseFetched(root = process.cwd(), baseRef = "main") {
|
|
135
|
+
try {
|
|
136
|
+
execFileSync("git", ["fetch", "origin", baseRef, "--depth=100"], {
|
|
137
|
+
cwd: root,
|
|
138
|
+
encoding: "utf-8",
|
|
139
|
+
shell: false,
|
|
140
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
141
|
+
});
|
|
142
|
+
return true;
|
|
143
|
+
} catch (_) {
|
|
144
|
+
try {
|
|
145
|
+
execFileSync("git", ["fetch", "origin", baseRef, "--unshallow"], {
|
|
146
|
+
cwd: root,
|
|
147
|
+
encoding: "utf-8",
|
|
148
|
+
shell: false,
|
|
149
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
150
|
+
});
|
|
151
|
+
return true;
|
|
152
|
+
} catch (__) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
134
158
|
export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
135
159
|
const candidates = [`origin/${baseRef}`, `refs/remotes/origin/${baseRef}`, baseRef];
|
|
136
160
|
for (const ref of candidates) {
|
|
@@ -146,6 +170,23 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
|
146
170
|
}
|
|
147
171
|
} catch (_) {}
|
|
148
172
|
}
|
|
173
|
+
|
|
174
|
+
ensureBaseFetched(root, baseRef);
|
|
175
|
+
|
|
176
|
+
for (const ref of candidates) {
|
|
177
|
+
try {
|
|
178
|
+
const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
179
|
+
cwd: root,
|
|
180
|
+
encoding: "utf-8",
|
|
181
|
+
shell: false,
|
|
182
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
183
|
+
});
|
|
184
|
+
if (res && res.trim()) {
|
|
185
|
+
return res.trim();
|
|
186
|
+
}
|
|
187
|
+
} catch (_) {}
|
|
188
|
+
}
|
|
189
|
+
|
|
149
190
|
throw new GateError(
|
|
150
191
|
`Cannot resolve base reference "${baseRef}". Ensure base branch is fetched (e.g., fetch-depth: 0 in CI).`,
|
|
151
192
|
{ code: 1 }
|