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.
- package/.agent/rules/jules-protocol.md +3 -0
- package/JULES_RULES_TEMPLATE.md +3 -1
- package/README.md +163 -307
- package/bin/agentctl.mjs +177 -7
- package/bin/init.js +8 -8
- package/index.mjs +14 -2
- package/package.json +1 -1
- package/scripts/command-resolver.mjs +1 -2
- package/scripts/doc-sync-check.mjs +36 -1
- package/scripts/jules-create.mjs +1 -1
- package/scripts/jules-dispatch.mjs +1 -2
- package/scripts/jules-nightly.mjs +2 -2
- package/scripts/jules-patch.mjs +2 -2
- package/scripts/jules-queue-runner.mjs +1 -1
- package/scripts/jules-scan-todos.mjs +1 -1
- package/scripts/jules-self-audit.mjs +1 -2
- package/scripts/jules-status.mjs +1 -1
- package/scripts/release.mjs +55 -0
- package/scripts/run-tests.mjs +15 -1
- package/scripts/utils.mjs +14 -9
- package/src/asset-integrity.mjs +76 -0
- package/src/asset_integrity.mjs +1 -76
- package/src/config.mjs +42 -23
- package/src/dag-engine.mjs +44 -6
- package/src/engine.mjs +18 -11
- package/src/execution-envelope.mjs +111 -0
- package/src/execution_envelope.mjs +1 -111
- package/src/git.mjs +6 -3
- package/src/merge-blocks.mjs +178 -1
- package/src/ops/command-registry.mjs +25 -0
- package/src/ops/handover.mjs +411 -0
- package/src/ops/ide-scaffold.mjs +3 -3
- package/src/ops/next-step.mjs +6 -2
- package/src/prompt-guard.mjs +34 -3
- package/src/provider.mjs +191 -86
- package/src/role-resolver.mjs +31 -0
- package/src/rules-budget.mjs +179 -0
- package/src/rules_budget.mjs +1 -179
- package/src/security.mjs +98 -56
- package/src/state.mjs +11 -6
- package/src/task-optimizer.mjs +10 -2
- package/src/tui.mjs +1 -1
- package/src/webhook.mjs +2 -2
- 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
|
+
}
|
package/src/asset_integrity.mjs
CHANGED
|
@@ -1,76 +1 @@
|
|
|
1
|
-
|
|
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
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const
|
|
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
|
|
398
|
-
const
|
|
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:
|
|
422
|
-
lint:
|
|
423
|
-
test:
|
|
424
|
-
unit:
|
|
425
|
-
fuzz:
|
|
426
|
-
invariant:
|
|
427
|
-
e2e:
|
|
428
|
-
teardown:
|
|
429
|
-
build:
|
|
430
|
-
stages: parsed.verify?.stages
|
|
431
|
-
policy: parsed.verify?.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
|
};
|
package/src/dag-engine.mjs
CHANGED
|
@@ -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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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 "./
|
|
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
|
-
|
|
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###
|
|
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 = `##
|
|
924
|
+
const prBody = `## Autonomous Agent Execution Summary
|
|
918
925
|
|
|
919
|
-
###
|
|
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
|
-
###
|
|
932
|
+
### Security & Scope Audit
|
|
926
933
|
- **Scope Guard:** ${scopePhase.ok ? "✅ PASS (0 protected path violations)" : "❌ FAIL"}
|
|
927
|
-
- **Diff Payload
|
|
928
|
-
- **Secret Scanner:** ${secretPhase.ok ? "✅ PASS (0
|
|
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
|
-
###
|
|
937
|
+
### Terminal Verification Output
|
|
931
938
|
\`\`\`text
|
|
932
939
|
${options.testOutput || "All verification tests passed with Exit Code 0."}
|
|
933
940
|
\`\`\`
|
|
934
941
|
|
|
935
|
-
###
|
|
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
|
-
|
|
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(
|
|
242
|
+
untrackedDiff += content.split(/\r?\n/).map((line) => `+${line}`).join("\n") + "\n";
|
|
240
243
|
}
|
|
241
244
|
} catch (_) {}
|
|
242
245
|
}
|