Add opt-in Integer autoCompactWindow to FleetConfig.Profile (last field,
null/unset = today's behaviour). Validated at config load to [100000,
1000000] — the band Claude Code's own --autocompact flag accepts.
Claude Code: appends --autocompact <window> to argv (mirrors --model),
so it survives the ccs <profile> wrapper.
opencode: has no absolute compact-at-N knob (only compaction.auto/prune/
reserved/tail_turns/preserve_recent_tokens), so the window is applied as
the resolved model's own limit.context (+ a required limit.output:16384
default) in the generated opencode.json, merged via get-or-create nodes
so it does not clobber a custom-provider block. Only applies when model:
resolves to "provider/model"; otherwise logs a WARN naming the profile
rather than silently doing nothing.
Docs added to fleetd.example.yaml explaining the cross-backend semantics
difference (compacts AT the window vs. WITHIN it).
The overlay pinned project_path to the worktree root. For a repo whose Maven
module is a subdir (this repo's pom is in `bridged/`, not at the root), opening
the root imports no module and every ide_* call resolves nothing. Pin and open
the module dir instead.
Two new opt-in per-Profile keys, both read only when ideMcpUrl is set:
- ideProjectDir: repo-relative module dir the IDE opens and the overlay pins;
blank keeps the old worktree-root behaviour.
- ideOpenCommand: host command that opens that dir in the IDE at spawn, with
{dir} substituted and run through /bin/sh -c so env (e.g. DISPLAY) can be set
inline. Best-effort and non-fatal — a failure never fails the spawn. Blank
keeps the manual-open behaviour. No close half yet (deferred).
Shared helpers PeerLauncher.ideProjectPath / openInIde back both launchers.
The two Profile fields ride a back-compat constructor, so every existing call
site and YAML compiles and behaves unchanged.
Tests: overlay content pins the module dir when ideProjectDir is set;
ideProjectPath resolution; openInIde no-op on a blank command. 918 tests green.
Adds `ideMcpUrl` to FleetConfig.Profile (default off). When set, the
Claude Code launcher mounts the IDE Index MCP as a second inline
--mcp-config server named `intellij`, and appends an IDE charter that
pins every ide_* call to the member's own worktree (spec.cwd()). The
charter order is role -> ide -> reply, one --append-system-prompt-file,
reply last (CB-618). The mount gate now fires on ideMcpUrl alone, not
only mcpUrl. ConfigRef treats an ideMcpUrl change as deferred, like the
other launch flags.
Never touches .mcp.json or CLAUDE.md — the mount and the rule arrive as
launch flags, so a project's own config is untouched.
Not yet done (see fleetd #162): the bridged-owned IDE lifecycle
(open on provision, close before worktree removal), and the opencode
adapter (separate ticket). fleetd.example.yaml documents ideMcpUrl and
fixes the stale parityOverlay default.
911 tests green.
#151: Broker gains uriEnv beside uri, taking the AMQP URI from an env var so
the password stays out of fleetd.yaml (same pattern as auth.tokenEnv). uriEnv
wins when set; isConfigured() treats a uriEnv naming an unset/blank variable
as unconfigured. A configured uriEnv is added to the startup required-secrets
report. Never logs the resolved URI (it carries the password).
#152: AmqpReplyInbox.open throwing at boot no longer stops the daemon. The
selection at the call site catches the failure and falls back to the in-memory
inbox for the process lifetime, warning loudly that durable cross-restart
delivery is off and logging the failed URI with credentials stripped.
Six review findings, from the peer lead `vms` and a reviewer worker. The first
one is a real defect that would have shipped as a dead control.
1. The scrub only ran in a login shell. It lived in the generated `.zlogin`,
and zsh reads `.zlogin` only for a login shell. herdr does not open the same
kind of shell everywhere: measured on herdr 0.8.0, a macOS pane runs `-zsh`
(login) while a Linux pane runs a plain `/usr/bin/zsh`. So on the vhost this
was being built for, `.zlogin` never ran and every member kept the whole
secret store, in silence.
The scrub body now lives in a generated `scrub.zsh` that BOTH `.zshrc` and
`.zlogin` source, each after sourcing its own `$HOME` counterpart. Linux
runs the first, macOS runs both, and the second pass is not merely harmless
-- it re-scrubs anything the operator's `~/.zlogin` exported after `~/.zshrc`
had finished. Re-running is idempotent.
2. `INFRASTRUCTURE_PASSTHROUGH` listed names that are not infrastructure:
ANTHROPIC_AUTH_TOKEN, GITEA_TOKEN, GITEA_HOST, ANTHROPIC_BASE_URL,
ANTHROPIC_MODEL, CLAUDE_CONFIG_DIR, OPENCODE_CONFIG, BRIDGED_MEMBER. I read
each injection point and confirmed every one of them reaches `launch.env()`
only when actually injected, so `allowed.addAll(launch.env().keySet())`
already covers the legitimate case. As static entries they were pure leak
surface: a host that happened to export ANTHROPIC_AUTH_TOKEN would have had
it passed straight through.
3. A missing `scrub-report.txt` at teardown was logged at debug. The report is
the only evidence the scrub ran at all. Its absence has an innocent reading
and a serious one, and we cannot tell them apart from the daemon -- so it is
now a WARN that says exactly that. Logging it at debug is how a control that
quietly stopped working stays unnoticed.
4. Nothing tested that the control was wired in. Deleting the single
`applyEnvironmentAllowListPolicy(cfg, launch)` line left all 896 tests green
while turning the feature completely off -- the CB-586/CB-611 shape again.
`HerdrPeerLauncherAllowListWiringTest` starts a real spawn and asserts on the
env that reached herdr. Mutation-checked: unwiring that line fails it.
5. `EnvAllowListScrubTest` now also runs `zsh -i` with no `-l`, which is the
Linux pane shape, so finding 1 is tested from a Mac. Mutation-checked:
putting the scrub back in `.zlogin` alone fails that test alone, while the
login-shell test still passes -- which is exactly the blind spot that let
the bug through.
6. Two ZDOTDIR leaks closed. A failed spawn has no pane id, so its directory
was never keyed for teardown; it is now removed on the way out. And
`deleteOnExit` covers a clean shutdown and nothing else, so `generate` now
reaps sibling directories older than 24h left by a killed daemon.
Also: `policy:` is lowercased with Locale.ROOT, and the `.zlogin`-only claim is
corrected in fleetd.example.yaml, FleetConfig and HerdrPeerLauncher.
901 tests, 0 failures, `mvn clean install` green.
Move member environment control out of the pane-creation env overlay
(defeated by any file the login shell sources) into a per-spawn ZDOTDIR
directory whose .zlogin runs LAST, after the operator's whole chain, and
blanks every exported variable not on an allow-list DERIVED from what the
launcher itself injects (profiles' tokenEnv/gitTokenEnv/gitHostEnv/env
keys + an infrastructure set) — never hand-typed.
- memberCredentials.policy: allow-list (deny-by-default/deny-list stay
default and unchanged); known:/allow: become reporting only under it.
- memberCredentials.sshAuthSock knob, blocked by default; allowing it is
an explicit decision (operator ssh-agent handle).
- Non-zsh login shell: loud WARN, protection off, fallback to the old
enumerated-name overlay.
- Scrub writes an 'allowed N of M' denominator report, read at teardown;
credential-shaped blanked names go to WARN (names only, never values).
- Equality test against a real login zsh from a clean parent: surviving
non-empty exports EQUAL baseline ∩ derived allow-list.