Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a1f0e96
Skill Update
harshit-bstack Jul 29, 2026
4429fba
Update
harshit-bstack Jul 29, 2026
b733759
fix(rca): revert testing-only hardcoding from v2 latency study
Dave3130 Jul 30, 2026
45a1ba8
fix(rca): drop the mandatory pre-turn-1 connector sweep entirely
Dave3130 Jul 30, 2026
94cb38d
feat(rca): pre-fetch build-level evidence once, share via file across…
harshit-bstack Jul 30, 2026
4a3912a
updating prompts
Dave3130 Jul 31, 2026
1eba07f
fix(rca): tighten turnMessageMaxChars to 1000
harshit-bstack Jul 31, 2026
70ef859
feat(rca): let coordinators write gathered evidence back to the share…
harshit-bstack Jul 31, 2026
d33e99e
fix(rca): eliminate lost evidence write-backs via single-writer shards
harshit-bstack Jul 31, 2026
8becf43
feat(rca): memoize read-only tool calls; fast-fail wedged TFA drains
harshit-bstack Jul 31, 2026
6cabb9d
fix(security): write RCA state, evidence and tool cache owner-only
harshit-bstack Jul 31, 2026
8e4a6d0
fix(rca): six defects found by running the cache against a real build
harshit-bstack Jul 31, 2026
69aa4cd
fix(rca): make the tool cache work on real traffic (0% -> 26% hit rate)
harshit-bstack Jul 31, 2026
9e14eec
fix(rca): three defects found by a 6-coordinator swarm on a live build
harshit-bstack Jul 31, 2026
9955ba3
fix(rca): flip() silently discarded results written in RCA_OUTPUT voc…
harshit-bstack Jul 31, 2026
e088c4b
fix(rca): retract the message-size wedge claim; surface folded eviden…
harshit-bstack Jul 31, 2026
c91f7c9
docs(rca): require pre-seeding the MCP cache from Step 4's own queries
harshit-bstack Jul 31, 2026
3c55f1f
fix(rca): three gaps a warm-system verification run exposed
harshit-bstack Jul 31, 2026
96815a9
feat(rca): window-guard PR table; name wrong-arity flip() calls preci…
harshit-bstack Jul 31, 2026
99c1858
fix(rca): pad the log window past finished_at; forward direction; pro…
harshit-bstack Jul 31, 2026
94cad2d
docs(rca): distinguish the two TFA failures; size-check before trusti…
harshit-bstack Jul 31, 2026
20b8bf7
perf(rca): pre-fetch PR changed-file paths in the existing PR-list call
harshit-bstack Aug 2, 2026
19e0f5f
perf(rca): read repo files from local clones, sha-pinned, instead of gh
harshit-bstack Aug 2, 2026
aa00966
fix(rca): keep the local-repo reader generic — no product or machine …
harshit-bstack Aug 2, 2026
286884e
feat(rca): bounded workspace discovery, resolved once and shared via …
harshit-bstack Aug 2, 2026
d40ed4d
fix(rca): wire local-repo reading into the gate and the coordinator p…
harshit-bstack Aug 2, 2026
867b6a3
fix(rca): reachability guard, folded-view reads, and a staleness signal
harshit-bstack Aug 2, 2026
220c7ba
fix(rca): refuse a foreign state-CSV schema instead of silently dropp…
harshit-bstack Aug 2, 2026
3f015a6
fix(rca): tighten a pre-existing state dir, not just newly created ones
harshit-bstack Aug 2, 2026
95539f1
feat(rca): sweep the state dir at gate startup; add an explicit prune
harshit-bstack Aug 2, 2026
2e4e714
fix(rca): two gate bugs found by an actual dry run
harshit-bstack Aug 2, 2026
8c60a83
fix(rca): stop numberless PRs collapsing into a single evidence entry
harshit-bstack Aug 2, 2026
deac271
fix(rca): make clustering persist — clusterRows silently discarded cl…
harshit-bstack Aug 2, 2026
ab9ce22
fix(rca): order siblings after their representative and require a pre…
harshit-bstack Aug 2, 2026
6932e23
fix(rca): make the base evidence file announce that it is a partial view
harshit-bstack Aug 2, 2026
bc9cf65
fix(rca): guard un-resumable pending-resume rows; document turnId/vie…
harshit-bstack Aug 2, 2026
8d7adca
docs(rca): add a generic API reference, and a test that keeps it from…
harshit-bstack Aug 2, 2026
9d24c86
fix(rca): enforce the product-bug evidence rule instead of only stati…
harshit-bstack Aug 2, 2026
1b13b07
feat(rca): pre-dispatch a cluster representative's turn 1 concurrentl…
Dave3130 Aug 3, 2026
c7b2ded
feat(rca): delete a build's temp/registry artifacts after a successfu…
Dave3130 Aug 3, 2026
4fd5a54
feat(rca): server-side failure-theme clustering (Step 3), preferred o…
Dave3130 Aug 3, 2026
ad2cc7c
docs(rca): document server-side theme clustering, Step 4b pre-dispatc…
Dave3130 Aug 3, 2026
9283ae1
refactor: rewrite Step 4b as fire-and-forget async dispatch, parallel…
Dave3130 Aug 3, 2026
aa32532
fix(rca): restate Step 4b→5 resume/turn1_result mapping inline in Step 5
Dave3130 Aug 3, 2026
ca8c64f
docs(rca): strip precise measured stats from SKILL.md, keep the quali…
Dave3130 Aug 3, 2026
164962f
perf(rca): enforce genuine parallel tool dispatch across gate, eviden…
Dave3130 Aug 4, 2026
395960c
fix(rca): bulletproof 4 real step-skips found in a production run
Dave3130 Aug 4, 2026
dafcd29
fix(rca): clarify Step 5's Step-4b gate checks dispatch, not completion
Dave3130 Aug 4, 2026
b726aa1
fix(rca): pin clustersFromThemes' rows source to the seeded CSV, not …
Dave3130 Aug 4, 2026
24d8179
fix(rca): stop resubmitting a thread after 2 consecutive TFA wedges, …
Dave3130 Aug 4, 2026
ef8d695
fix(rca): recover from context-exceeded via one condensed-hypothesis …
Dave3130 Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
node_modules/
.env
.DS_Store
*.code-workspace
# Per-run RCA batch state (the CSV/WAL spine + report) is workspace-local.
.rca/
# Planning docs (brainstorm/ideation/plan) stay local — not pushed.
Expand Down
348 changes: 331 additions & 17 deletions agents/ai-tfa-coordinator.md

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions bin/cached-exec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/usr/bin/env node
// Run a READ-ONLY command through the build's tool cache, in ONE tool call.
//
// Why a wrapper: a "check cache / run / store" sequence done by hand costs
// three tool calls to save one, which is worse than not caching. This collapses
// it to a single call that behaves exactly like the underlying command —
// same stdout, same exit code — but only actually executes on a miss.
//
// Usage (command is ONE argument, so the caller's own quoting survives):
// node bin/cached-exec.mjs <buildId> <writerId> '<command>'
// node bin/cached-exec.mjs <buildId> <writerId> - # command on STDIN
// node bin/cached-exec.mjs <buildId> --stats
//
// Wrap only the expensive fetch and leave filtering to the outer shell:
// node bin/cached-exec.mjs "$B" 3895581484 'gh api repos/o/r/contents/f' | jq -r .content | head -40
// Two coordinators piping the same fetch through different greps then share
// one cache entry, instead of each paying for the fetch.
//
// TWO GOTCHAS, both hit in real use:
//
// 1. Hit/miss banners go to STDERR, so stdout stays byte-identical to the raw
// command and `| jq` works. But `2>&1 | jq` merges the banner back into
// the pipe and jq dies on it ("Invalid literal at line 1, column 12").
// Don't redirect stderr into a pipe. If you silence it with `2>/dev/null`
// you also lose the hit/miss signal — so set `TOOLCACHE_LOG=<path>` and
// the banners are teed there too: `grep -c HIT <path>` still works.
//
// 2. Nested single quotes. A command containing its own `'…'` (typically
// `--jq '.[] | "\(.number)"'`) cannot be passed inside a single-quoted
// argument — the outer shell terminates the string early and the argument
// arrives mangled. Use `-` and pipe the command in on stdin instead:
// printf '%s' 'gh pr list -R o/r --json number --jq ".[].number"' \
// | node bin/cached-exec.mjs "$B" 3895 -

import { execFileSync } from "node:child_process";
import { readFileSync, appendFileSync } from "node:fs";
import {
toolCacheDirFor, cacheKey, cacheGet, cachePut, cacheStats, isRunnable, tokenize,
} from "../lib/tool-cache.mjs";

const [, , buildId, writerOrFlag, commandArg] = process.argv;

// `-` means the command arrives on stdin, which sidesteps the nested-quoting
// problem entirely (see gotcha 2 above).
let command = commandArg;
if (command === "-") {
try {
command = readFileSync(0, "utf8").trim();
} catch {
command = "";
}
if (!command) {
console.error("[tool-cache] '-' given but stdin was empty");
process.exit(2);
}
}

if (!buildId || (writerOrFlag !== "--stats" && !command)) {
console.error("usage: cached-exec.mjs <buildId> <writerId> '<command>'");
console.error(" cached-exec.mjs <buildId> --stats");
process.exit(2);
}

const dir = toolCacheDirFor(buildId, process.env.RCA_STATE_DIR ?? "");

// Where hit/miss banners go. Default stderr keeps stdout byte-identical to the
// wrapped command. But callers pipe stdout into jq/sed and silence stderr with
// `2>/dev/null` to keep the tool chatter out — which also throws away the
// banner, so the run's own hit-rate becomes unmeasurable. Setting
// TOOLCACHE_LOG=<path> tees banners to a file, letting a caller suppress
// stderr and still count hits afterwards (`grep -c HIT <path>`).
const logPath = process.env.TOOLCACHE_LOG ?? "";
function banner(line) {
console.error(line);
if (logPath) {
try {
appendFileSync(logPath, line + "\n", { encoding: "utf8", mode: 0o600 });
} catch {
/* logging must never break the fetch */
}
}
}

if (writerOrFlag === "--stats") {
const s = cacheStats(dir);
console.log(JSON.stringify({ cacheDir: dir, ...s }, null, 2));
process.exit(0);
}

// Parse into a fetch + filter chain before anything runs.
const gate = isRunnable(command);
if (!gate.ok) {
console.error(`[tool-cache REFUSED] ${gate.reason}`);
console.error(` command: ${command}`);
process.exit(2);
}

// Key on the FETCH ONLY. Downstream filters are pure text transforms, so two
// agents filtering the same fetch differently share one cached network call.
const key = cacheKey(gate.fetchText);

// Run one argv with `input` on stdin, no shell. Returns { stdout, exitCode }.
function run(argv, input) {
try {
return {
stdout: execFileSync(argv[0], argv.slice(1), {
encoding: "utf8",
maxBuffer: 64 * 1024 * 1024,
// Capture stderr rather than let it inherit: execFileSync otherwise
// BOTH inherits and captures, so relaying it ourselves printed
// failures three times.
stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
...(input === undefined ? {} : { input }),
}),
exitCode: 0,
};
} catch (err) {
if (err.stderr) process.stderr.write(err.stderr.toString()); // the only copy
return {
stdout: (err.stdout ?? "").toString(),
exitCode: typeof err.status === "number" ? err.status : 1,
};
}
}

let fetched;
const hit = cacheGet(dir, key);
if (hit) {
banner(`[tool-cache HIT ${key} — captured by ${hit.writerId ?? "?"}, ${hit.bytes}B]`);
fetched = hit.stdout;
} else {
const res = run(gate.fetch, undefined);
fetched = res.stdout;
if (res.exitCode !== 0) {
// Preserve the real behaviour. Deliberately NOT cached — a transient
// failure (rate limit, expired token) must not become a permanent answer.
banner(`[tool-cache MISS ${key} — fetch exited ${res.exitCode}, NOT cached]`);
process.stdout.write(fetched);
process.exit(res.exitCode);
}
if (fetched.trim() === "") {
// An empty result is usually a wrong selector or a silently failed lookup;
// caching it creates a sticky, invisible negative for every later reader.
banner(`[tool-cache MISS ${key} — empty result, NOT cached]`);
} else {
// nowMs is read here, at the process edge — lib/ keeps its no-clock
// discipline so it stays sandbox-safe.
cachePut(dir, key, { command: gate.fetchText, writerId: writerOrFlag, stdout: fetched, exitCode: 0 }, Date.now());
banner(`[tool-cache MISS ${key} — stored ${fetched.length}B]`);
}
}

// Apply the filter chain to whatever the fetch produced (cached or fresh).
let out = fetched;
let finalExit = 0;
for (const f of gate.filters) {
const res = run(f, out);
out = res.stdout;
if (res.exitCode !== 0) { finalExit = res.exitCode; break; }
}

process.stdout.write(out);
process.exit(finalExit);
133 changes: 133 additions & 0 deletions bin/cached-mcp.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env node
// Memo cache for READ-ONLY **MCP** tool calls, sharing the same per-build
// store as `cached-exec.mjs`.
//
// Shell calls can be wrapped transparently (`cached-exec.mjs` runs the command
// for you). MCP calls cannot — only the agent can invoke an MCP tool — so the
// contract here is check-then-call:
//
// 1. get → node bin/cached-mcp.mjs <buildId> get <tool> '<argsJson>'
// exit 0 + result on stdout = HIT, skip the MCP call entirely
// exit 1, empty stdout = MISS, make the MCP call yourself
// 2. put → node bin/cached-mcp.mjs <buildId> put <tool> '<argsJson>' <writerId>
// (payload on STDIN — pipe the digest you want shared)
//
// WHEN THIS PAYS OFF, and when it does not. A hit replaces one MCP call with
// one cheap local read, so it wins on latency and on tokens whenever the
// cached payload is a digest smaller than the raw response. A miss costs two
// extra calls (the probe + the store), so this is worth it for **expensive,
// broadly-reusable, build-level queries** — a VictoriaLogs sweep, a
// `listTestIds`, a `getFailureLogs` several coordinators would each re-run —
// and NOT worth it for a one-off lookup only this test will ever need.
//
// Never cacheable (refused): `tfaRcaTurn`, `getTfaTurnResult`,
// `triggerRcaReport`. Those are stateful — a turn's status is *expected* to
// change between reads, so serving one from cache is wrong, not just stale.
// Prefer storing a DIGEST rather than a raw payload: the point is to spare the
// next reader the raw rows, not to relay them.

import { readFileSync, readdirSync, existsSync, appendFileSync } from "node:fs";
import { join } from "node:path";
import {
toolCacheDirFor, mcpCacheKey, cacheGet, cachePut, cacheStats, isCacheableMcp,
} from "../lib/tool-cache.mjs";

// Same TOOLCACHE_LOG tee as cached-exec, so shell and MCP hits can be counted
// from one file. Previously only shell banners were logged, which made a run's
// combined hit rate impossible to total.
const logPath = process.env.TOOLCACHE_LOG ?? "";
function banner(line) {
console.error(line);
if (logPath) {
try { appendFileSync(logPath, line + "\n", { encoding: "utf8", mode: 0o600 }); } catch { /* never break the call */ }
}
}

const [, , buildId, verb, tool, argsJson, writerId] = process.argv;

if (!buildId || !verb) {
console.error("usage: cached-mcp.mjs <buildId> get <tool> '<argsJson>'");
console.error(" cached-mcp.mjs <buildId> put <tool> '<argsJson>' <writerId> # payload on stdin");
console.error(" cached-mcp.mjs <buildId> list # what is cached, with exact args to copy");
console.error(" cached-mcp.mjs <buildId> stats");
process.exit(2);
}

const dir = toolCacheDirFor(buildId, process.env.RCA_STATE_DIR ?? "");

if (verb === "stats") {
console.log(JSON.stringify({ cacheDir: dir, ...cacheStats(dir) }, null, 2));
process.exit(0);
}

// `list` exists because a HIT requires reproducing the args EXACTLY, and
// canonicalization only normalizes key ORDER, not content. A coordinator that
// guesses the logql/window/limit triple misses — one real run burned four
// probe calls guessing, to save two. Listing what is actually cached turns
// that into a single call: read the available queries, then `get` the one you
// want with its args copied verbatim.
if (verb === "list") {
if (!existsSync(dir)) { console.log("(no cache yet)"); process.exit(0); }
let n = 0;
for (const f of readdirSync(dir).filter((x) => x.endsWith(".json"))) {
let e; try { e = JSON.parse(readFileSync(join(dir, f), "utf8")); } catch { continue; }
if (!/^mcp__/.test(e.command ?? "")) continue; // shell entries live here too
n++;
const sp = e.command.indexOf(" ");
console.log(`\n[${e.key}] ${e.command.slice(0, sp)} (by ${e.writerId ?? "?"}, ${e.bytes}B)`);
console.log(` args: ${e.command.slice(sp + 1)}`);
console.log(` digest: ${String(e.stdout).replace(/\s+/g, " ").slice(0, 150)}…`);
}
if (!n) console.log("(no MCP entries cached — the orchestrator should pre-seed Step 4's queries)");
process.exit(0);
}

if (!tool || argsJson === undefined) {
console.error("both <tool> and '<argsJson>' are required");
process.exit(2);
}

if (!isCacheableMcp(tool)) {
console.error(`[mcp-cache REFUSED] ${tool} is stateful — never cache it; call it directly.`);
process.exit(2);
}

let args;
try {
args = JSON.parse(argsJson);
} catch (err) {
console.error(`[mcp-cache] argsJson is not valid JSON: ${err.message}`);
process.exit(2);
}

const key = mcpCacheKey(tool, args);

if (verb === "get") {
const hit = cacheGet(dir, key);
if (!hit) {
banner(`[mcp-cache MISS ${key} ${tool}] — make the MCP call, then 'put' the digest`);
process.exit(1);
}
banner(`[mcp-cache HIT ${key} ${tool} — captured by ${hit.writerId ?? "?"}, ${hit.bytes}B]`);
process.stdout.write(hit.stdout);
process.exit(0);
}

if (verb === "put") {
let payload = "";
try {
payload = readFileSync(0, "utf8"); // stdin
} catch {
payload = "";
}
if (!payload.trim()) {
console.error("[mcp-cache] refusing to store an empty payload");
process.exit(2);
}
const rec = cachePut(dir, key, { command: `${tool} ${argsJson}`, writerId, stdout: payload }, Date.now());
banner(`[mcp-cache STORED ${key} ${tool} — ${rec.bytes}B]`);
process.exit(0);
}

console.error(`unknown verb: ${verb}`);
process.exit(2);
Loading
Loading