jules-orchestrator-kit 0.42.0 → 0.52.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/.agent/prompts/A11y.md +23 -0
- package/.agent/prompts/Alchemist.md +23 -0
- package/.agent/prompts/Scribe.md +23 -0
- package/.agent/prompts/Spectator.md +24 -0
- package/JULES_RULES_TEMPLATE.md +10 -8
- package/README.md +11 -3
- package/bin/agentctl.mjs +428 -1
- package/index.mjs +36 -1
- package/package.json +1 -1
- package/scripts/asset-integrity-check.mjs +1 -1
- package/scripts/rules-lint.mjs +1 -1
- package/src/assertions.mjs +244 -0
- package/src/config.mjs +25 -0
- package/src/coverage.mjs +328 -0
- package/src/engine.mjs +20 -4
- package/src/git.mjs +131 -9
- package/src/mcp.mjs +277 -0
- package/src/mutation.mjs +739 -0
- package/src/ops/checkpoint.mjs +31 -3
- package/src/perf.mjs +92 -0
- package/src/provider.mjs +144 -6
- package/src/remediation.mjs +154 -1
- package/src/rules-budget.mjs +8 -2
- package/src/scaffold.mjs +1 -1
- package/src/security.mjs +286 -28
- package/src/session-ops.mjs +312 -0
- package/src/stability.mjs +86 -0
- package/src/web-templates.mjs +172 -0
- package/src/asset_integrity.mjs +0 -1
- package/src/execution_envelope.mjs +0 -1
- package/src/rules_budget.mjs +0 -1
package/src/ops/checkpoint.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { git, changedFiles, diffText } from "../git.mjs";
|
|
4
|
-
import { resolveRoot } from "../config.mjs";
|
|
4
|
+
import { resolveRoot, isWindowsAbsolutePath } from "../config.mjs";
|
|
5
5
|
|
|
6
6
|
export class CheckpointError extends Error {
|
|
7
7
|
constructor(message, opts = {}) {
|
|
@@ -11,6 +11,29 @@ export class CheckpointError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Checkpoint ids are used verbatim as the snapshot filename under
|
|
15
|
+
// `.agent/state/checkpoints/`. An id that is not a single plain filename —
|
|
16
|
+
// `../…`, `C:\…`, `\\server\share`, or any value carrying a separator — would
|
|
17
|
+
// let a restore read (or a create write) outside that directory. Ids are
|
|
18
|
+
// therefore restricted to one `[A-Za-z0-9]`-led filename component, and the
|
|
19
|
+
// drive/UNC spellings are rejected explicitly on top of the whitelist
|
|
20
|
+
// (`SEC-02` / `P-01`).
|
|
21
|
+
const CHECKPOINT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
22
|
+
|
|
23
|
+
function assertSafeCheckpointId(id) {
|
|
24
|
+
if (
|
|
25
|
+
typeof id !== "string" ||
|
|
26
|
+
id.length === 0 ||
|
|
27
|
+
isWindowsAbsolutePath(id) ||
|
|
28
|
+
!CHECKPOINT_ID_RE.test(id)
|
|
29
|
+
) {
|
|
30
|
+
throw new CheckpointError(
|
|
31
|
+
`Invalid checkpoint session id '${id}': expected a plain filename (letters, digits, '.', '_', '-') without path separators.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
/**
|
|
15
38
|
* Ensures checkpoint storage directory exists.
|
|
16
39
|
*/
|
|
@@ -29,6 +52,7 @@ export function getCheckpointDir(root = resolveRoot()) {
|
|
|
29
52
|
* @returns {object} Checkpoint metadata
|
|
30
53
|
*/
|
|
31
54
|
export function createCheckpoint(sessionId = `session-${Date.now()}`, options = {}) {
|
|
55
|
+
const safeId = assertSafeCheckpointId(sessionId);
|
|
32
56
|
const root = options.root || resolveRoot();
|
|
33
57
|
const dir = getCheckpointDir(root);
|
|
34
58
|
|
|
@@ -47,7 +71,7 @@ export function createCheckpoint(sessionId = `session-${Date.now()}`, options =
|
|
|
47
71
|
|
|
48
72
|
const snapshot = {
|
|
49
73
|
version: 1,
|
|
50
|
-
id:
|
|
74
|
+
id: safeId,
|
|
51
75
|
timestamp: new Date().toISOString(),
|
|
52
76
|
headSha,
|
|
53
77
|
branch,
|
|
@@ -55,7 +79,7 @@ export function createCheckpoint(sessionId = `session-${Date.now()}`, options =
|
|
|
55
79
|
diffContent,
|
|
56
80
|
};
|
|
57
81
|
|
|
58
|
-
const snapshotPath = join(dir, `${
|
|
82
|
+
const snapshotPath = join(dir, `${safeId}.json`);
|
|
59
83
|
writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
60
84
|
|
|
61
85
|
// Keep last 10 checkpoints
|
|
@@ -83,6 +107,10 @@ export function restoreCheckpoint(sessionId = "--latest", options = {}) {
|
|
|
83
107
|
targetId = list[0].id;
|
|
84
108
|
}
|
|
85
109
|
|
|
110
|
+
// A caller-supplied id becomes a filename here; reject anything that is not
|
|
111
|
+
// a plain filename before `join` can turn it into a path escape (`P-01`).
|
|
112
|
+
targetId = assertSafeCheckpointId(targetId);
|
|
113
|
+
|
|
86
114
|
const snapshotFile = join(dir, `${targetId}.json`);
|
|
87
115
|
if (!existsSync(snapshotFile)) {
|
|
88
116
|
throw new CheckpointError(`Checkpoint snapshot file not found for session '${targetId}'.`);
|
package/src/perf.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { resolveVerify } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Measures Node.js Event Loop delay during test execution or asynchronous workloads
|
|
7
|
+
* to detect synchronous blocking, catastrophic regex backtracking, or O(n^2) starvation.
|
|
8
|
+
*
|
|
9
|
+
* @param {string|Function} target - Command line string to execute or async/sync function
|
|
10
|
+
* @param {object} [options]
|
|
11
|
+
* @param {string} [options.root=process.cwd()] - Working directory
|
|
12
|
+
* @param {number} [options.maxDelayMs=50] - Maximum allowable p99/max event loop delay in milliseconds
|
|
13
|
+
* @param {number} [options.resolution=10] - Sampling resolution in milliseconds (default: 10)
|
|
14
|
+
* @param {number} [options.timeoutMs=60000] - Process timeout in milliseconds
|
|
15
|
+
* @returns {object} { ok, maxMs, meanMs, p50Ms, p90Ms, p99Ms, minMs, thresholdMs, summary, stdout, stderr, exitCode }
|
|
16
|
+
*/
|
|
17
|
+
export function measureEventLoopDelay(target, options = {}) {
|
|
18
|
+
const root = options.root || process.cwd();
|
|
19
|
+
const maxDelayMs = typeof options.maxDelayMs === "number" ? options.maxDelayMs : 50;
|
|
20
|
+
const resolution = options.resolution || 10;
|
|
21
|
+
const timeoutMs = options.timeoutMs || 60000;
|
|
22
|
+
|
|
23
|
+
const histogram = monitorEventLoopDelay({ resolution });
|
|
24
|
+
histogram.enable();
|
|
25
|
+
|
|
26
|
+
let exitCode = 0;
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (typeof target === "function") {
|
|
34
|
+
target();
|
|
35
|
+
} else {
|
|
36
|
+
const cmd = target || resolveVerify(root).testCmd || "npm test";
|
|
37
|
+
const env = { ...process.env };
|
|
38
|
+
for (const k of Object.keys(env)) {
|
|
39
|
+
if (k.startsWith("NODE_TEST_") || k.startsWith("NODE_CHANNEL_")) {
|
|
40
|
+
delete env[k];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
stdout = execSync(cmd, {
|
|
46
|
+
cwd: root,
|
|
47
|
+
env,
|
|
48
|
+
timeout: timeoutMs,
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
exitCode = err.status || 1;
|
|
54
|
+
stdout = err.stdout ? String(err.stdout) : "";
|
|
55
|
+
stderr = err.stderr ? String(err.stderr) : err.message;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
histogram.disable();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const durationMs = Date.now() - startTime;
|
|
63
|
+
|
|
64
|
+
// Convert nanoseconds to milliseconds
|
|
65
|
+
const minMs = Math.round((histogram.min / 1e6) * 100) / 100;
|
|
66
|
+
const maxMs = Math.round((histogram.max / 1e6) * 100) / 100;
|
|
67
|
+
const meanMs = Number.isFinite(histogram.mean) ? Math.round((histogram.mean / 1e6) * 100) / 100 : 0;
|
|
68
|
+
const p50Ms = Math.round((histogram.percentile(50) / 1e6) * 100) / 100;
|
|
69
|
+
const p90Ms = Math.round((histogram.percentile(90) / 1e6) * 100) / 100;
|
|
70
|
+
const p99Ms = Math.round((histogram.percentile(99) / 1e6) * 100) / 100;
|
|
71
|
+
|
|
72
|
+
// If p99 or max exceeds threshold, mark as lag violation
|
|
73
|
+
const ok = exitCode === 0 && (p99Ms <= maxDelayMs || maxMs <= maxDelayMs);
|
|
74
|
+
|
|
75
|
+
const summary = `Event Loop Delay: p99=${p99Ms}ms, max=${maxMs}ms, mean=${meanMs}ms (threshold: ${maxDelayMs}ms, duration: ${durationMs}ms)`;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok,
|
|
79
|
+
minMs,
|
|
80
|
+
maxMs,
|
|
81
|
+
meanMs,
|
|
82
|
+
p50Ms,
|
|
83
|
+
p90Ms,
|
|
84
|
+
p99Ms,
|
|
85
|
+
thresholdMs: maxDelayMs,
|
|
86
|
+
durationMs,
|
|
87
|
+
exitCode,
|
|
88
|
+
stdout,
|
|
89
|
+
stderr,
|
|
90
|
+
summary,
|
|
91
|
+
};
|
|
92
|
+
}
|
package/src/provider.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join, extname } from "node:path";
|
|
4
4
|
import { redactSecrets } from "./security.mjs";
|
|
5
|
+
import { resolveWindowsSpawn } from "./git.mjs";
|
|
5
6
|
|
|
6
7
|
// Extensions node --check can parse as a script. .mjs/.cjs are unambiguous;
|
|
7
8
|
// plain .js is checked as a script too — a stray top-level ESM import would
|
|
@@ -509,10 +510,18 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
509
510
|
|
|
510
511
|
const processedArgs = filteredArgs.map((arg) => interpolateString(arg, data));
|
|
511
512
|
|
|
512
|
-
|
|
513
|
+
// `claude`, `gemini` and `codex` are npm-installed CLI binaries, which on
|
|
514
|
+
// Windows are `.cmd` shims that `spawnSync(..., { shell: false })` cannot
|
|
515
|
+
// start (CreateProcess cannot run a batch file). Route them through
|
|
516
|
+
// cmd.exe with per-argument quoting instead (P-08); on other platforms
|
|
517
|
+
// this is a null and the command spawns directly as before.
|
|
518
|
+
const winSpawn = resolveWindowsSpawn(command, processedArgs, process.env);
|
|
519
|
+
|
|
520
|
+
const res = spawnSync(winSpawn ? winSpawn.file : command, winSpawn ? winSpawn.args : processedArgs, {
|
|
513
521
|
cwd: config._root || process.cwd(),
|
|
514
522
|
encoding: "utf-8",
|
|
515
523
|
shell: false,
|
|
524
|
+
windowsVerbatimArguments: Boolean(winSpawn && winSpawn.verbatim),
|
|
516
525
|
input: providerSpec.promptViaStdin ? data.prompt : undefined,
|
|
517
526
|
timeout: providerSpec.timeoutMs || 900000,
|
|
518
527
|
maxBuffer: 32 * 1024 * 1024,
|
|
@@ -682,9 +691,9 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
682
691
|
}
|
|
683
692
|
|
|
684
693
|
if (providerSpec.type === "http") {
|
|
685
|
-
const getSessionUrlTemplate = providerSpec.getSessionUrl || `${providerSpec.url}/${sessionId}`;
|
|
694
|
+
const getSessionUrlTemplate = ctx.customUrl || providerSpec.getSessionUrl || `${providerSpec.url}/${sessionId}`;
|
|
686
695
|
const urlData = { sessionId, ...ctx };
|
|
687
|
-
const url = interpolateString(getSessionUrlTemplate, urlData);
|
|
696
|
+
const url = ctx.customUrl || interpolateString(getSessionUrlTemplate, urlData);
|
|
688
697
|
|
|
689
698
|
const headerData = { ...urlData, token: rawToken };
|
|
690
699
|
const headers = {};
|
|
@@ -711,8 +720,9 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
711
720
|
let res;
|
|
712
721
|
try {
|
|
713
722
|
res = await fetch(url, {
|
|
714
|
-
method: "GET",
|
|
723
|
+
method: ctx.method || "GET",
|
|
715
724
|
headers,
|
|
725
|
+
body: ctx.body ? JSON.stringify(ctx.body) : undefined,
|
|
716
726
|
signal: AbortSignal.timeout(timeoutMs),
|
|
717
727
|
});
|
|
718
728
|
} catch (err) {
|
|
@@ -807,9 +817,9 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
807
817
|
}
|
|
808
818
|
|
|
809
819
|
if (providerSpec.type === "http") {
|
|
810
|
-
const approvePlanUrlTemplate = providerSpec.approvePlanUrl || `${providerSpec.url}/${sessionId}:approvePlan`;
|
|
820
|
+
const approvePlanUrlTemplate = ctx.customUrl || providerSpec.approvePlanUrl || `${providerSpec.url}/${sessionId}:approvePlan`;
|
|
811
821
|
const urlData = { sessionId, ...ctx };
|
|
812
|
-
const url = interpolateString(approvePlanUrlTemplate, urlData);
|
|
822
|
+
const url = ctx.customUrl || interpolateString(approvePlanUrlTemplate, urlData);
|
|
813
823
|
|
|
814
824
|
const headerData = { ...urlData, token: rawToken };
|
|
815
825
|
const headers = {};
|
|
@@ -909,6 +919,94 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
909
919
|
|
|
910
920
|
throw new Error(`Unsupported provider type: ${providerSpec.type}`);
|
|
911
921
|
},
|
|
922
|
+
|
|
923
|
+
async listSessions(ctx = {}) {
|
|
924
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
925
|
+
if (ctx.dryRun) return { sessions: [], raw: {} };
|
|
926
|
+
if (providerSpec.type === "http") {
|
|
927
|
+
const baseUrl = providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions";
|
|
928
|
+
const params = new URLSearchParams();
|
|
929
|
+
if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
|
|
930
|
+
if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
|
|
931
|
+
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
|
932
|
+
const res = await this.getSession("", { ...ctx, customUrl: url });
|
|
933
|
+
return {
|
|
934
|
+
sessions: res?.raw?.sessions || [],
|
|
935
|
+
nextPageToken: res?.raw?.nextPageToken,
|
|
936
|
+
raw: res?.raw || {},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return { sessions: [], raw: {} };
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
async listActivities(sessionId, ctx = {}) {
|
|
943
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
944
|
+
throw new TypeError("listActivities() requires a valid sessionId string");
|
|
945
|
+
}
|
|
946
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
947
|
+
if (ctx.dryRun) return { activities: [], raw: {} };
|
|
948
|
+
if (providerSpec.type === "http") {
|
|
949
|
+
const baseUrl = `${providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions"}/${sessionId}/activities`;
|
|
950
|
+
const params = new URLSearchParams();
|
|
951
|
+
if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
|
|
952
|
+
if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
|
|
953
|
+
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
|
954
|
+
const res = await this.getSession("", { ...ctx, customUrl: url });
|
|
955
|
+
return {
|
|
956
|
+
activities: res?.raw?.activities || [],
|
|
957
|
+
nextPageToken: res?.raw?.nextPageToken,
|
|
958
|
+
raw: res?.raw || {},
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
return { activities: [], raw: {} };
|
|
962
|
+
},
|
|
963
|
+
|
|
964
|
+
async archiveSession(sessionId, ctx = {}) {
|
|
965
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
966
|
+
throw new TypeError("archiveSession() requires a valid sessionId string");
|
|
967
|
+
}
|
|
968
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
969
|
+
if (ctx.dryRun) return { id: sessionId, archived: true, raw: {} };
|
|
970
|
+
if (providerSpec.type === "http") {
|
|
971
|
+
const url = `${providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions"}/${sessionId}:archive`;
|
|
972
|
+
const res = await this.approvePlan(sessionId, { ...ctx, customUrl: url });
|
|
973
|
+
return { id: sessionId, archived: true, raw: res?.raw || {} };
|
|
974
|
+
}
|
|
975
|
+
return { id: sessionId, archived: true, raw: {} };
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
async deleteSession(sessionId, ctx = {}) {
|
|
979
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
980
|
+
throw new TypeError("deleteSession() requires a valid sessionId string");
|
|
981
|
+
}
|
|
982
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
983
|
+
if (ctx.dryRun) return { id: sessionId, deleted: true, raw: {} };
|
|
984
|
+
if (providerSpec.type === "http") {
|
|
985
|
+
const url = `${providerSpec.url || "https://jules.googleapis.com/v1alpha/sessions"}/${sessionId}`;
|
|
986
|
+
const res = await this.getSession("", { ...ctx, customUrl: url, method: "DELETE" });
|
|
987
|
+
return { id: sessionId, deleted: true, raw: res?.raw || {} };
|
|
988
|
+
}
|
|
989
|
+
return { id: sessionId, deleted: true, raw: {} };
|
|
990
|
+
},
|
|
991
|
+
|
|
992
|
+
async listSources(ctx = {}) {
|
|
993
|
+
if (!ctx || typeof ctx !== "object") ctx = {};
|
|
994
|
+
if (ctx.dryRun) return { sources: [], raw: {} };
|
|
995
|
+
if (providerSpec.type === "http") {
|
|
996
|
+
const baseUrl = "https://jules.googleapis.com/v1alpha/sources";
|
|
997
|
+
const params = new URLSearchParams();
|
|
998
|
+
if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
|
|
999
|
+
if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
|
|
1000
|
+
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
|
1001
|
+
const res = await this.getSession("", { ...ctx, customUrl: url });
|
|
1002
|
+
return {
|
|
1003
|
+
sources: res?.raw?.sources || [],
|
|
1004
|
+
nextPageToken: res?.raw?.nextPageToken,
|
|
1005
|
+
raw: res?.raw || {},
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return { sources: [], raw: {} };
|
|
1009
|
+
},
|
|
912
1010
|
};
|
|
913
1011
|
}
|
|
914
1012
|
|
|
@@ -1029,6 +1127,26 @@ export function createFailoverProvider(providers = ["jules"], config = {}) {
|
|
|
1029
1127
|
}
|
|
1030
1128
|
},
|
|
1031
1129
|
|
|
1130
|
+
async listSessions(ctx = {}) {
|
|
1131
|
+
return providerList[0].listSessions(ctx);
|
|
1132
|
+
},
|
|
1133
|
+
|
|
1134
|
+
async listActivities(sessionId, ctx = {}) {
|
|
1135
|
+
return providerList[0].listActivities(sessionId, ctx);
|
|
1136
|
+
},
|
|
1137
|
+
|
|
1138
|
+
async archiveSession(sessionId, ctx = {}) {
|
|
1139
|
+
return providerList[0].archiveSession(sessionId, ctx);
|
|
1140
|
+
},
|
|
1141
|
+
|
|
1142
|
+
async deleteSession(sessionId, ctx = {}) {
|
|
1143
|
+
return providerList[0].deleteSession(sessionId, ctx);
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
async listSources(ctx = {}) {
|
|
1147
|
+
return providerList[0].listSources(ctx);
|
|
1148
|
+
},
|
|
1149
|
+
|
|
1032
1150
|
validate() {
|
|
1033
1151
|
return providerList.every((p) => typeof p.validate !== "function" || p.validate());
|
|
1034
1152
|
},
|
|
@@ -1099,5 +1217,25 @@ export function createSyntaxVerifiedProvider(fastProvider, complexProvider, conf
|
|
|
1099
1217
|
approvePlan(...args) {
|
|
1100
1218
|
return fastProvider.approvePlan(...args);
|
|
1101
1219
|
},
|
|
1220
|
+
|
|
1221
|
+
listSessions(...args) {
|
|
1222
|
+
return typeof fastProvider.listSessions === "function" ? fastProvider.listSessions(...args) : { sessions: [] };
|
|
1223
|
+
},
|
|
1224
|
+
|
|
1225
|
+
listActivities(...args) {
|
|
1226
|
+
return typeof fastProvider.listActivities === "function" ? fastProvider.listActivities(...args) : { activities: [] };
|
|
1227
|
+
},
|
|
1228
|
+
|
|
1229
|
+
archiveSession(...args) {
|
|
1230
|
+
return typeof fastProvider.archiveSession === "function" ? fastProvider.archiveSession(...args) : { id: args[0], archived: true };
|
|
1231
|
+
},
|
|
1232
|
+
|
|
1233
|
+
deleteSession(...args) {
|
|
1234
|
+
return typeof fastProvider.deleteSession === "function" ? fastProvider.deleteSession(...args) : { id: args[0], deleted: true };
|
|
1235
|
+
},
|
|
1236
|
+
|
|
1237
|
+
listSources(...args) {
|
|
1238
|
+
return typeof fastProvider.listSources === "function" ? fastProvider.listSources(...args) : { sources: [] };
|
|
1239
|
+
},
|
|
1102
1240
|
};
|
|
1103
1241
|
}
|
package/src/remediation.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync, appendFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
4
4
|
import { getStateDir, ensureDir } from "./state.mjs";
|
|
5
5
|
import { redactSecrets } from "./security.mjs";
|
|
6
6
|
|
|
@@ -162,3 +162,156 @@ export function hydrateMemory(root, options = {}) {
|
|
|
162
162
|
return `[LEARNED_REMEDIATIONS_CONTEXT]\nThe following historical failure-and-fix patterns were harvested from previous OODA runs in this repository. Avoid repeating these failure modes:\n\n${items.join("\n\n")}`;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Creates an OODA Thrash Cycle Breaker.
|
|
167
|
+
* Tracks rolling SHA-256 fingerprints of diff hunks and failure states in repair loops.
|
|
168
|
+
* Trips circuit breaker if an identical patch or failure ping-pong cycle is observed.
|
|
169
|
+
*
|
|
170
|
+
* @param {Object} [opts]
|
|
171
|
+
* @param {number} [opts.maxHistory=10] - Maximum states to retain in ring buffer
|
|
172
|
+
* @param {number} [opts.threshold=2] - Number of occurrences before tripping (default: 2 -> A -> B -> A)
|
|
173
|
+
* @returns {{ recordAttempt: (state: { diff?: string, symptom?: string, targetFiles?: string[] }) => { thrash: boolean, cycleLength?: number, fingerprint: string, occurrences: number, reason?: string }, getHistory: () => string[], reset: () => void }}
|
|
174
|
+
*/
|
|
175
|
+
export function createThrashDetector(opts = {}) {
|
|
176
|
+
const maxHistory = opts.maxHistory || 10;
|
|
177
|
+
const threshold = opts.threshold || 2;
|
|
178
|
+
const history = [];
|
|
179
|
+
|
|
180
|
+
function computeStateFingerprint(state = {}) {
|
|
181
|
+
const normDiff = (state.diff || "").trim();
|
|
182
|
+
const normSymptom = (state.symptom || "").trim();
|
|
183
|
+
const normFiles = (state.targetFiles || []).slice().sort().join(",");
|
|
184
|
+
return createHash("sha256")
|
|
185
|
+
.update(`${normDiff}:::${normSymptom}:::${normFiles}`)
|
|
186
|
+
.digest("hex");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function recordAttempt(state = {}) {
|
|
190
|
+
const fingerprint = computeStateFingerprint(state);
|
|
191
|
+
history.push(fingerprint);
|
|
192
|
+
if (history.length > maxHistory) {
|
|
193
|
+
history.shift();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check for duplicate occurrences in history
|
|
197
|
+
const occurrences = history.filter((h) => h === fingerprint).length;
|
|
198
|
+
if (occurrences >= threshold) {
|
|
199
|
+
const firstIdx = history.indexOf(fingerprint);
|
|
200
|
+
const lastIdx = history.lastIndexOf(fingerprint);
|
|
201
|
+
const cycleLength = lastIdx - firstIdx;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
thrash: true,
|
|
205
|
+
cycleLength,
|
|
206
|
+
fingerprint,
|
|
207
|
+
occurrences,
|
|
208
|
+
reason: `OODA Thrash Circuit Tripped: State fingerprint ${fingerprint.slice(0, 12)} repeated ${occurrences} times across repair loop (cycle length: ${cycleLength}).`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
thrash: false,
|
|
214
|
+
fingerprint,
|
|
215
|
+
occurrences,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
recordAttempt,
|
|
221
|
+
getHistory: () => [...history],
|
|
222
|
+
reset: () => {
|
|
223
|
+
history.length = 0;
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Creates a Whack-a-Mole Test-Oscillation Detector for OODA repair loops.
|
|
230
|
+
* Detects when consecutive repair turns oscillate between different failing test sets
|
|
231
|
+
* (e.g. fixing Test A causes Test B to break, then fixing Test B re-breaks Test A).
|
|
232
|
+
*
|
|
233
|
+
* @param {Object} [opts]
|
|
234
|
+
* @param {number} [opts.maxHistory=8] - Maximum test state signatures to retain
|
|
235
|
+
* @param {number} [opts.threshold=2] - Number of repeating occurrences before triggering
|
|
236
|
+
* @returns {{ recordTestOutcome: (failingTests: string[] | string) => { whackAMole: boolean, cycleLength?: number, oscillatingTests?: string[], occurrences: number, promptDirective?: string, reason?: string }, getHistory: () => string[], reset: () => void }}
|
|
237
|
+
*/
|
|
238
|
+
export function createWhackAMoleDetector(opts = {}) {
|
|
239
|
+
const maxHistory = opts.maxHistory || 8;
|
|
240
|
+
const threshold = opts.threshold || 2;
|
|
241
|
+
const history = []; // Array of signature strings
|
|
242
|
+
const testSets = []; // Array of string[]
|
|
243
|
+
|
|
244
|
+
function normalizeTestSet(input) {
|
|
245
|
+
if (!input) return [];
|
|
246
|
+
if (Array.isArray(input)) {
|
|
247
|
+
return [...new Set(input.map((s) => String(s).trim()).filter(Boolean))].sort();
|
|
248
|
+
}
|
|
249
|
+
const str = String(input).trim();
|
|
250
|
+
return str ? [str] : [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function recordTestOutcome(failingInput) {
|
|
254
|
+
const tests = normalizeTestSet(failingInput);
|
|
255
|
+
const signature = tests.join("||") || "__ALL_PASS__";
|
|
256
|
+
|
|
257
|
+
history.push(signature);
|
|
258
|
+
testSets.push(tests);
|
|
259
|
+
|
|
260
|
+
if (history.length > maxHistory) {
|
|
261
|
+
history.shift();
|
|
262
|
+
testSets.shift();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (signature === "__ALL_PASS__") {
|
|
266
|
+
return {
|
|
267
|
+
whackAMole: false,
|
|
268
|
+
occurrences: 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const occurrences = history.filter((h) => h === signature).length;
|
|
273
|
+
if (occurrences >= threshold) {
|
|
274
|
+
const firstIdx = history.indexOf(signature);
|
|
275
|
+
const lastIdx = history.lastIndexOf(signature);
|
|
276
|
+
const cycleLength = lastIdx - firstIdx;
|
|
277
|
+
|
|
278
|
+
// Extract all distinct failing tests observed in the cycle window
|
|
279
|
+
const cycleTests = new Set();
|
|
280
|
+
for (let i = firstIdx; i <= lastIdx; i++) {
|
|
281
|
+
for (const t of testSets[i]) {
|
|
282
|
+
cycleTests.add(t);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const oscillatingTests = [...cycleTests];
|
|
286
|
+
|
|
287
|
+
const testSummary = oscillatingTests.length > 0 ? oscillatingTests.join(" <-> ") : "tests";
|
|
288
|
+
const promptDirective = `[WHACK_A_MOLE_WARNING] You are trapped in a local optimization cycle where fixing one test breaks another (${testSummary}). Do not add more conditional edge-case band-aids. Revert recent patches and refactor the core logic cleanly.`;
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
whackAMole: true,
|
|
292
|
+
cycleLength,
|
|
293
|
+
oscillatingTests,
|
|
294
|
+
occurrences,
|
|
295
|
+
promptDirective,
|
|
296
|
+
reason: `Whack-a-Mole Test Oscillation Detected: Test failure signature repeated across repair turns (${testSummary}).`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
whackAMole: false,
|
|
302
|
+
occurrences,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
recordTestOutcome,
|
|
308
|
+
getHistory: () => [...history],
|
|
309
|
+
reset: () => {
|
|
310
|
+
history.length = 0;
|
|
311
|
+
testSets.length = 0;
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
package/src/rules-budget.mjs
CHANGED
|
@@ -45,8 +45,14 @@ export function checkRulesBudget(root = process.cwd(), opts = {}) {
|
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
47
|
const content = readFileSync(fullPath, "utf-8");
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// A CRLF checkout inflates the character count by one `\r` per line, so
|
|
49
|
+
// the identical rules file passes as LF and fails as CRLF — a false
|
|
50
|
+
// budget violation that depends purely on git's autocrlf setting rather
|
|
51
|
+
// than the content. Normalise line endings before measuring (P-12) so the
|
|
52
|
+
// budget counts the rule text, not the line-ending dialect.
|
|
53
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
54
|
+
const charCount = normalized.length;
|
|
55
|
+
const lineCount = normalized.split("\n").length;
|
|
50
56
|
|
|
51
57
|
if (charCount > maxChars) {
|
|
52
58
|
violations.push({
|
package/src/scaffold.mjs
CHANGED
|
@@ -250,7 +250,7 @@ export function scaffoldRepoAssets(root = process.cwd(), options = {}) {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
if (copyDir(join(KIT_ROOT, ".agent/prompts"), join(agentDir, "prompts"), force) > 0) {
|
|
253
|
-
created.push(".agent/prompts/ (Overseer, Bolt, Sentinel, Janitor)");
|
|
253
|
+
created.push(".agent/prompts/ (Overseer, Bolt, Sentinel, Janitor, A11y, Scribe, Spectator, Alchemist)");
|
|
254
254
|
}
|
|
255
255
|
if (copyDir(join(KIT_ROOT, ".agent/rules"), join(agentDir, "rules"), force) > 0) {
|
|
256
256
|
created.push(".agent/rules/");
|