jules-orchestrator-kit 0.38.0 → 0.38.2

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.
Files changed (44) hide show
  1. package/.agent/rules/jules-protocol.md +3 -0
  2. package/JULES_RULES_TEMPLATE.md +3 -1
  3. package/README.md +163 -307
  4. package/bin/agentctl.mjs +177 -7
  5. package/bin/init.js +8 -8
  6. package/index.mjs +14 -2
  7. package/package.json +1 -1
  8. package/scripts/command-resolver.mjs +1 -2
  9. package/scripts/doc-sync-check.mjs +36 -1
  10. package/scripts/jules-create.mjs +1 -1
  11. package/scripts/jules-dispatch.mjs +1 -2
  12. package/scripts/jules-nightly.mjs +2 -2
  13. package/scripts/jules-patch.mjs +2 -2
  14. package/scripts/jules-queue-runner.mjs +1 -1
  15. package/scripts/jules-scan-todos.mjs +1 -1
  16. package/scripts/jules-self-audit.mjs +1 -2
  17. package/scripts/jules-status.mjs +1 -1
  18. package/scripts/release.mjs +55 -0
  19. package/scripts/run-tests.mjs +15 -1
  20. package/scripts/utils.mjs +14 -9
  21. package/src/asset-integrity.mjs +76 -0
  22. package/src/asset_integrity.mjs +1 -76
  23. package/src/config.mjs +42 -23
  24. package/src/dag-engine.mjs +44 -6
  25. package/src/engine.mjs +18 -11
  26. package/src/execution-envelope.mjs +111 -0
  27. package/src/execution_envelope.mjs +1 -111
  28. package/src/git.mjs +6 -3
  29. package/src/merge-blocks.mjs +178 -1
  30. package/src/ops/command-registry.mjs +25 -0
  31. package/src/ops/handover.mjs +411 -0
  32. package/src/ops/ide-scaffold.mjs +3 -3
  33. package/src/ops/next-step.mjs +6 -2
  34. package/src/prompt-guard.mjs +34 -3
  35. package/src/provider.mjs +191 -86
  36. package/src/role-resolver.mjs +31 -0
  37. package/src/rules-budget.mjs +179 -0
  38. package/src/rules_budget.mjs +1 -179
  39. package/src/security.mjs +98 -56
  40. package/src/state.mjs +11 -6
  41. package/src/task-optimizer.mjs +10 -2
  42. package/src/tui.mjs +1 -1
  43. package/src/webhook.mjs +2 -2
  44. package/src/wizard-task.mjs +4 -30
@@ -0,0 +1,76 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join, extname } from "node:path";
3
+
4
+ const BINARY_EXTENSIONS = new Set([
5
+ ".woff2", ".woff", ".ttf", ".otf", ".eot",
6
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
7
+ ".pdf", ".ico"
8
+ ]);
9
+
10
+ /**
11
+ * Checks binary and font assets for silent HTML/text corruption (e.g. fake font saved HTML error page).
12
+ *
13
+ * @param {string} searchDir - Directory to scan (e.g., public/ or src/assets/)
14
+ * @returns {{ ok: boolean, checkedCount: number, corruptedFiles: Array<{ path: string, reason: string }> }}
15
+ */
16
+ export function checkAssetIntegrity(searchDir) {
17
+ const corruptedFiles = [];
18
+ let checkedCount = 0;
19
+
20
+ function scan(dir) {
21
+ let entries = [];
22
+ try {
23
+ entries = readdirSync(dir);
24
+ } catch (_) {
25
+ return;
26
+ }
27
+
28
+ for (const entry of entries) {
29
+ if (entry.startsWith(".") || entry === "node_modules") continue;
30
+ const fullPath = join(dir, entry);
31
+ let stat;
32
+ try {
33
+ stat = statSync(fullPath);
34
+ } catch (_) {
35
+ continue;
36
+ }
37
+
38
+ if (stat.isDirectory()) {
39
+ scan(fullPath);
40
+ } else if (stat.isFile()) {
41
+ const ext = extname(entry).toLowerCase();
42
+ if (BINARY_EXTENSIONS.has(ext)) {
43
+ checkedCount++;
44
+ try {
45
+ // Read first 64 bytes
46
+ const fd = readFileSync(fullPath);
47
+ const header = fd.subarray(0, 64).toString("utf-8").toLowerCase().trim();
48
+
49
+ if (
50
+ header.startsWith("<!doctype") ||
51
+ header.startsWith("<html") ||
52
+ header.startsWith("<?xml") ||
53
+ header.startsWith("{\"error") ||
54
+ header.startsWith("{\"status")
55
+ ) {
56
+ corruptedFiles.push({
57
+ path: fullPath,
58
+ reason: `File has extension '${ext}' but starts with text header '${header.substring(0, 20)}'`,
59
+ });
60
+ }
61
+ } catch (_) {
62
+ // Ignore read errors
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ scan(searchDir);
70
+
71
+ return {
72
+ ok: corruptedFiles.length === 0,
73
+ checkedCount,
74
+ corruptedFiles,
75
+ };
76
+ }
@@ -1,76 +1 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
2
- import { join, extname } from "node:path";
3
-
4
- const BINARY_EXTENSIONS = new Set([
5
- ".woff2", ".woff", ".ttf", ".otf", ".eot",
6
- ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
7
- ".pdf", ".ico"
8
- ]);
9
-
10
- /**
11
- * Checks binary and font assets for silent HTML/text corruption (e.g. fake font saved HTML error page).
12
- *
13
- * @param {string} searchDir - Directory to scan (e.g., public/ or src/assets/)
14
- * @returns {{ ok: boolean, checkedCount: number, corruptedFiles: Array<{ path: string, reason: string }> }}
15
- */
16
- export function checkAssetIntegrity(searchDir) {
17
- const corruptedFiles = [];
18
- let checkedCount = 0;
19
-
20
- function scan(dir) {
21
- let entries = [];
22
- try {
23
- entries = readdirSync(dir);
24
- } catch (_) {
25
- return;
26
- }
27
-
28
- for (const entry of entries) {
29
- if (entry.startsWith(".") || entry === "node_modules") continue;
30
- const fullPath = join(dir, entry);
31
- let stat;
32
- try {
33
- stat = statSync(fullPath);
34
- } catch (_) {
35
- continue;
36
- }
37
-
38
- if (stat.isDirectory()) {
39
- scan(fullPath);
40
- } else if (stat.isFile()) {
41
- const ext = extname(entry).toLowerCase();
42
- if (BINARY_EXTENSIONS.has(ext)) {
43
- checkedCount++;
44
- try {
45
- // Read first 64 bytes
46
- const fd = readFileSync(fullPath);
47
- const header = fd.subarray(0, 64).toString("utf-8").toLowerCase().trim();
48
-
49
- if (
50
- header.startsWith("<!doctype") ||
51
- header.startsWith("<html") ||
52
- header.startsWith("<?xml") ||
53
- header.startsWith("{\"error") ||
54
- header.startsWith("{\"status")
55
- ) {
56
- corruptedFiles.push({
57
- path: fullPath,
58
- reason: `File has extension '${ext}' but starts with text header '${header.substring(0, 20)}'`,
59
- });
60
- }
61
- } catch (_) {
62
- // Ignore read errors
63
- }
64
- }
65
- }
66
- }
67
- }
68
-
69
- scan(searchDir);
70
-
71
- return {
72
- ok: corruptedFiles.length === 0,
73
- checkedCount,
74
- corruptedFiles,
75
- };
76
- }
1
+ export * from "./asset-integrity.mjs";
package/src/config.mjs CHANGED
@@ -43,11 +43,22 @@ export const BUILTIN_DENY = [
43
43
 
44
44
  export const BUILTIN_PROTECT = [
45
45
  ".agent/rules/**",
46
+ ".agent/SYSTEM_LEARNINGS.md",
47
+ ".agent/knowledge/**",
46
48
  "package.json",
49
+ "**/package.json",
47
50
  "Cargo.toml",
51
+ "**/Cargo.toml",
48
52
  "pyproject.toml",
53
+ "**/pyproject.toml",
49
54
  "go.mod",
55
+ "**/go.mod",
56
+ "composer.json",
57
+ "**/composer.json",
50
58
  "Makefile",
59
+ "**/Makefile",
60
+ "**/.npmrc",
61
+ "**/.netrc",
51
62
  ];
52
63
 
53
64
  const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
@@ -383,22 +394,24 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
383
394
  }
384
395
  }
385
396
 
386
- const setupCmd = parsed.setup_cmd || parsed.verify?.setup || "";
387
- const testCmd = parsed.test_cmd || parsed.verify?.test || "";
388
- const lintCmd = parsed.lint_cmd || parsed.verify?.lint || "";
389
- const fuzzCmd = parsed.fuzz_cmd || parsed.verify?.fuzz || "";
390
- const invariantCmd = parsed.invariant_cmd || parsed.verify?.invariant || "";
391
- const e2eCmd = parsed.e2e_cmd || parsed.verify?.e2e || "";
392
- const teardownCmd = parsed.teardown_cmd || parsed.verify?.teardown || "";
393
- const buildCmd = parsed.build_cmd || parsed.verify?.build || "";
397
+ const rawSetup = parsed.setup_cmd ?? parsed.verify?.setup;
398
+ const rawTest = parsed.test_cmd ?? parsed.verify?.test;
399
+ const rawLint = parsed.lint_cmd ?? parsed.verify?.lint;
400
+ const rawFuzz = parsed.fuzz_cmd ?? parsed.verify?.fuzz;
401
+ const rawInvariant = parsed.invariant_cmd ?? parsed.verify?.invariant;
402
+ const rawE2e = parsed.e2e_cmd ?? parsed.verify?.e2e;
403
+ const rawTeardown = parsed.teardown_cmd ?? parsed.verify?.teardown;
404
+ const rawBuild = parsed.build_cmd ?? parsed.verify?.build;
405
+ const rawUnit = parsed.verify?.unit;
394
406
  const verifyTimeoutMs = parsed.verify?.timeoutMs ?? parsed.verify?.timeout_ms ?? 60000;
395
407
  const autoVerify = resolveVerify(root);
396
408
 
397
- const activeTier = String(process.env.JULES_TIER || parsed.tier || "ultra").toLowerCase();
398
- const tierLimits = TIER_PRESETS[activeTier] || TIER_PRESETS.ultra;
409
+ const rawTier = String(process.env.JULES_TIER || parsed.tier || "ultra").toLowerCase();
410
+ const activeTier = TIER_PRESETS[rawTier] ? rawTier : "ultra";
411
+ const tierLimits = TIER_PRESETS[activeTier];
399
412
 
400
- const envDailyTasks = process.env.JULES_DAILY_BUDGET !== undefined ? Number(process.env.JULES_DAILY_BUDGET) : null;
401
- const envDiffKb = process.env.JULES_MAX_DIFF_KB !== undefined ? Number(process.env.JULES_MAX_DIFF_KB) : null;
413
+ const envDailyTasks = process.env.JULES_DAILY_BUDGET !== undefined && Number.isFinite(Number(process.env.JULES_DAILY_BUDGET)) ? Number(process.env.JULES_DAILY_BUDGET) : null;
414
+ const envDiffKb = process.env.JULES_MAX_DIFF_KB !== undefined && Number.isFinite(Number(process.env.JULES_MAX_DIFF_KB)) ? Number(process.env.JULES_MAX_DIFF_KB) : null;
402
415
 
403
416
  const parsedLimits = parsed.limits || {};
404
417
  const normalizedLimits = {
@@ -418,17 +431,17 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
418
431
  provider: parsed.provider || DEFAULTS.provider,
419
432
  tier: activeTier,
420
433
  verify: {
421
- setup: setupCmd || autoVerify.setup || "",
422
- lint: lintCmd || autoVerify.lint || "",
423
- test: testCmd || autoVerify.test,
424
- unit: parsed.verify?.unit || testCmd || autoVerify.unit || autoVerify.test,
425
- fuzz: fuzzCmd || autoVerify.fuzz || "",
426
- invariant: invariantCmd || autoVerify.invariant || "",
427
- e2e: e2eCmd || autoVerify.e2e || "",
428
- teardown: teardownCmd || autoVerify.teardown || "",
429
- build: buildCmd || autoVerify.build,
430
- stages: parsed.verify?.stages || autoVerify.stages || null,
431
- policy: parsed.verify?.policy || autoVerify.policy,
434
+ setup: rawSetup ?? autoVerify.setup ?? "",
435
+ lint: rawLint ?? autoVerify.lint ?? "",
436
+ test: rawTest ?? autoVerify.test,
437
+ unit: rawUnit ?? rawTest ?? autoVerify.unit ?? autoVerify.test,
438
+ fuzz: rawFuzz ?? autoVerify.fuzz ?? "",
439
+ invariant: rawInvariant ?? autoVerify.invariant ?? "",
440
+ e2e: rawE2e ?? autoVerify.e2e ?? "",
441
+ teardown: rawTeardown ?? autoVerify.teardown ?? "",
442
+ build: rawBuild ?? autoVerify.build,
443
+ stages: parsed.verify?.stages ?? autoVerify.stages ?? null,
444
+ policy: parsed.verify?.policy ?? autoVerify.policy,
432
445
  timeoutMs: Number.isFinite(Number(verifyTimeoutMs)) ? Number(verifyTimeoutMs) : 60000,
433
446
  },
434
447
  evidence: {
@@ -481,6 +494,12 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
481
494
  runner: parsed.runner || DEFAULTS.runner,
482
495
  branchPrefix: parsed.branch_prefix || parsed.branchPrefix || DEFAULTS.branchPrefix,
483
496
  baseBranch: parsed.base_branch || parsed.baseBranch || DEFAULTS.baseBranch,
497
+ julesApiKeys: Array.from(new Set([
498
+ (process.env.JULES_API_KEY || "").trim(),
499
+ ...(process.env.JULES_API_KEYS || process.env.JULES_API_KEY_SECONDARY || "").split(",").map((k) => k.trim()),
500
+ ...(Array.isArray(parsed.jules_api_keys) ? parsed.jules_api_keys : []),
501
+ ...(Array.isArray(parsed.julesApiKeys) ? parsed.julesApiKeys : []),
502
+ ].filter(Boolean))),
484
503
  _root: root,
485
504
  _file: configFile || null,
486
505
  };
@@ -417,9 +417,42 @@ export function resolveAffectedTests(modifiedFiles = [], options = {}) {
417
417
  * @param {Object} [options.config]
418
418
  * @returns {Promise<{ processed: number, results: Array<any> }>}
419
419
  */
420
+ /**
421
+ * Decides whether a queue-directory entry is a task this runner should execute.
422
+ *
423
+ * The DAG runner accepts more shapes than `isTaskFile` does — `.json` and
424
+ * `.task` envelopes as well as Markdown — but "every `.json` in the queue
425
+ * folder" also swept up manifests (swarm run files, indexes). A manifest has no
426
+ * `prompt`, so the whole file became the prompt and a large one blew the
427
+ * provider payload limit. Shape decides, not the extension.
428
+ *
429
+ * @param {string} fileName
430
+ * @param {string} content
431
+ * @param {Function} isTaskFile - `isTaskFile` from engine.mjs, passed in to avoid a circular import.
432
+ * @returns {boolean}
433
+ */
434
+ function isDagTaskFile(fileName, content, isTaskFile) {
435
+ if (fileName.endsWith(".task")) return true;
436
+ if (fileName.endsWith(".md")) return isTaskFile(fileName, content);
437
+ if (fileName.endsWith(".json")) {
438
+ let parsed;
439
+ try {
440
+ parsed = JSON.parse(content);
441
+ } catch (_) {
442
+ return false;
443
+ }
444
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
445
+ // A manifest *describes* tasks; a task envelope *is* one.
446
+ if (Array.isArray(parsed.tasks) || Array.isArray(parsed.envelopes)) return false;
447
+ return typeof parsed.prompt === "string" && parsed.prompt.length > 0;
448
+ }
449
+ return false;
450
+ }
451
+
420
452
  export async function executeQueueDag(root = process.cwd(), options = {}) {
421
453
  const { readdirSync, renameSync, mkdirSync } = await import("node:fs");
422
454
  const { join, basename } = await import("node:path");
455
+ const { isTaskFile } = await import("./engine.mjs");
423
456
 
424
457
  const queueDir = join(root, ".agent", "jules-queue");
425
458
  const fallbackQueueDir = join(root, ".agent", "queue");
@@ -429,7 +462,7 @@ export async function executeQueueDag(root = process.cwd(), options = {}) {
429
462
  if (!existsSync(actualQueueDir)) {
430
463
  return { processed: 0, results: [] };
431
464
  }
432
- if (!existsSync(completedDir)) {
465
+ if (!existsSync(completedDir) && !options.dryRun) {
433
466
  try {
434
467
  mkdirSync(completedDir, { recursive: true });
435
468
  } catch (_) {}
@@ -452,6 +485,7 @@ export async function executeQueueDag(root = process.cwd(), options = {}) {
452
485
  if (basename(file) !== file) continue;
453
486
  const fullPath = join(actualQueueDir, file);
454
487
  const content = readFileSync(fullPath, "utf-8");
488
+ if (!isDagTaskFile(file, content, isTaskFile)) continue;
455
489
  let taskId = file.replace(/\.(md|json|task)$/, "");
456
490
  let dependsOn = [];
457
491
  let title = taskId;
@@ -508,13 +542,17 @@ export async function executeQueueDag(root = process.cwd(), options = {}) {
508
542
  results.push({ file: t.file, taskId: t.taskId, ok: false, status: session.status, error: session.error, session });
509
543
  throw new Error(`DAG Task '${t.taskId}' failed: ${session.error || session.status || "Unknown error"}`);
510
544
  } else {
511
- const dstPath = join(completedDir, t.file);
512
- if (existsSync(srcPath)) {
513
- if (!existsSync(dstPath)) {
514
- renameSync(srcPath, dstPath);
545
+ // A dry run simulates; it must leave the queue exactly as it found it,
546
+ // or the second `--dry-run` sees an empty queue.
547
+ if (!options.dryRun) {
548
+ const dstPath = join(completedDir, t.file);
549
+ if (existsSync(srcPath)) {
550
+ if (!existsSync(dstPath)) {
551
+ renameSync(srcPath, dstPath);
552
+ }
515
553
  }
516
554
  }
517
- results.push({ file: t.file, taskId: t.taskId, ok: true, session });
555
+ results.push({ file: t.file, taskId: t.taskId, ok: true, dryRun: Boolean(options.dryRun), session });
518
556
  }
519
557
  },
520
558
  });
package/src/engine.mjs CHANGED
@@ -16,7 +16,7 @@ import { spawn } from "node:child_process";
16
16
  import { resolveAffectedTests, executeQueueDag } from "./dag-engine.mjs";
17
17
  import { recordRemediation, queryRemediations, harvestFailureRecord, hydrateMemory } from "./remediation.mjs";
18
18
  import { hydratePrompt, harvestFailure } from "./memory.mjs";
19
- import { resolveRolePrompt } from "./wizard-task.mjs";
19
+ import { resolveRolePrompt } from "./role-resolver.mjs";
20
20
 
21
21
  import {
22
22
  computeDirectoryHash,
@@ -824,7 +824,7 @@ export async function run(tasksOrOpts = {}, opts = {}) {
824
824
 
825
825
  const queueDir = getQueueDir(root);
826
826
  const completedDir = join(queueDir, "completed");
827
- ensureDir(completedDir);
827
+ if (!isDry) ensureDir(completedDir);
828
828
 
829
829
  let filesToProcess = [];
830
830
  if (Array.isArray(tasks)) {
@@ -858,10 +858,17 @@ export async function run(tasksOrOpts = {}, opts = {}) {
858
858
  return;
859
859
  }
860
860
 
861
- const session = await dispatch(task, { root, config, dryRun: isDry });
861
+ // options.provider mirrors dispatch()'s existing injection point so a
862
+ // caller can exercise the real queue lifecycle without a live provider.
863
+ const session = await dispatch(task, { root, config, dryRun: isDry, provider: options.provider });
862
864
 
863
865
  if (session && session.ok === false) {
864
866
  results.push({ file: fileName, ok: false, status: session.status, error: session.error, session });
867
+ } else if (isDry) {
868
+ // A dry run simulates; it must leave the queue exactly as it found
869
+ // it, or the second `--dry-run` sees an empty queue. Nothing
870
+ // completed either, so nothing is written to the ledger.
871
+ results.push({ file: fileName, ok: true, dryRun: true, session });
865
872
  } else {
866
873
  const dstPath = join(completedDir, fileName);
867
874
  if (existsSync(srcPath)) {
@@ -908,31 +915,31 @@ export function synthesizePrDescription(session = {}, gateResult = {}, options =
908
915
  const kbLimit = ((payloadPhase.limitBytes || 76800) / 1024).toFixed(0);
909
916
  const evidenceManifest = gateResult.evidence || (options.evidence ? options.evidence : null);
910
917
  const evidenceSection = evidenceManifest
911
- ? `\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`
918
+ ? `\n### Evidence Manifest\n- **Manifest ID:** \`${evidenceManifest.manifestId}\`\n- **Digest:** \`${evidenceManifest.evidenceHash?.slice(0, 16)}...\`\n- **Test Integrity:** ${evidenceManifest.testIntegrity?.tamperDetected ? "❌ Tampered" : "✅ Verified (0 test weakening)"}\n`
912
919
  : "";
913
920
 
914
921
  const modifiedFiles = options.modifiedFiles || [];
915
922
  const affectedTests = resolveAffectedTests(modifiedFiles, options);
916
923
 
917
- const prBody = `## 🚀 Autonomous Jules Agent Execution Evidence
924
+ const prBody = `## Autonomous Agent Execution Summary
918
925
 
919
- ### ⏱️ OODA Verification Trace
926
+ ### Verification Trace
920
927
  - **Session ID:** \`${sessionId}\`
921
928
  - **OODA Turns:** \`${attemptsCount}/${maxAttempts}\`
922
929
  - **Warm Resumption:** ${isWarm ? "✅ Active Context Stream" : "Cold Start"}
923
930
  - **Execution Latency:** \`${durationSec}s\`
924
931
  ${evidenceSection}
925
- ### 🛡️ Zero-Trust Security Audit Matrix
932
+ ### Security & Scope Audit
926
933
  - **Scope Guard:** ${scopePhase.ok ? "✅ PASS (0 protected path violations)" : "❌ FAIL"}
927
- - **Diff Payload Governor:** ${payloadPhase.ok ? `✅ PASS (${kbDiff} KB / ${kbLimit} KB limit)` : "❌ EXCEEDED"}
928
- - **Secret Scanner:** ${secretPhase.ok ? "✅ PASS (0 high-entropy leaks detected)" : "❌ LEAKS FOUND"}
934
+ - **Diff Payload Budget:** ${payloadPhase.ok ? `✅ PASS (${kbDiff} KB / ${kbLimit} KB limit)` : "❌ EXCEEDED"}
935
+ - **Secret Scanner:** ${secretPhase.ok ? "✅ PASS (0 credentials detected)" : "❌ LEAKS FOUND"}
929
936
 
930
- ### 🧪 Terminal Verification Evidence
937
+ ### Terminal Verification Output
931
938
  \`\`\`text
932
939
  ${options.testOutput || "All verification tests passed with Exit Code 0."}
933
940
  \`\`\`
934
941
 
935
- ### 📂 AST Impact & Dependency Graph
942
+ ### Affected Tests & Impact Analysis
936
943
  ${
937
944
  affectedTests === null
938
945
  ? "- **Impact Scope:** ⚠️ Global contract change detected (`package.json`/`tsconfig`/`schema`). Full test suite executed."
@@ -0,0 +1,111 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolveRoot } from "./config.mjs";
3
+ import { resolveBase } from "./git.mjs";
4
+ import { classifyRiskTier } from "./risk.mjs";
5
+ import { queryRemediations } from "./remediation.mjs";
6
+
7
+ /**
8
+ * Generates a SHA-256 hash of the execution envelope payload.
9
+ */
10
+ export function hashExecutionEnvelope(envelope) {
11
+ if (!envelope || typeof envelope !== "object") return "";
12
+ const rawPayload = {
13
+ id: envelope.id,
14
+ taskId: envelope.taskId,
15
+ baseRef: envelope.baseRef,
16
+ baseSha: envelope.baseSha,
17
+ configSha: envelope.configSha,
18
+ scope: envelope.scope,
19
+ verify: envelope.verify,
20
+ riskTier: envelope.riskTier,
21
+ budgetReservationId: envelope.budgetReservationId,
22
+ capabilities: envelope.capabilities,
23
+ remediations: envelope.remediations || [],
24
+ createdAt: envelope.createdAt,
25
+ };
26
+ const sortedKeys = Object.keys(rawPayload).sort();
27
+ const canonicalPayload = {};
28
+ for (const k of sortedKeys) {
29
+ if (rawPayload[k] !== undefined) {
30
+ canonicalPayload[k] = rawPayload[k];
31
+ }
32
+ }
33
+ return createHash("sha256").update(JSON.stringify(canonicalPayload)).digest("hex");
34
+ }
35
+
36
+ /**
37
+ * Deep freezes an execution envelope to prevent modifications during OODA repair loops.
38
+ */
39
+ export function freezeExecutionEnvelope(envelope) {
40
+ if (!envelope || typeof envelope !== "object") return envelope;
41
+ return Object.freeze({
42
+ ...envelope,
43
+ scope: Object.freeze({
44
+ deny: Object.freeze([...(envelope.scope?.deny || [])]),
45
+ allow: Object.freeze([...(envelope.scope?.allow || [])]),
46
+ protect: Object.freeze([...(envelope.scope?.protect || [])]),
47
+ }),
48
+ verify: Object.freeze({ ...(envelope.verify || {}) }),
49
+ capabilities: Object.freeze([...(envelope.capabilities || [])]),
50
+ remediations: Object.freeze([...(envelope.remediations || [])]),
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Creates an immutable Capability-Bounded Execution Envelope (CBEE) with pre-flight remediation hydration.
56
+ */
57
+ export function createExecutionEnvelope(task = {}, opts = {}) {
58
+ const root = opts.root || resolveRoot();
59
+ const baseRef = opts.base || task.base || "main";
60
+ const baseSha = resolveBase(root, baseRef);
61
+
62
+ const taskId = task.id || opts.taskId || `task-${Date.now()}`;
63
+ const envelopeId = `env-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
64
+
65
+ const denyPaths = opts.forbiddenPaths || task.forbiddenPaths || [".github/**", "package-lock.json", "pnpm-lock.yaml"];
66
+ const allowPaths = opts.allowPaths || task.allowPaths || [];
67
+ const protectPaths = opts.protectPaths || task.protectPaths || ["package.json", "tsconfig.json", "AGENTS.md"];
68
+
69
+ const files = task.files || opts.files || [];
70
+ const riskMeta = classifyRiskTier(files, { diffLines: opts.diffLines || 0 });
71
+
72
+ const taskFingerprint = task.fingerprint || opts.fingerprint || null;
73
+ const remediations = queryRemediations(root, { targetFiles: files, fingerprint: taskFingerprint, limit: 3 });
74
+
75
+ const rawEnvelope = {
76
+ id: envelopeId,
77
+ taskId,
78
+ baseRef,
79
+ baseSha,
80
+ configSha: createHash("sha256").update(JSON.stringify({ denyPaths, allowPaths, protectPaths })).digest("hex"),
81
+ scope: {
82
+ deny: denyPaths,
83
+ allow: allowPaths,
84
+ protect: protectPaths,
85
+ },
86
+ verify: {
87
+ testCmd: opts.testCmd || task.testCmd || "npm test",
88
+ buildCmd: opts.buildCmd || task.buildCmd || "npm run build",
89
+ },
90
+ riskTier: riskMeta.tier,
91
+ budgetReservationId: opts.reservationId || null,
92
+ capabilities: opts.capabilities || ["git:read", "git:diff", "test:run"],
93
+ remediations,
94
+ createdAt: new Date().toISOString(),
95
+ };
96
+
97
+ const envelopeHash = hashExecutionEnvelope(rawEnvelope);
98
+ const fullEnvelope = { ...rawEnvelope, hash: envelopeHash };
99
+
100
+ return freezeExecutionEnvelope(fullEnvelope);
101
+ }
102
+
103
+ /**
104
+ * Validates that an envelope has not been tampered with.
105
+ */
106
+ export function verifyExecutionEnvelope(envelope) {
107
+ if (!envelope || !envelope.hash) return false;
108
+ const expectedHash = hashExecutionEnvelope(envelope);
109
+ return envelope.hash === expectedHash;
110
+ }
111
+
@@ -1,111 +1 @@
1
- import { createHash } from "node:crypto";
2
- import { resolveRoot } from "./config.mjs";
3
- import { resolveBase } from "./git.mjs";
4
- import { classifyRiskTier } from "./risk.mjs";
5
- import { queryRemediations } from "./remediation.mjs";
6
-
7
- /**
8
- * Generates a SHA-256 hash of the execution envelope payload.
9
- */
10
- export function hashExecutionEnvelope(envelope) {
11
- if (!envelope || typeof envelope !== "object") return "";
12
- const rawPayload = {
13
- id: envelope.id,
14
- taskId: envelope.taskId,
15
- baseRef: envelope.baseRef,
16
- baseSha: envelope.baseSha,
17
- configSha: envelope.configSha,
18
- scope: envelope.scope,
19
- verify: envelope.verify,
20
- riskTier: envelope.riskTier,
21
- budgetReservationId: envelope.budgetReservationId,
22
- capabilities: envelope.capabilities,
23
- remediations: envelope.remediations || [],
24
- createdAt: envelope.createdAt,
25
- };
26
- const sortedKeys = Object.keys(rawPayload).sort();
27
- const canonicalPayload = {};
28
- for (const k of sortedKeys) {
29
- if (rawPayload[k] !== undefined) {
30
- canonicalPayload[k] = rawPayload[k];
31
- }
32
- }
33
- return createHash("sha256").update(JSON.stringify(canonicalPayload)).digest("hex");
34
- }
35
-
36
- /**
37
- * Deep freezes an execution envelope to prevent modifications during OODA repair loops.
38
- */
39
- export function freezeExecutionEnvelope(envelope) {
40
- if (!envelope || typeof envelope !== "object") return envelope;
41
- return Object.freeze({
42
- ...envelope,
43
- scope: Object.freeze({
44
- deny: Object.freeze([...(envelope.scope?.deny || [])]),
45
- allow: Object.freeze([...(envelope.scope?.allow || [])]),
46
- protect: Object.freeze([...(envelope.scope?.protect || [])]),
47
- }),
48
- verify: Object.freeze({ ...(envelope.verify || {}) }),
49
- capabilities: Object.freeze([...(envelope.capabilities || [])]),
50
- remediations: Object.freeze([...(envelope.remediations || [])]),
51
- });
52
- }
53
-
54
- /**
55
- * Creates an immutable Capability-Bounded Execution Envelope (CBEE) with pre-flight remediation hydration.
56
- */
57
- export function createExecutionEnvelope(task = {}, opts = {}) {
58
- const root = opts.root || resolveRoot();
59
- const baseRef = opts.base || task.base || "main";
60
- const baseSha = resolveBase(root, baseRef);
61
-
62
- const taskId = task.id || opts.taskId || `task-${Date.now()}`;
63
- const envelopeId = `env-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
64
-
65
- const denyPaths = opts.forbiddenPaths || task.forbiddenPaths || [".github/**", "package-lock.json", "pnpm-lock.yaml"];
66
- const allowPaths = opts.allowPaths || task.allowPaths || [];
67
- const protectPaths = opts.protectPaths || task.protectPaths || ["package.json", "tsconfig.json", "AGENTS.md"];
68
-
69
- const files = task.files || opts.files || [];
70
- const riskMeta = classifyRiskTier(files, { diffLines: opts.diffLines || 0 });
71
-
72
- const taskFingerprint = task.fingerprint || opts.fingerprint || null;
73
- const remediations = queryRemediations(root, { targetFiles: files, fingerprint: taskFingerprint, limit: 3 });
74
-
75
- const rawEnvelope = {
76
- id: envelopeId,
77
- taskId,
78
- baseRef,
79
- baseSha,
80
- configSha: createHash("sha256").update(JSON.stringify({ denyPaths, allowPaths, protectPaths })).digest("hex"),
81
- scope: {
82
- deny: denyPaths,
83
- allow: allowPaths,
84
- protect: protectPaths,
85
- },
86
- verify: {
87
- testCmd: opts.testCmd || task.testCmd || "npm test",
88
- buildCmd: opts.buildCmd || task.buildCmd || "npm run build",
89
- },
90
- riskTier: riskMeta.tier,
91
- budgetReservationId: opts.reservationId || null,
92
- capabilities: opts.capabilities || ["git:read", "git:diff", "test:run"],
93
- remediations,
94
- createdAt: new Date().toISOString(),
95
- };
96
-
97
- const envelopeHash = hashExecutionEnvelope(rawEnvelope);
98
- const fullEnvelope = { ...rawEnvelope, hash: envelopeHash };
99
-
100
- return freezeExecutionEnvelope(fullEnvelope);
101
- }
102
-
103
- /**
104
- * Validates that an envelope has not been tampered with.
105
- */
106
- export function verifyExecutionEnvelope(envelope) {
107
- if (!envelope || !envelope.hash) return false;
108
- const expectedHash = hashExecutionEnvelope(envelope);
109
- return envelope.hash === expectedHash;
110
- }
111
-
1
+ export * from "./execution-envelope.mjs";
package/src/git.mjs CHANGED
@@ -138,8 +138,11 @@ export function git(args = [], opts = {}) {
138
138
  }
139
139
 
140
140
  export function ensureBaseFetched(root = process.cwd(), baseRef = "main") {
141
+ if (!baseRef || typeof baseRef !== "string" || baseRef.startsWith("-") || !/^[A-Za-z0-9._/-]+$/.test(baseRef)) {
142
+ return false;
143
+ }
141
144
  try {
142
- execFileSync("git", ["fetch", "origin", baseRef, "--depth=100"], {
145
+ execFileSync("git", ["fetch", "origin", "--", baseRef, "--depth=100"], {
143
146
  cwd: root,
144
147
  encoding: "utf-8",
145
148
  shell: false,
@@ -148,7 +151,7 @@ export function ensureBaseFetched(root = process.cwd(), baseRef = "main") {
148
151
  return true;
149
152
  } catch (_) {
150
153
  try {
151
- execFileSync("git", ["fetch", "origin", baseRef, "--unshallow"], {
154
+ execFileSync("git", ["fetch", "origin", "--", baseRef, "--unshallow"], {
152
155
  cwd: root,
153
156
  encoding: "utf-8",
154
157
  shell: false,
@@ -236,7 +239,7 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
236
239
  if (existsSync(fullPath)) {
237
240
  const content = readFileSync(fullPath, "utf-8");
238
241
  untrackedDiff += `\ndiff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}\n`;
239
- untrackedDiff += content.split("\n").map((line) => `+${line}`).join("\n") + "\n";
242
+ untrackedDiff += content.split(/\r?\n/).map((line) => `+${line}`).join("\n") + "\n";
240
243
  }
241
244
  } catch (_) {}
242
245
  }