From 52c764872276a25bf4f15b6f484b6c8de6405424 Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Fri, 29 May 2026 14:38:09 +0000 Subject: [PATCH 1/3] fix(supervisor): validate tx hash evidence --- apps/supervisor/src/index.test.ts | 175 ++++++++++++++++++++++++++++++ apps/supervisor/src/index.ts | 119 ++++++++++++++------ 2 files changed, 258 insertions(+), 36 deletions(-) diff --git a/apps/supervisor/src/index.test.ts b/apps/supervisor/src/index.test.ts index 8a7cf49..ffe57a8 100644 --- a/apps/supervisor/src/index.test.ts +++ b/apps/supervisor/src/index.test.ts @@ -1496,6 +1496,7 @@ describe("classification", () => { outcome: "bot_match_committed", terminal: false, actions: { matchedOrders: 1, deposits: 0 }, + txHashes: [txHash("44")], }); }); @@ -1516,6 +1517,7 @@ describe("classification", () => { expect(classifyActorResult("bot", commandResult("bot", stdout))).toMatchObject({ outcome: "terminal_chain_rejection", terminal: true, + txHashes: [txHash("46")], }); }); @@ -1726,6 +1728,87 @@ describe("classification", () => { }); }); + it("preserves accepted tx hashes in generic early classifications", () => { + expect(classifyActorResult("tester", { + ...commandResult("tester", JSON.stringify({ txHash: txHash("50") })), + timedOut: true, + })).toMatchObject({ + outcome: "command_timeout", + txHashes: [txHash("50")], + }); + expect(classifyActorResult("bot", { + ...commandResult("bot", JSON.stringify(botEvent("bot.transaction.committed", { txHash: txHash("51") }))), + spawnError: "ENOENT", + status: null, + })).toMatchObject({ + outcome: "nonzero_exit", + txHashes: [txHash("51")], + }); + expect(classifyActorResult("bot", { + ...commandResult("bot", JSON.stringify(botEvent("bot.transaction.committed", { txHash: txHash("52") }))), + stdoutTruncated: true, + })).toMatchObject({ + outcome: "malformed_evidence", + txHashes: [txHash("52")], + }); + expect(classifyActorResult("tester", commandResult("tester", [ + JSON.stringify({ txHash: txHash("53") }), + "{not-json}", + ].join("\n")))).toMatchObject({ + outcome: "malformed_evidence", + txHashes: [txHash("53")], + }); + }); + + it("preserves accepted preflight tx hashes in generic early classifications", () => { + expect(classifyActorResult("preflight", { + ...commandResult("preflight", JSON.stringify({ txHash: txHash("54"), bounded: true, maxIterations: 1 }, null, 2)), + timedOut: true, + })).toMatchObject({ + outcome: "command_timeout", + txHashes: [txHash("54")], + }); + }); + + it("does not preserve conflicted tx hashes in generic early classifications", () => { + expect(classifyActorResult("tester", { + ...commandResult("tester", JSON.stringify({ txHash: txHash("56"), error: { txHash: txHash("57") } })), + timedOut: true, + })).toMatchObject({ + outcome: "command_timeout", + txHashes: [], + }); + }); + + it("rejects bot post-broadcast failures with mismatched tx hash evidence", () => { + const stdout = JSON.stringify(botEvent("bot.transaction.failed", { + outcome: "post_broadcast_unresolved", + txHash: txHash("58"), + error: { txHash: txHash("59") }, + })); + + expect(classifyActorResult("bot", commandResult("bot", stdout))).toMatchObject({ + outcome: "malformed_evidence", + terminal: true, + reason: "bot post-broadcast transaction failure evidence contained mismatched tx hashes", + txHashes: [], + }); + }); + + it("rejects tester skips with mismatched tx hash evidence", () => { + const stdout = JSON.stringify({ + txHash: txHash("5a"), + skip: { reason: "fresh-matchable-order", txHash: txHash("5b") }, + }); + + expect(classifyActorResult("tester", commandResult("tester", stdout))).toMatchObject({ + outcome: "malformed_evidence", + terminal: true, + reason: "tester skip evidence contained mismatched tx hashes", + txHashes: [], + }); + }); + it("keeps tester confirmation timeouts classified by safety evidence despite exit code 2", () => { const result = { ...commandResult("tester", JSON.stringify({ @@ -1747,6 +1830,49 @@ describe("classification", () => { }); }); + it("counts matching top-level and nested tester transaction failure tx hashes once", () => { + const result = { + ...commandResult("tester", JSON.stringify({ + txHash: txHash("ae"), + error: { + name: "TransactionConfirmationError", + message: "Transaction confirmation timed out", + txHash: txHash("ae"), + isTimeout: true, + }, + })), + status: 2, + }; + + expect(classifyActorResult("tester", result)).toMatchObject({ + outcome: "confirmation_timeout", + terminal: true, + txHashes: [txHash("ae")], + }); + }); + + it("rejects mismatched top-level and nested tester transaction failure tx hashes", () => { + const result = { + ...commandResult("tester", JSON.stringify({ + txHash: txHash("ae"), + error: { + name: "TransactionConfirmationError", + message: "Transaction confirmation timed out", + txHash: txHash("af"), + isTimeout: true, + }, + })), + status: 2, + }; + + expect(classifyActorResult("tester", result)).toMatchObject({ + outcome: "malformed_evidence", + terminal: true, + reason: "tester transaction failure evidence contained mismatched tx hashes", + txHashes: [], + }); + }); + it("classifies serialized tester post-broadcast unresolved failures", () => { const result = { ...commandResult("tester", JSON.stringify({ @@ -3015,6 +3141,55 @@ describe("deterministic incident handling", () => { }); }); + it("counts matching top-level and nested transaction hashes once in summaries", async () => { + const writes = new Map(); + const args = parseArgs([ + "--bot-config", "config/bot-testnet.json", + "--tester-config", "config/tester-testnet.json", + "--out-dir", "logs/live-supervisor/dedup-tx-hash-summary-test", + "--scenario", "bot-only", + "--stop-after-tx-count", "1", + "--max-cycles", "1", + ]); + const plan = resolvePlan(args, "/repo", { spawnSyncCommand: ignoredChecker(true) }); + + const exitCode = await supervise(args, plan, { + skipBuiltRuntimeCheck: true, + spawnCommand: ((_command: string, commandArgs: string[]) => isPreflightCommand(commandArgs) ? fakeSuccessfulPreflightChild() : fakeChild([ + JSON.stringify(botEvent("bot.transaction.built", { + actions: { collectedOrders: 0, completedDeposits: 0, matchedOrders: 1, deposits: 0, withdrawalRequests: 0, withdrawals: 0 }, + })), + JSON.stringify(botEvent("bot.transaction.committed", { + txHash: txHash("5c"), + error: { txHash: txHash("5c") }, + })), + ].join("\n"))) as never, + spawnSyncCommand: ignoredChecker(true) as never, + stat: missingStat, + mkdir: () => Promise.resolve(undefined), + appendFile: (path, text) => { + const key = pathToString(path); + writes.set(key, `${writes.get(key) ?? ""}${textToString(text)}`); + return Promise.resolve(); + }, + writeFile: (path, text) => { + writes.set(pathToString(path), textToString(text)); + return Promise.resolve(); + }, + }); + + expect(exitCode).toBe(0); + const summary = jsonArtifact(writes, "/repo/logs/live-supervisor/dedup-tx-hash-summary-test/summary.json"); + expect(summary).toMatchObject({ + stopped: "stop_after_tx_count", + txCreatingTxHashCount: 1, + txCreatingOutcomeCount: 1, + }); + expect(recordAt(summary["txHashesByOutcome"], "summary tx hashes")).toEqual({ + bot_match_committed: [txHash("5c")], + }); + }); + }); function commandResult(actor: "bot" | "tester" | "preflight", stdout: string): CommandResult { diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index 95c2c7a..50e932e 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -541,10 +541,9 @@ export function classifyActorResult( const evidence = actor === "preflight" ? parsePreflightEvidence(result.stdout) : parseJsonEvidence(result.stdout); - const txHashes = extractTxHashes(evidence.records); const base = { actor, - txHashes, + txHashes: [], evidence: { recordsAccepted: evidence.records.length, ignoredLineCount: evidence.ignoredLines.length, @@ -556,10 +555,11 @@ export function classifyActorResult( stderrTruncated: result.stderrTruncated, }, }; + const genericBase = { ...base, txHashes: extractTxHashes(evidence.records) ?? [] }; if (result.timedOut) { return { - ...base, + ...genericBase, outcome: "command_timeout", terminal: true, reason: "supervisor command timeout expired", @@ -567,7 +567,7 @@ export function classifyActorResult( } if (result.spawnError !== undefined) { return { - ...base, + ...genericBase, outcome: "nonzero_exit", terminal: true, reason: `${actor} failed to spawn: ${result.spawnError}`, @@ -575,7 +575,7 @@ export function classifyActorResult( } if (result.stdoutTruncated || result.stderrTruncated) { return { - ...base, + ...genericBase, outcome: "malformed_evidence", terminal: true, reason: result.stdoutTruncated @@ -585,7 +585,7 @@ export function classifyActorResult( } if (evidence.malformedLines.length > 0) { return { - ...base, + ...genericBase, outcome: "malformed_evidence", terminal: true, reason: "stdout contained malformed JSON evidence", @@ -1312,26 +1312,39 @@ function classifyBotResult( const outcome = stringField(failed, "outcome"); const phase = stringField(failed, "phase"); if (outcome === "timeout_after_broadcast" || outcome === "post_broadcast_unresolved" || outcome === "terminal_rejection") { + const txHashes = extractTxHashes([failed]); + if (txHashes === undefined) { + return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot post-broadcast transaction failure evidence contained mismatched tx hashes", publicState }; + } if (!hasValidTxHash(failed)) { return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot post-broadcast transaction failure evidence did not include a valid tx hash", publicState }; } if (outcome === "timeout_after_broadcast") { - return { ...base, outcome: "confirmation_timeout", terminal: true, reason: "bot tx confirmation timed out", publicState }; + return { ...base, txHashes, outcome: "confirmation_timeout", terminal: true, reason: "bot tx confirmation timed out", publicState }; } if (outcome === "post_broadcast_unresolved") { - return { ...base, outcome: "post_broadcast_unresolved", terminal: true, reason: "bot tx remained unresolved after broadcast", publicState }; + return { ...base, txHashes, outcome: "post_broadcast_unresolved", terminal: true, reason: "bot tx remained unresolved after broadcast", publicState }; } - return { ...base, outcome: "terminal_chain_rejection", terminal: true, reason: "bot tx reached terminal chain rejection", publicState }; + return { ...base, txHashes, outcome: "terminal_chain_rejection", terminal: true, reason: "bot tx reached terminal chain rejection", publicState }; } if (phase === "pre_broadcast" && (failed["retryable"] !== true || failed["terminal"] !== false)) { - return { ...base, outcome: "unknown", terminal: true, reason: "bot pre-broadcast transaction failure", publicState }; + const txHashes = extractTxHashes([failed]); + if (txHashes === undefined) { + return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot pre-broadcast transaction failure evidence contained mismatched tx hashes", publicState }; + } + return { ...base, txHashes, outcome: "unknown", terminal: true, reason: "bot pre-broadcast transaction failure", publicState }; } } const skip = lastRecordOfType(botRecords, "bot.decision.skipped"); if (skip !== undefined && stringField(skip, "reason") === "capital_below_minimum") { + const txHashes = extractTxHashes([skip]); + if (txHashes === undefined) { + return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot skip evidence contained mismatched tx hashes", publicState }; + } return { ...base, + txHashes, outcome: "low_capital_stop", terminal: true, reason: "bot reported capital_below_minimum", @@ -1364,17 +1377,22 @@ function classifyBotResult( if (lifecycle?.type === "bot.transaction.committed") { const committed = lifecycle.record; + const txHashes = extractTxHashes([committed]); + if (txHashes === undefined) { + return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot committed transaction evidence contained mismatched tx hashes", publicState }; + } if (!hasValidTxHash(committed)) { return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot committed transaction evidence did not include a valid tx hash", publicState }; } const actions = latestBotActions(botRecords, numberField(committed, "iterationId")); if (actions === undefined) { - return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot committed transaction evidence did not include matching built action evidence", publicState }; + return { ...base, txHashes, outcome: "malformed_evidence", terminal: true, reason: "bot committed transaction evidence did not include matching built action evidence", publicState }; } const outcome = classifyBotCommittedActions(actions); if (outcome === "unknown") { return { ...base, + txHashes, outcome, terminal: true, reason: "bot committed transaction evidence did not include classifiable action evidence", @@ -1384,6 +1402,7 @@ function classifyBotResult( } return { ...base, + txHashes, outcome, terminal: false, reason: "bot transaction committed according to app evidence", @@ -1395,9 +1414,14 @@ function classifyBotResult( if (skip !== undefined) { const reason = stringField(skip, "reason") ?? "unknown"; const actions = actionCounts(skip["actions"]); + const txHashes = extractTxHashes([skip]); + if (txHashes === undefined) { + return { ...base, outcome: "malformed_evidence", terminal: true, reason: "bot skip evidence contained mismatched tx hashes", publicState }; + } if (reason === "post_tx_ckb_reserve") { return { ...base, + txHashes, outcome: "bot_reserve_skip", terminal: false, reason: "bot skipped to preserve CKB reserve", @@ -1408,6 +1432,7 @@ function classifyBotResult( } return { ...base, + txHashes, outcome: "bot_no_action_skip", terminal: false, reason: `bot skipped: ${reason}`, @@ -1462,41 +1487,50 @@ function classifyTesterResult( const skip = recordField(latest, "skip"); if (skip !== undefined) { const reason = stringField(skip, "reason") ?? "unknown"; + const txHashes = extractTxHashes([latest]); + if (txHashes === undefined) { + return { ...base, outcome: "malformed_evidence", terminal: true, reason: "tester skip evidence contained mismatched tx hashes" }; + } if (reason === "fresh-matchable-order") { - return { ...base, outcome: "tester_fresh_order_skip", terminal: false, reason: "tester skipped fresh matchable order", skipReason: reason }; + return { ...base, txHashes, outcome: "tester_fresh_order_skip", terminal: false, reason: "tester skipped fresh matchable order", skipReason: reason }; } if (reason === "matchable-order-transaction-missing") { - return { ...base, outcome: "tester_fresh_order_skip", terminal: false, reason: "tester skipped because matchable order transaction was not readable yet", skipReason: reason }; + return { ...base, txHashes, outcome: "tester_fresh_order_skip", terminal: false, reason: "tester skipped because matchable order transaction was not readable yet", skipReason: reason }; } if (reason === "sampled-amount-too-small") { - return { ...base, outcome: "tester_sampled_too_small_skip", terminal: false, reason: "tester sampled amount too small", skipReason: reason }; + return { ...base, txHashes, outcome: "tester_sampled_too_small_skip", terminal: false, reason: "tester sampled amount too small", skipReason: reason }; } if (reason === "estimated-conversion-too-small") { - return { ...base, outcome: "tester_estimated_too_small_skip", terminal: false, reason: "tester estimate converted amount too small", skipReason: reason }; + return { ...base, txHashes, outcome: "tester_estimated_too_small_skip", terminal: false, reason: "tester estimate converted amount too small", skipReason: reason }; } if (reason === "post-tx-ckb-reserve") { - return { ...base, outcome: "tester_reserve_skip", terminal: false, reason: "tester skipped to preserve CKB reserve", skipReason: reason }; + return { ...base, txHashes, outcome: "tester_reserve_skip", terminal: false, reason: "tester skipped to preserve CKB reserve", skipReason: reason }; } - return { ...base, outcome: "unknown", terminal: true, reason: `tester skip reason is not classified: ${reason}`, skipReason: reason }; + return { ...base, txHashes, outcome: "unknown", terminal: true, reason: `tester skip reason is not classified: ${reason}`, skipReason: reason }; } if ("txHash" in latest) { + const txHashes = extractTxHashes([latest]); + if (txHashes === undefined) { + return { ...base, outcome: "malformed_evidence", terminal: true, reason: "tester committed transaction evidence contained mismatched tx hashes" }; + } if (!hasValidTxHash(latest)) { return { ...base, outcome: "malformed_evidence", terminal: true, reason: "tester committed transaction evidence did not include a valid tx hash" }; } const actions = recordField(latest, "actions"); const expectationFailure = validateTesterEvidenceExpectation(actions, expectation); if (expectationFailure !== undefined) { - return { ...base, outcome: "tester_deterministic_pre_broadcast_error", terminal: true, reason: expectationFailure }; + return { ...base, txHashes, outcome: "tester_deterministic_pre_broadcast_error", terminal: true, reason: expectationFailure }; } const testerOrder = testerOrderEvidence(actions); const conversionKind = stringField(recordField(actions ?? {}, "conversion"), "kind"); if (testerOrder === undefined && conversionKind === undefined) { - return { ...base, outcome: "malformed_evidence", terminal: true, reason: "tester committed transaction evidence did not include action evidence" }; + return { ...base, txHashes, outcome: "malformed_evidence", terminal: true, reason: "tester committed transaction evidence did not include action evidence" }; } if (conversionKind !== undefined) { return { ...base, + txHashes, outcome: "tester_conversion_created", terminal: false, reason: "tester created a direct conversion transaction", @@ -1505,6 +1539,7 @@ function classifyTesterResult( } return { ...base, + txHashes, outcome: "tester_order_created", terminal: false, reason: "tester created an order transaction", @@ -2071,20 +2106,32 @@ function emptyActions(): ActionCounts { }; } -function extractTxHashes(records: Record[]): string[] { +function extractTxHashes(records: Record[]): string[] | undefined { const hashes = new Set(); for (const record of records) { - addValidTxHash(hashes, stringField(record, "txHash")); - addValidTxHash(hashes, stringField(recordField(record, "error"), "txHash")); - addValidTxHash(hashes, stringField(recordField(record, "skip"), "txHash")); + const recordHashes = validTxHashes([ + stringField(record, "txHash"), + stringField(recordField(record, "error"), "txHash"), + stringField(recordField(record, "skip"), "txHash"), + ]); + if (recordHashes.length > 1) { + return undefined; + } + for (const hash of recordHashes) { + hashes.add(hash); + } } return [...hashes]; } -function addValidTxHash(hashes: Set, value: unknown): void { - if (typeof value === "string" && TX_HASH_PATTERN.test(value)) { - hashes.add(value); +function validTxHashes(values: unknown[]): string[] { + const hashes = new Set(); + for (const value of values) { + if (typeof value === "string" && TX_HASH_PATTERN.test(value)) { + hashes.add(value); + } } + return [...hashes]; } function hasValidTxHash(record: Record): boolean { @@ -2092,10 +2139,6 @@ function hasValidTxHash(record: Record): boolean { return typeof txHash === "string" && TX_HASH_PATTERN.test(txHash); } -function hasValidTxHashInRecordOrError(record: Record): boolean { - return hasValidTxHash(record) || hasValidTxHash(recordField(record, "error") ?? {}); -} - function minimalProcessEnv(env: NodeJS.ProcessEnv): Record { return Object.fromEntries(["PATH", "HOME", "LANG", "LC_ALL", "TERM"].flatMap((key) => { const value = env[key]; @@ -2111,23 +2154,27 @@ function liveActorEnv(extra: Record): Record { }; } -function classifyTesterTransactionFailure(value: unknown, record: Record): Pick | undefined { +function classifyTesterTransactionFailure(value: unknown, record: Record): Pick | undefined { if (!isRecord(value)) { return undefined; } if (stringField(value, "name") !== "TransactionConfirmationError") { return undefined; } - if (!hasValidTxHashInRecordOrError(record)) { - return { outcome: "malformed_evidence", terminal: true, reason: "tester transaction failure evidence did not include a valid tx hash" }; + const txHashes = extractTxHashes([record]); + if (txHashes === undefined) { + return { outcome: "malformed_evidence", terminal: true, reason: "tester transaction failure evidence contained mismatched tx hashes", txHashes: [] }; + } + if (txHashes.length === 0) { + return { outcome: "malformed_evidence", terminal: true, reason: "tester transaction failure evidence did not include a valid tx hash", txHashes: [] }; } if (value["isTimeout"] === false) { - return { outcome: "terminal_chain_rejection", terminal: true, reason: "tester tx reached terminal chain rejection" }; + return { outcome: "terminal_chain_rejection", terminal: true, reason: "tester tx reached terminal chain rejection", txHashes }; } if (value["isTimeout"] === true && "cause" in value) { - return { outcome: "post_broadcast_unresolved", terminal: true, reason: "tester tx remained unresolved after broadcast" }; + return { outcome: "post_broadcast_unresolved", terminal: true, reason: "tester tx remained unresolved after broadcast", txHashes }; } - return { outcome: "confirmation_timeout", terminal: true, reason: "tester transaction confirmation timed out" }; + return { outcome: "confirmation_timeout", terminal: true, reason: "tester transaction confirmation timed out", txHashes }; } function isTesterFundingError(value: unknown): boolean { From dd9b4609c1587de6086fb5be0714d971953f915a Mon Sep 17 00:00:00 2001 From: phroi <90913182+phroi@users.noreply.github.com> Date: Fri, 29 May 2026 14:38:24 +0000 Subject: [PATCH 2/3] fix(supervisor): surface loop inspection stops --- apps/supervisor/README.md | 6 +- scripts/ickb-supervisor-dynamic-loop.mjs | 61 ++++++++++++++- scripts/ickb-supervisor-dynamic-loop.test.mjs | 77 +++++++++++++++++-- scripts/ickb-supervisor-loop.mjs | 54 ++++++++++++- scripts/ickb-supervisor-loop.test.mjs | 49 ++++++++++-- 5 files changed, 225 insertions(+), 22 deletions(-) diff --git a/apps/supervisor/README.md b/apps/supervisor/README.md index 326a6fd..020e29c 100644 --- a/apps/supervisor/README.md +++ b/apps/supervisor/README.md @@ -81,11 +81,13 @@ The KISS watcher script runs one deterministic supervisor invocation per child o node scripts/ickb-supervisor-loop.mjs --max-runs 1 --stable-limit 2 --backoff-seconds 0 -- --scenario standard-cycle --max-cycles 1 ``` -Loop-owned options go before `--`; supervisor options go after `--`. If using `pnpm live:supervisor:loop`, keep loop-owned options before the first `--` so they are not passed through to the supervisor. The loop stops on supervisor nonzero exit, incident artifacts listed in `summary.json`, tx-creating outcomes or tx hashes for tx-creating outcomes, a new outcome after the first run, repeated no-progress signatures, or `--max-runs`. +Loop-owned options go before `--`; supervisor options go after `--`. If using `pnpm live:supervisor:loop`, keep loop-owned options before the first `--` so they are not passed through to the supervisor. The loop stops on supervisor nonzero exit, incident artifacts listed in `summary.json`, tx-creating outcomes or tx hashes for tx-creating outcomes, a new outcome after the first run, repeated no-progress signatures, or `--max-runs`. `-- --help` and `-- -h` are child help passthroughs: the delegated help is printed and the wrapper exits with the child status. The external loop also has a loop-owned `--child-timeout-seconds` guard for the supervisor child process. Keep it long enough for the whole delegated supervisor run, including actor preflights and actor commands, not just one `--command-timeout-seconds` window. The dynamic loop defaults this guard to the supervisor-loop default so the supervisor keeps ownership of killing funded actor process groups on command timeout. -For continuous tester-bot matching, use `node scripts/ickb-supervisor-dynamic-loop.mjs` or `pnpm live:supervisor:dynamic-loop`. This remains outside `apps/supervisor`: it reads tester preflight balance summaries, chooses a currently fundable tester scenario, and delegates each bounded chunk to `scripts/ickb-supervisor-loop.mjs`. When `--target-outcome tester_fresh_order_skip` is passed through, supervisor auto-planning can choose `tester-fresh-skip-two-pass`; the dynamic loop itself only chooses fundable tester stimuli. +For continuous tester-bot matching, use `node scripts/ickb-supervisor-dynamic-loop.mjs` or `pnpm live:supervisor:dynamic-loop`. This remains outside `apps/supervisor`: it reads tester preflight balance summaries, chooses a currently fundable tester scenario, and delegates each bounded chunk to `scripts/ickb-supervisor-loop.mjs`. When `--target-outcome tester_fresh_order_skip` is passed through, supervisor auto-planning can choose `tester-fresh-skip-two-pass`; the dynamic loop itself only chooses fundable tester stimuli. The dynamic loop also treats `-- --help` and `-- -h` as child help passthroughs and exits with the delegated status. + +Loop and dynamic-loop exit codes are operator-visible control flow: tx/new-outcome stops exit `0`, incidents exit `2`, `max_runs` and `stable_no_progress` inspection stops exit `3`, and child nonzero statuses are preserved. Dynamic loop sessions are live-validation artifacts, separate from production bot-only logs. They default to ignored `log/validation/dynamic-