jules-orchestrator-kit 0.32.4 → 0.32.6

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/src/engine.mjs CHANGED
@@ -2,6 +2,7 @@ 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 { resolveRoutedProvider } from "./router.mjs";
5
6
  import { withBudget, appendLedger, getQueueDir, ensureDir, rollbackBudgetReservation, isConcurrencyGroupLocked } from "./state.mjs";
6
7
  import { sanitizeUntrustedData, buildAgentEnvelope } from "./prompt-guard.mjs";
7
8
  import { recordVerifyRun, readVerifyRuns, flakyVerdict } from "./flaky-ledger.mjs";
@@ -11,10 +12,41 @@ import { createHash } from "node:crypto";
11
12
  import { appendTelemetry as appendTelemetryUnsafe } from "./telemetry.mjs";
12
13
 
13
14
  import { spawn } from "node:child_process";
14
- import { resolveAffectedTests } from "./dag-engine.mjs";
15
+ import { resolveAffectedTests, executeQueueDag } from "./dag-engine.mjs";
15
16
  import { recordRemediation, queryRemediations, harvestFailureRecord, hydrateMemory } from "./remediation.mjs";
16
-
17
- export { recordVerifyRun, readVerifyRuns, flakyVerdict, sanitizeUntrustedData, resolveAffectedTests, recordRemediation, queryRemediations, harvestFailureRecord, hydrateMemory, isConcurrencyGroupLocked };
17
+ import { hydratePrompt, harvestFailure } from "./memory.mjs";
18
+ import { resolveRolePrompt } from "./wizard-task.mjs";
19
+
20
+ import {
21
+ computeDirectoryHash,
22
+ generateEvidenceManifest,
23
+ writeEvidenceManifest,
24
+ verifyEvidenceManifest,
25
+ generateEvidenceMarkdown,
26
+ sha256,
27
+ } from "./evidence.mjs";
28
+
29
+ export {
30
+ recordVerifyRun,
31
+ readVerifyRuns,
32
+ flakyVerdict,
33
+ sanitizeUntrustedData,
34
+ resolveAffectedTests,
35
+ executeQueueDag,
36
+ recordRemediation,
37
+ queryRemediations,
38
+ harvestFailureRecord,
39
+ hydrateMemory,
40
+ hydratePrompt,
41
+ harvestFailure,
42
+ resolveRolePrompt,
43
+ isConcurrencyGroupLocked,
44
+ computeDirectoryHash,
45
+ generateEvidenceManifest,
46
+ writeEvidenceManifest,
47
+ verifyEvidenceManifest,
48
+ generateEvidenceMarkdown,
49
+ };
18
50
 
19
51
 
20
52
  function appendTelemetry(root, kind, fields = {}) {
@@ -146,9 +178,16 @@ export async function gate(opts = {}) {
146
178
  if (parsed.verify || parsed.test_cmd || parsed.build_cmd) {
147
179
  trustedVerify = {
148
180
  setup: parsed.verify?.setup || config.verify.setup,
181
+ lint: parsed.verify?.lint || parsed.lint_cmd || config.verify.lint,
149
182
  test: parsed.verify?.test || parsed.test_cmd || config.verify.test,
183
+ unit: parsed.verify?.unit || config.verify.unit || parsed.verify?.test || parsed.test_cmd || config.verify.test,
184
+ fuzz: parsed.verify?.fuzz || parsed.fuzz_cmd || config.verify.fuzz,
185
+ invariant: parsed.verify?.invariant || parsed.invariant_cmd || config.verify.invariant,
186
+ e2e: parsed.verify?.e2e || parsed.e2e_cmd || config.verify.e2e,
150
187
  teardown: parsed.verify?.teardown || config.verify.teardown,
151
188
  build: parsed.verify?.build || parsed.build_cmd || config.verify.build,
189
+ stages: parsed.verify?.stages || config.verify.stages,
190
+ policy: parsed.verify?.policy || config.verify.policy,
152
191
  timeoutMs: parsed.verify?.timeoutMs || parsed.verify?.timeout_ms || config.verify.timeoutMs,
153
192
  };
154
193
  }
@@ -200,11 +239,16 @@ export async function gate(opts = {}) {
200
239
  return { ok: false, code: 6, phases };
201
240
  }
202
241
 
242
+ // Pre-verification: compute test directory integrity hash
243
+ const preTestHashResult = computeDirectoryHash(root, { testOnly: true });
244
+ const preTestHash = preTestHashResult.treeHash;
245
+ const executionRecords = [];
246
+
203
247
  // Phase 4: Automated Test & Build Verification (Uses trusted verify commands only)
204
- const testCmd = trustedVerify.test;
205
- const buildCmd = trustedVerify.build;
206
248
  let testResult = { ok: true, status: 0 };
207
249
  let buildResult = { ok: true, status: 0 };
250
+ let serverResult = { ok: true, status: 0 };
251
+ let failingCmd = null;
208
252
 
209
253
  const existingNodeOptions = process.env.NODE_OPTIONS || "";
210
254
  const netGuardUrl = new URL("./preload-net-guard.mjs", import.meta.url).href;
@@ -219,100 +263,178 @@ export async function gate(opts = {}) {
219
263
  };
220
264
 
221
265
  let flakyVerdictResult = null;
266
+ const verifyTimeout = trustedVerify.timeoutMs || 60000;
222
267
 
223
- if (testCmd) {
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
- };
268
+ // Build sequential execution pipeline (Setup -> Lint -> Test/Unit -> Fuzz -> Invariant -> E2E -> Build -> Server -> Teardown)
269
+ const stagesToRun = [];
270
+ if (Array.isArray(trustedVerify.stages) && trustedVerify.stages.length > 0) {
271
+ stagesToRun.push(...trustedVerify.stages);
272
+ } else {
273
+ if (trustedVerify.setup) {
274
+ stagesToRun.push({ id: "setup", kind: "setup", cmd: trustedVerify.setup, required: true, networkAccess: "allow" });
275
+ }
276
+ if (trustedVerify.lint) {
277
+ stagesToRun.push({ id: "lint", kind: "lint", cmd: trustedVerify.lint, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
278
+ }
279
+ if (trustedVerify.test || trustedVerify.unit) {
280
+ stagesToRun.push({ id: "unit", kind: "test", cmd: trustedVerify.test || trustedVerify.unit, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
281
+ }
282
+ if (trustedVerify.fuzz) {
283
+ stagesToRun.push({ id: "fuzz", kind: "fuzz", cmd: trustedVerify.fuzz, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
284
+ }
285
+ if (trustedVerify.invariant) {
286
+ stagesToRun.push({ id: "invariant", kind: "invariant", cmd: trustedVerify.invariant, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
287
+ }
288
+ if (trustedVerify.e2e) {
289
+ stagesToRun.push({ id: "e2e", kind: "e2e", cmd: trustedVerify.e2e, required: true, networkAccess: "allow" });
290
+ }
291
+ if (trustedVerify.build) {
292
+ stagesToRun.push({ id: "build", kind: "build", cmd: trustedVerify.build, required: true, networkAccess: trustedVerify.policy?.networkAccess || "allow" });
293
+ }
294
+ }
254
295
 
255
- const fingerprint = !testResult.ok ? fingerprintFailureState(testResult, root) : null;
256
- recordVerifyRun(root, testCmd, testResult.ok, fingerprint, durationMs);
296
+ try {
297
+ for (const stage of stagesToRun) {
298
+ if (!stage.cmd) continue;
299
+ const startTime = Date.now();
300
+ const stageEnv = stage.networkAccess === "forbidden" || trustedVerify.policy?.offline
301
+ ? { ...testEnv, JULES_SANDBOX_OFFLINE: "1" }
302
+ : testEnv;
303
+
304
+ const res = runCmd(stage.cmd, { cwd: root, ignoreError: true, env: stageEnv, timeout: stage.timeoutMs || verifyTimeout });
305
+ const durationMs = Date.now() - startTime;
306
+ const stdoutRedacted = redactSecrets(res.stdout || "");
307
+ const stderrRedacted = redactSecrets(res.stderr || "");
308
+ const stageOk = res.status === 0;
309
+
310
+ executionRecords.push({
311
+ id: stage.id || stage.kind,
312
+ kind: stage.kind,
313
+ cmd: stage.cmd,
314
+ exitCode: res.status,
315
+ durationMs,
316
+ networkAccess: stage.networkAccess || "allow",
317
+ stdoutHash: "sha256:" + sha256(stdoutRedacted),
318
+ stderrHash: "sha256:" + sha256(stderrRedacted),
319
+ });
257
320
 
258
- if (!testResult.ok) {
259
- const runs = readVerifyRuns(root, testCmd);
321
+ if (stage.kind === "test" || stage.kind === "unit") {
322
+ testResult = { ok: stageOk, status: res.status, stdout: stdoutRedacted, stderr: stderrRedacted, command: stage.cmd };
323
+ const fingerprint = !stageOk ? fingerprintFailureState(testResult, root) : null;
324
+ recordVerifyRun(root, stage.cmd, stageOk, fingerprint, durationMs);
325
+ if (!stageOk) {
326
+ const runs = readVerifyRuns(root, stage.cmd);
260
327
  flakyVerdictResult = flakyVerdict(runs);
261
328
  if (flakyVerdictResult.verdict === "QUARANTINED") {
262
- phases.push({ phase: "verify", ok: false, testResult, buildResult, flakyVerdict: flakyVerdictResult });
329
+ phases.push({ phase: "verify", ok: false, testResult, buildResult, flakyVerdict: flakyVerdictResult, executionRecords });
263
330
  appendTelemetry(root, "gate_phase", { phase: "verify", ok: false, quarantined: true });
264
331
  appendTelemetry(root, "gate_finished", { ok: false, code: 8 });
265
332
  return { ok: false, code: 8, phases, flakyVerdict: flakyVerdictResult };
266
333
  }
267
334
  }
335
+ } else if (stage.kind === "build") {
336
+ buildResult = { ok: stageOk, status: res.status, stdout: stdoutRedacted, stderr: stderrRedacted, command: stage.cmd };
268
337
  }
269
- } finally {
270
- if (teardownCmd) {
271
- try {
272
- runCmd(teardownCmd, { cwd: root, ignoreError: true, env: testEnv });
273
- } catch (_) {}
338
+
339
+ if (!stageOk && stage.required !== false) {
340
+ failingCmd = {
341
+ ok: false,
342
+ status: res.status,
343
+ stdout: stdoutRedacted,
344
+ stderr: stderrRedacted,
345
+ command: stage.cmd,
346
+ phase: stage.kind,
347
+ };
348
+ break;
274
349
  }
275
350
  }
351
+ } finally {
352
+ if (trustedVerify.teardown) {
353
+ try {
354
+ runCmd(trustedVerify.teardown, { cwd: root, ignoreError: true, env: testEnv });
355
+ } catch (_) {}
356
+ }
276
357
  }
277
358
 
278
- if (testResult.ok && buildCmd) {
279
- const res = runCmd(buildCmd, { cwd: root, ignoreError: true, env: testEnv });
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
- };
359
+ if (!failingCmd && trustedVerify.server && trustedVerify.server.command) {
360
+ serverResult = await probeDevServer(trustedVerify.server, root);
361
+ if (!serverResult.ok) {
362
+ failingCmd = serverResult;
363
+ }
287
364
  }
288
365
 
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);
366
+ // Post-verification test integrity check (No Test Weakening Invariant)
367
+ const postTestHashResult = computeDirectoryHash(root, { testOnly: true });
368
+ const postTestHash = postTestHashResult.treeHash;
369
+ let testTampered = false;
370
+ if (config.evidence?.strictTestLock && !opts.allowTestModifications && preTestHashResult.fileCount > 0) {
371
+ const changedTestFile = files.find((f) => {
372
+ const lower = f.toLowerCase();
373
+ return lower.startsWith("test/") || lower.startsWith("tests/") || lower.includes(".test.") || lower.includes(".spec.");
374
+ });
375
+ if (changedTestFile && preTestHash !== postTestHash) {
376
+ testTampered = true;
377
+ }
378
+ }
379
+
380
+ // Generate & persist Evidence Manifest
381
+ const evidenceManifest = generateEvidenceManifest(root, {
382
+ preTestHash,
383
+ executionRecords,
384
+ secretScanOk: secretResult.ok,
385
+ diffKb: Math.round(bytes / 1024),
386
+ maxDiffKb: config.limits.diffKb || 75,
387
+ protectedScopeOk: scopeResult.ok,
388
+ repository: config.provider || "jules",
389
+ });
390
+ if (testTampered) {
391
+ evidenceManifest.testIntegrity.tamperDetected = true;
292
392
  }
393
+ let evidencePath = null;
394
+ try {
395
+ evidencePath = writeEvidenceManifest(root, evidenceManifest);
396
+ } catch (_) {}
397
+
398
+ const verifyOk = !failingCmd && !testTampered;
399
+ phases.push({
400
+ phase: "verify",
401
+ ok: verifyOk,
402
+ testResult,
403
+ buildResult,
404
+ serverResult,
405
+ executionRecords,
406
+ testIntegrity: {
407
+ preTestHash,
408
+ postTestHash,
409
+ tamperDetected: testTampered,
410
+ },
411
+ });
412
+ phases.push({
413
+ phase: "evidence",
414
+ ok: !testTampered,
415
+ manifestPath: evidencePath,
416
+ evidenceHash: evidenceManifest.evidenceHash,
417
+ });
293
418
 
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 });
297
419
  appendTelemetry(root, "gate_phase", { phase: "verify", ok: verifyOk });
298
420
  if (progressBus && progressToken) {
299
- progressBus.reportProgress(progressToken, 100, 100, "Phase 4/4: Automated Test & Build Verification complete");
421
+ progressBus.reportProgress(progressToken, 100, 100, "Phase 4/4: Automated Tiered Verification complete");
300
422
  }
301
423
 
302
- if (!verifyOk && opts.fix) {
424
+ if (!verifyOk && opts.fix && failingCmd) {
303
425
  const repairs = await repair(failingCmd, { config, root, signal: opts.signal, progressBus, progressToken });
304
426
  const finalOk = repairs.ok;
305
427
  const finalCode = repairs.ok ? 0 : 4;
306
428
  appendTelemetry(root, "gate_finished", { ok: finalOk, code: finalCode, repaired: true });
307
429
  if (repairs.ok) {
308
- return { ok: true, code: 0, phases, repairs };
430
+ return { ok: true, code: 0, phases, repairs, evidence: evidenceManifest };
309
431
  }
310
- return { ok: false, code: 4, phases, repairs };
432
+ return { ok: false, code: 4, phases, repairs, evidence: evidenceManifest };
311
433
  }
312
434
 
313
- const code = verifyOk ? 0 : 4;
435
+ const code = verifyOk ? 0 : testTampered ? 3 : 4;
314
436
  appendTelemetry(root, "gate_finished", { ok: verifyOk, code });
315
- return { ok: verifyOk, code, phases };
437
+ return { ok: verifyOk, code, phases, evidence: evidenceManifest };
316
438
  }
317
439
 
318
440
  /**
@@ -483,6 +605,15 @@ export async function repair(failure, opts = {}) {
483
605
  targetFiles: currentFailure.targetFiles || [],
484
606
  });
485
607
 
608
+ try {
609
+ harvestFailure(root, {
610
+ exitCode: 4,
611
+ diffText: diffText(root) || "",
612
+ taskId: failure?.taskId || "ooda-repair",
613
+ logPath: failure?.logPath,
614
+ });
615
+ } catch (_) {}
616
+
486
617
  return { ok: false, attempts, finalStatus: "OODA_EXHAUSTED" };
487
618
 
488
619
  }
@@ -564,7 +695,15 @@ export async function dispatch(task = {}, opts = {}) {
564
695
  }
565
696
  const root = opts.config?._root || opts.root || process.cwd();
566
697
  const config = opts.config || loadConfig(root);
567
- const provider = createProvider(config.provider, config);
698
+ const { provider, routed, classification } = resolveRoutedProvider(task, config);
699
+ if (routed && classification) {
700
+ appendTelemetry(root, "router_decision", {
701
+ tier: classification.tier,
702
+ forced: classification.forced,
703
+ reason: classification.reason,
704
+ score: classification.score,
705
+ });
706
+ }
568
707
 
569
708
  // Enforce prompt size limit
570
709
  const promptKb = (config.limits.promptKb || 50) * 1024;
@@ -573,7 +712,20 @@ export async function dispatch(task = {}, opts = {}) {
573
712
  }
574
713
 
575
714
  // Redact secrets in prompt before dispatching
576
- const cleanPrompt = redactSecrets(task.prompt || "");
715
+ let cleanPrompt = redactSecrets(task.prompt || "");
716
+
717
+ // Specialist Role resolution (if role is set and not already present in prompt)
718
+ if (task.role && !cleanPrompt.includes("Protocol - ")) {
719
+ const roleObj = resolveRolePrompt(root, task.role);
720
+ if (roleObj) {
721
+ cleanPrompt = `${roleObj.content}\n\n${cleanPrompt}`.trim();
722
+ }
723
+ }
724
+
725
+ // SPORE Memory Hydration (auto-hydrate platform quirks & system learnings)
726
+ if (!cleanPrompt.includes("<ACTIVE_SYSTEM_LEARNINGS>")) {
727
+ cleanPrompt = hydratePrompt(root, cleanPrompt);
728
+ }
577
729
 
578
730
  // Wire Prompt Guard, Memory Hydration & Envelope to wrap raw task arguments
579
731
  const taskInstructions = task.taskInstructions || cleanPrompt || task.title || "Autonomous Task Execution";
@@ -588,11 +740,12 @@ export async function dispatch(task = {}, opts = {}) {
588
740
  const cleanTask = { ...task, prompt: envelopedPrompt };
589
741
 
590
742
  try {
591
- return await withBudget(
743
+ const session = await withBudget(
592
744
  () => provider.dispatch(cleanTask, { root, dryRun: opts.dryRun }),
593
745
  root,
594
746
  config.limits.dailyTasks
595
747
  );
748
+ return classification ? { ...session, _routeTier: classification.tier, _routeReason: classification.reason } : session;
596
749
  } catch (err) {
597
750
  if (err instanceof ProviderRateLimitError || err instanceof ProviderUnavailableError) {
598
751
  if (err.reservationId && !err.budgetReservationRolledBack) {
@@ -629,6 +782,15 @@ export async function run(tasksOrOpts = {}, opts = {}) {
629
782
  const requestedConcurrency = Number(options.concurrency ?? config.limits.concurrency ?? 1);
630
783
  const concurrency = Number.isInteger(requestedConcurrency) && requestedConcurrency > 0 ? requestedConcurrency : 1;
631
784
  const isDry = options.dryRun !== undefined ? options.dryRun : (process.env.JULES_DRY_RUN === "true" || process.env.JULES_DRY_RUN === "1");
785
+
786
+ if (options.dag || options.useDag) {
787
+ return await executeQueueDag(root, {
788
+ concurrency,
789
+ dryRun: isDry,
790
+ config,
791
+ });
792
+ }
793
+
632
794
  const queueDir = getQueueDir(root);
633
795
  const completedDir = join(queueDir, "completed");
634
796
  ensureDir(completedDir);
@@ -713,6 +875,10 @@ export function synthesizePrDescription(session = {}, gateResult = {}, options =
713
875
 
714
876
  const kbDiff = ((payloadPhase.bytes || 0) / 1024).toFixed(1);
715
877
  const kbLimit = ((payloadPhase.limitBytes || 76800) / 1024).toFixed(0);
878
+ const evidenceManifest = gateResult.evidence || (options.evidence ? options.evidence : null);
879
+ const evidenceSection = evidenceManifest
880
+ ? `\n### 🛡️ Cryptographic Evidence Manifest\n- **Manifest ID:** \`${evidenceManifest.manifestId}\`\n- **Signature:** \`${evidenceManifest.evidenceHash?.slice(0, 16)}...\`\n- **Test Integrity:** ${evidenceManifest.testIntegrity?.tamperDetected ? "❌ Tampered" : "✅ Verified (0 test weakening)"}\n`
881
+ : "";
716
882
 
717
883
  const modifiedFiles = options.modifiedFiles || [];
718
884
  const affectedTests = resolveAffectedTests(modifiedFiles, options);
@@ -724,7 +890,7 @@ export function synthesizePrDescription(session = {}, gateResult = {}, options =
724
890
  - **OODA Turns:** \`${attemptsCount}/${maxAttempts}\`
725
891
  - **Warm Resumption:** ${isWarm ? "✅ Active Context Stream" : "Cold Start"}
726
892
  - **Execution Latency:** \`${durationSec}s\`
727
-
893
+ ${evidenceSection}
728
894
  ### 🛡️ Zero-Trust Security Audit Matrix
729
895
  - **Scope Guard:** ${scopePhase.ok ? "✅ PASS (0 protected path violations)" : "❌ FAIL"}
730
896
  - **Diff Payload Governor:** ${payloadPhase.ok ? `✅ PASS (${kbDiff} KB / ${kbLimit} KB limit)` : "❌ EXCEEDED"}