feat(recovery,context): tier-2 intent-holder arbiter + remaining-delta pinning + surfaced signal events

- recovery: two-tier repair ladder — owner then arbiter (recovery stage recast
  as intent-holder, holds initial intent, reconciles cross-file contract disputes)
  with independent INTENT_ROUTE_BUDGET; RecoveryRoutingTest coverage
- context: remaining-delta checklist pinning (RemainingDelta{Pinning,Entry}Test)
- surface write-only events (session name, quality signals) into stats/browse
- parseToolArguments lenient-Json fallback for bare-key drift
- tools: ChildProcess seam
This commit is contained in:
2026-07-11 23:56:52 +04:00
parent 3d5e05c1fb
commit 15248cae8a
55 changed files with 1932 additions and 231 deletions
@@ -0,0 +1,44 @@
package com.correx.core.tools.process
import java.io.File
/**
* Single construction point for child processes spawned on behalf of a run — both model-authored
* shell (ShellTool) and operator-trusted gate commands (static-analysis runner) go through here.
*
* Every child is configured for *unattended* execution, closing the two ways a subprocess hangs a
* headless run:
* 1. **stdin from /dev/null** — a tool that still prompts reads EOF and fails fast instead of
* blocking on a dangling pipe until the caller's timeout kills it.
* 2. **non-interactive env overlay** — package managers / scaffolders (`npm create`, `npm init`,
* …) honour `CI`, `npm_config_yes`, etc. and take defaults rather than prompting at all.
*
* Output redirection is deliberately NOT set here: callers differ (ShellTool drains stdout/stderr
* separately; the gate runner merges them via `redirectErrorStream`). They also keep their own
* timeout backstop — this only builds the [ProcessBuilder].
*/
object ChildProcess {
private val DEV_NULL = File("/dev/null")
// Overlaid on the inherited environment. Forces the common Node toolchain into non-interactive
// mode so `npm create`/`npm init` and friends never block on a stdin prompt.
private val NON_INTERACTIVE_ENV = mapOf(
"CI" to "true",
"npm_config_yes" to "true",
"NPM_CONFIG_YES" to "true",
"ADBLOCK" to "1",
"DISABLE_OPENCOLLECTIVE" to "1",
)
/**
* A [ProcessBuilder] for [command] in [workingDir] (JVM cwd when null), with the non-interactive
* env overlay and stdin redirected from /dev/null. The caller sets any output redirection and
* owns the timeout.
*/
fun builder(command: List<String>, workingDir: File? = null): ProcessBuilder =
ProcessBuilder(command).apply {
workingDir?.let { directory(it) }
environment().putAll(NON_INTERACTIVE_ENV)
redirectInput(ProcessBuilder.Redirect.from(DEV_NULL))
}
}