Skip to content
  • Garry Tan's avatar
    c7ae6320
    v1.58.1.0 feat: hermetic local E2E + Conductor prose AskUserQuestion (#2004) · c7ae6320
    Garry Tan authored
    
    
    * feat: add shared call-time isConductor() helper
    
    Single source of truth for Conductor host detection in TS consumers
    (CONDUCTOR_WORKSPACE_PATH / CONDUCTOR_PORT). Reads the passed env at
    call time, not a module-load snapshot, so unit tests can pin the env
    inline without Bun --preload (esm-hoist-breaks-env-pin-bootstrap).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: harden question-preference-hook harness against ambient Conductor env
    
    runHook copied all of process.env into the hook subprocess, so running the
    suite inside Conductor (CONDUCTOR_WORKSPACE_PATH/PORT set) would leak those
    markers. Strip them so the existing cases deterministically characterize
    NON-Conductor behavior before the Conductor branch lands. Baseline: 15 pass.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: PreToolUse hook denies AskUserQuestion in Conductor, redirects to prose
    
    Conductor disables native AskUserQuestion and routes through a flaky MCP
    variant that returns '[Tool result missing due to internal error]'. The
    hook now denies any AUQ call in a Conductor session and instructs the model
    to render a prose decision brief instead (transport avoidance, not preference
    enforcement) — firing for one-way doors too, with a typed-confirmation
    requirement for destructive paths.
    
    Precedence: never-ask auto-decide still wins (user already settled those);
    Conductor prose is the fallback for everything else; non-Conductor behavior
    is byte-for-byte unchanged. Restructured the per-question loop to compute
    eligibility without early-returning so the Conductor branch can run as the
    fallback while preserving memoryContext on every exit.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: Conductor renders AskUserQuestion decisions as prose by default
    
    In Conductor, native AskUserQuestion is disabled and the MCP variant is
    flaky, so skills now render every decision as a plain-text prose brief the
    user answers by typing a letter — proactively, not as a failure reaction.
    
    - Preamble emits CONDUCTOR_SESSION, gated on != headless so eval/CI inside
      Conductor still BLOCKs instead of rendering prose to nobody.
    - AskUserQuestion Format gains a Conductor-default-prose rule (auto-decide
      preferences still apply first; prose decisions log via gstack-question-log
      since PostToolUse never fires), a one-way/destructive typed-confirmation
      rule, and a typed-reply continuation protocol for split chains.
    - Regenerated all SKILL.md + ship golden fixtures; bumped affected carve
      skeleton caps to absorb the always-loaded additions.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: deploy the Conductor AskUserQuestion hook (setup + upgrade migration)
    
    The PreToolUse hook only delivers its Conductor-prose guarantee if it's
    installed, but setup skips hook registration in non-interactive (conductor/CI)
    setups. Two fixes so layer 3 actually deploys:
    
    - setup: treat a Conductor workspace as an implicit opt-in for the PreToolUse
      hook on the silent fall-through (never overriding an explicit opt-out).
    - migration v1.58.0.0: re-register the hook for existing Conductor installs on
      /gstack-upgrade, idempotent and respecting plan_tune_hooks=no.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: E2E for Conductor prose + fix auto-decide-preserved GSTACK_HOME bug
    
    - New skill-e2e-conductor-prose (periodic): Conductor env + plan-eng-review
      surfaces a prose decision brief, not a silent skip. Header documents this is
      end-to-end behavior coverage; the deterministic Conductor guard is the
      question-preference-hook unit test (the PTY harness can't register the MCP
      variant — Codex #10).
    - Fix the pre-existing bug in auto-decide-preserved: it seeded the never-ask
      preference under GSTACK_HOME=tmpHome but never passed GSTACK_HOME into the
      PTY run, so the spawned claude read the real ~/.gstack and the preference
      was inert (Codex #9). Now passes GSTACK_HOME + CONDUCTOR_WORKSPACE_PATH to
      prove auto-decide still wins over the Conductor prose redirect.
    - Register both in touchfiles (periodic tier).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * v1.58.0.0 feat: Conductor renders AskUserQuestion decisions as prose
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: strip ambient Conductor env in memory-cache-injection hook harness
    
    Same dev-in-Conductor leak fixed for question-preference-hook: this suite's
    runHook copies process.env, so running it inside Conductor flipped the
    defer-path memoryContext assertions into the [conductor] prose deny. Strip
    CONDUCTOR_* so the cases characterize non-Conductor behavior. (CI is headless,
    so this only bit local Conductor runs.)
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: gstack-detach — run agent eval/bench jobs in their own session
    
    Long agent-run jobs (30-60 min evals, benchmarks) die when the harness sends
    SIGTERM to a background task's process group on turn boundaries / monitor
    stops / interruptions (observed: 'script test:gate terminated by signal
    SIGTERM'). gstack-detach runs the command in a fresh session (python3
    os.setsid, or setsid on Linux, nohup fallback) so a group SIGTERM can't reach
    it, and wraps it in caffeinate -i on macOS so idle-sleep can't kill it either.
    Returns immediately; caller polls the logfile. Secrets stay in env, never argv.
    
    The guard test pins the contract: the command runs in a different process
    group than the caller and outlives the launching shell.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: eval:bg* scripts — detached eval runs for agents
    
    Agent-facing convenience scripts that launch the eval suites through
    gstack-detach so a harness SIGTERM can't kill a long run. eval:bg (diff-based),
    eval:bg:all, eval:bg:gate, eval:bg:periodic — each returns immediately and
    streams to /tmp/gstack-evals.log for polling. The plain test:evals / test:e2e
    scripts stay foreground for humans.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * docs: CLAUDE.md — agents must run long evals via gstack-detach
    
    Codifies the detached-execution default: agent-launched eval/benchmark runs go
    through bin/gstack-detach (or the eval:bg* scripts) so a harness SIGTERM or
    macOS idle-sleep can't kill a 30-60 min run, then poll the log with a
    death-aware watcher. Humans keep foreground scripts.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: harden gstack-detach against all four eval-infra killers
    
    The basic bash detach fixed SIGTERM but a real run on a shared dev box hit
    three more killers: cross-worktree API saturation (15-way concurrency x a
    sibling worktree mass-timed-out the suite), a silent hang (periodic bun died
    with no exit marker), and shared-/tmp log contamination (a concurrent
    worktree's agent output bled into the log). Rewrite as a portable python3 tool
    that bakes in all four fixes:
    
    - fork + setsid: SIGTERM-proof (own session, survives harness polite-quit)
    - caffeinate -i on macOS: no idle-sleep death
    - --lock NAME (fcntl, machine-wide): concurrent worktrees SERIALIZE instead of
      saturating the shared model API
    - run-scoped default log (~/.gstack-dev/eval-runs/<label>-<slug>-<branch>-<ts>-<pid>):
      no cross-worktree collision/contamination
    - --timeout watchdog + a guaranteed '### gstack-detach EXIT=<code> ###' sentinel
      on every terminal path: no silent hang, finished-vs-died always detectable
    
    Guard test pins all four: detached pgid differs + outlives launcher, run-scoped
    log path, watchdog EXIT=timeout, and lock serialization (second run WAITS).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: eval:bg* use run-scoped logs + machine lock + watchdog
    
    Drop the shared /tmp/gstack-evals.log path (the cross-worktree collision that
    contaminated a live run) for gstack-detach's run-scoped default, and add the
    machine-wide gstack-evals lock (concurrent worktrees serialize, no API
    saturation) plus per-tier watchdog timeouts (60/90/120 min). Each eval:bg*
    prints its run-scoped log path to poll.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * docs: wire detached-eval guidance into /ship + correct CLAUDE.md flags
    
    - /ship eval step (sections/tests.md): long eval suites launch via gstack-detach
      (own session, machine lock, EXIT sentinel) so a turn boundary can't kill a
      30+ min run mid-ship — the exact failure observed during this branch's ship.
    - CLAUDE.md: correct the now-stale /tmp reference; document the --lock (serialize
      worktrees, no API saturation), --timeout watchdog, run-scoped log, and the
      guaranteed EXIT sentinel the poller breaks on.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * refactor: extract pure promotedEnv() from conductor-env-shim
    
    Single source of truth for GSTACK_* key promotion semantics. The ambient
    promoteConductorEnv() becomes a wrapper; behavior-preserving. Needed by the
    hermetic env builder which must not mutate process.env.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: hermetic child-env builder for E2E runners
    
    Allowlist scrub (basics/network/named-auth kept; CONDUCTOR_*, CLAUDE_*,
    GSTACK_*, MCP_*, GBRAIN_*, operator credentials dropped), per-runner
    extraAllow, overrides merge last, EVALS_HERMETIC=0 byte-identical escape
    hatch read at call time (ESM-hoist safe). Sync memoized singleton temp dirs
    (<runRoot>/.claude keeps the extractPlanFilePath contract), seeded
    .claude.json for non-interactive first run, pid-aware GC of crashed runs.
    19 free unit tests.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: session-runner spawns hermetic children + isolation canaries
    
    claude -p children now get the allowlist-scrubbed env and a gated
    --strict-mcp-config (EVALS_HERMETIC=0 restores operator env AND args).
    Two gate-tier canaries make the clean room falsifiable: hermetic-canary
    asserts env redirect + scrub + zero MCP servers + nonzero API-key cost
    from the Bash tool_result (never model prose); hermetic-sentinel plants a
    poisoned operator config (user CLAUDE.md + MCP server) and proves the
    child cannot see it. Empirically verified on claude 2.1.175: print mode
    needs no seed config (the seed serves the PTY path); the child CLI sets
    CLAUDECODE for its own tools, so that scrub is pinned in unit tests, not
    E2E. hermetic-env.ts joins GLOBAL_TOUCHFILES.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: PTY runner spawns hermetic claude sessions
    
    launchClaudePty children get the allowlist-scrubbed env, a gated
    --strict-mcp-config, and the session exposes hermeticConfigDir for
    forensics (hermetic plan files live under <dir>/plans/ and still match
    extractPlanFilePath via the /.claude dir-name contract). Seeded trust
    state covers repo-cwd sessions; the 15s trust-watcher stays as fallback.
    Verified foreground via the plan-mode-no-op gate test.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: codex/gemini runners spawn hermetic children
    
    Same allowlist scrub as the claude runners, with each provider's auth
    surface re-admitted via extraAllow (codex: OPENAI_API_KEY/CODEX_* plus
    its tempHome .codex copy; gemini: GEMINI_*/GOOGLE_* with real HOME for
    ~/.gemini auth). The gemini spawn previously inherited the full operator
    env with no env property at all.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: agent-sdk-runner spawns hermetic children via complete Options.env
    
    The historical 'env: breaks SDK auth' failure was partial-env replacement:
    Options.env replaces the child's entire environment, so objects lacking
    ANTHROPIC_API_KEY killed auth. Passing the complete hermetic env (key +
    PATH + redirected CLAUDE_CONFIG_DIR/GSTACK_HOME) works — validated live
    via query() with a Bash tool call (success, real cost, Conductor vars
    scrubbed). Per-test opts.env merges last; ambient key mutation still
    works because the builder reads process.env at call time.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: static tripwire pins hermetic wiring in all five runners
    
    Free-tier invariants: every runner builds child env via hermeticChildEnv,
    no raw ...process.env spread at any spawn site, --strict-mcp-config gated
    on isHermeticEnabled in both claude runners, and no test callsite passes
    the operator env into a runner's override parameter (scoped to runner
    calls — unit tests spawning gstack bin scripts directly are exempt).
    Mirrors the terminal-agent-pid-identity / server-embedder-terminal-port
    tripwire idiom.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: refresh codex/factory ship goldens with detached-eval block
    
    a38089aa added the gstack-detach guidance to the ship template and
    updated the claude golden; the codex and factory goldens missed the same
    16-line block. Regenerated via bun run gen:skill-docs.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * docs: hermetic local E2E is the default; retire stale SDK env warning
    
    CLAUDE.md now documents the hermetic clean room (allowlist scrub, fresh
    seeded CLAUDE_CONFIG_DIR, temp GSTACK_HOME, --strict-mcp-config),
    EVALS_HERMETIC=0 as the debug escape hatch, and replaces the 'never pass
    env: to runAgentSdkTest' rule with the verified mechanism (partial-env
    replacement was the failure; complete env is safe).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * fix: operational-learning fixture copies lib/jsonl-store.ts with the bin
    
    gstack-learnings-log imports $SCRIPT_DIR/../lib/jsonl-store.ts (hasInjection,
    v1.57.5.0) — copying only the bin scripts into the temp fixture broke the
    script with exit 1 since then. Latent because diff-based selection rarely
    runs this test; surfaced when hermetic-env.ts joined GLOBAL_TOUCHFILES and
    selected everything. Reproduced outside the hermetic env to confirm blame.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * fix: ios-qa daemon scenarios use unique pidfiles under --concurrent
    
    All scenarios shared join(workDir, 'daemon.pid') through a module-scope
    workDir binding that beforeEach reassigns mid-flight under bun --concurrent.
    First daemon claims; siblings get already_running against the test process's
    own always-alive pid and fail in milliseconds — the failure mode seen at
    15-way gate concurrency. Per-claim unique pidfiles keep the single-instance
    semantics under test.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * fix: workflow judge re-appends body-carved sections after the marker slice
    
    runWorkflowJudge appended sections/*.md before slicing startMarker..endMarker.
    That handles skills that moved their MARKERS into sections (plan-eng,
    plan-design) but not document-release, which keeps its markers in the
    skeleton and carved the workflow BODY (Steps 2-9 -> sections/release-body.md)
    AFTER the endMarker — so the slice dropped it and the judge scored
    completeness 2 ('Steps 2-9 are in an external file'). Now any carved section
    the marker window excluded is re-appended, so the judge sees the full
    workflow the agent executes. document-release: completeness 2->5, clarity
    3->4. ship/plan-ceo/plan-eng/plan-design judges unchanged (their section
    content is already inside the slice, so the head-dedup skips re-append).
    
    Pre-existing since the v1.57.0.0 carve (#1907); surfaced now because
    hermetic-env.ts is a global touchfile that selects every llm-judge test.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * harden: hermetic temp-dir GC grace window + half-seed cleanup
    
    Codex adversarial review (ship) flagged two temp-dir lifecycle edges:
    - GC deleted any dead-pid dir; PID reuse could delete a freshly-created dir
      whose original pid exited and was recycled to a live process. Now requires
      BOTH a dead pid AND mtime older than a 1h floor.
    - A seed-write failure after mkdir left an unseeded dir named with our live
      pid that this process's GC skips, leaking until exit. Now the partial dir
      is torn down before the (still loud) rethrow.
    
    Two findings left as-is by design: HOME stays allowlisted (CLAUDE_CONFIG_DIR
    wins for claude; codex/gemini need ~/.codex|~/.gemini auth; FS sandbox is
    TODOS.md:454 scope; the hermetic-sentinel canary proves config isolation),
    and PTY extraArgs --mcp-config is a deliberate caller opt-in like env overrides.
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * docs: document hermetic-by-default E2E + eval:bg detached runs in CONTRIBUTING
    
    The Testing & evals section now tells contributors that local E2E runners
    spawn children through a sealed clean room (allowlist-scrubbed env, seeded
    CLAUDE_CONFIG_DIR, temp GSTACK_HOME, --strict-mcp-config) so local signal
    matches CI, with EVALS_HERMETIC=0 as the escape hatch. The eval-tools list
    gains the eval:bg* detached-run scripts (gstack-detach: SIGTERM-proof,
    caffeinate-wrapped, machine-locked, run-scoped logs, EXIT= sentinel).
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * chore: sync package.json to 1.58.1.0
    
    The merge took main's package.json (1.58.0.0); gstack-version-bump repair
    fixed the working tree but the change was left uncommitted. Without this the
    committed tree disagrees with VERSION and CI's version-match test fails.
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * docs: regenerate diagram SKILL.md with Conductor prose preamble
    
    The diagram skill (new from main) was missing the Conductor-session prose
    AskUserQuestion blocks that gen-skill-docs propagates to every SKILL.md.
    Pure generated output; reproduced by bun run gen:skill-docs.
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: default avatarClaude Fable 5 <noreply@anthropic.com>
    c7ae6320
    v1.58.1.0 feat: hermetic local E2E + Conductor prose AskUserQuestion (#2004)
    Garry Tan authored
    
    
    * feat: add shared call-time isConductor() helper
    
    Single source of truth for Conductor host detection in TS consumers
    (CONDUCTOR_WORKSPACE_PATH / CONDUCTOR_PORT). Reads the passed env at
    call time, not a module-load snapshot, so unit tests can pin the env
    inline without Bun --preload (esm-hoist-breaks-env-pin-bootstrap).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: harden question-preference-hook harness against ambient Conductor env
    
    runHook copied all of process.env into the hook subprocess, so running the
    suite inside Conductor (CONDUCTOR_WORKSPACE_PATH/PORT set) would leak those
    markers. Strip them so the existing cases deterministically characterize
    NON-Conductor behavior before the Conductor branch lands. Baseline: 15 pass.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: PreToolUse hook denies AskUserQuestion in Conductor, redirects to prose
    
    Conductor disables native AskUserQuestion and routes through a flaky MCP
    variant that returns '[Tool result missing due to internal error]'. The
    hook now denies any AUQ call in a Conductor session and instructs the model
    to render a prose decision brief instead (transport avoidance, not preference
    enforcement) — firing for one-way doors too, with a typed-confirmation
    requirement for destructive paths.
    
    Precedence: never-ask auto-decide still wins (user already settled those);
    Conductor prose is the fallback for everything else; non-Conductor behavior
    is byte-for-byte unchanged. Restructured the per-question loop to compute
    eligibility without early-returning so the Conductor branch can run as the
    fallback while preserving memoryContext on every exit.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: Conductor renders AskUserQuestion decisions as prose by default
    
    In Conductor, native AskUserQuestion is disabled and the MCP variant is
    flaky, so skills now render every decision as a plain-text prose brief the
    user answers by typing a letter — proactively, not as a failure reaction.
    
    - Preamble emits CONDUCTOR_SESSION, gated on != headless so eval/CI inside
      Conductor still BLOCKs instead of rendering prose to nobody.
    - AskUserQuestion Format gains a Conductor-default-prose rule (auto-decide
      preferences still apply first; prose decisions log via gstack-question-log
      since PostToolUse never fires), a one-way/destructive typed-confirmation
      rule, and a typed-reply continuation protocol for split chains.
    - Regenerated all SKILL.md + ship golden fixtures; bumped affected carve
      skeleton caps to absorb the always-loaded additions.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: deploy the Conductor AskUserQuestion hook (setup + upgrade migration)
    
    The PreToolUse hook only delivers its Conductor-prose guarantee if it's
    installed, but setup skips hook registration in non-interactive (conductor/CI)
    setups. Two fixes so layer 3 actually deploys:
    
    - setup: treat a Conductor workspace as an implicit opt-in for the PreToolUse
      hook on the silent fall-through (never overriding an explicit opt-out).
    - migration v1.58.0.0: re-register the hook for existing Conductor installs on
      /gstack-upgrade, idempotent and respecting plan_tune_hooks=no.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: E2E for Conductor prose + fix auto-decide-preserved GSTACK_HOME bug
    
    - New skill-e2e-conductor-prose (periodic): Conductor env + plan-eng-review
      surfaces a prose decision brief, not a silent skip. Header documents this is
      end-to-end behavior coverage; the deterministic Conductor guard is the
      question-preference-hook unit test (the PTY harness can't register the MCP
      variant — Codex #10).
    - Fix the pre-existing bug in auto-decide-preserved: it seeded the never-ask
      preference under GSTACK_HOME=tmpHome but never passed GSTACK_HOME into the
      PTY run, so the spawned claude read the real ~/.gstack and the preference
      was inert (Codex #9). Now passes GSTACK_HOME + CONDUCTOR_WORKSPACE_PATH to
      prove auto-decide still wins over the Conductor prose redirect.
    - Register both in touchfiles (periodic tier).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * v1.58.0.0 feat: Conductor renders AskUserQuestion decisions as prose
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: strip ambient Conductor env in memory-cache-injection hook harness
    
    Same dev-in-Conductor leak fixed for question-preference-hook: this suite's
    runHook copies process.env, so running it inside Conductor flipped the
    defer-path memoryContext assertions into the [conductor] prose deny. Strip
    CONDUCTOR_* so the cases characterize non-Conductor behavior. (CI is headless,
    so this only bit local Conductor runs.)
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: gstack-detach — run agent eval/bench jobs in their own session
    
    Long agent-run jobs (30-60 min evals, benchmarks) die when the harness sends
    SIGTERM to a background task's process group on turn boundaries / monitor
    stops / interruptions (observed: 'script test:gate terminated by signal
    SIGTERM'). gstack-detach runs the command in a fresh session (python3
    os.setsid, or setsid on Linux, nohup fallback) so a group SIGTERM can't reach
    it, and wraps it in caffeinate -i on macOS so idle-sleep can't kill it either.
    Returns immediately; caller polls the logfile. Secrets stay in env, never argv.
    
    The guard test pins the contract: the command runs in a different process
    group than the caller and outlives the launching shell.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: eval:bg* scripts — detached eval runs for agents
    
    Agent-facing convenience scripts that launch the eval suites through
    gstack-detach so a harness SIGTERM can't kill a long run. eval:bg (diff-based),
    eval:bg:all, eval:bg:gate, eval:bg:periodic — each returns immediately and
    streams to /tmp/gstack-evals.log for polling. The plain test:evals / test:e2e
    scripts stay foreground for humans.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * docs: CLAUDE.md — agents must run long evals via gstack-detach
    
    Codifies the detached-execution default: agent-launched eval/benchmark runs go
    through bin/gstack-detach (or the eval:bg* scripts) so a harness SIGTERM or
    macOS idle-sleep can't kill a 30-60 min run, then poll the log with a
    death-aware watcher. Humans keep foreground scripts.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: harden gstack-detach against all four eval-infra killers
    
    The basic bash detach fixed SIGTERM but a real run on a shared dev box hit
    three more killers: cross-worktree API saturation (15-way concurrency x a
    sibling worktree mass-timed-out the suite), a silent hang (periodic bun died
    with no exit marker), and shared-/tmp log contamination (a concurrent
    worktree's agent output bled into the log). Rewrite as a portable python3 tool
    that bakes in all four fixes:
    
    - fork + setsid: SIGTERM-proof (own session, survives harness polite-quit)
    - caffeinate -i on macOS: no idle-sleep death
    - --lock NAME (fcntl, machine-wide): concurrent worktrees SERIALIZE instead of
      saturating the shared model API
    - run-scoped default log (~/.gstack-dev/eval-runs/<label>-<slug>-<branch>-<ts>-<pid>):
      no cross-worktree collision/contamination
    - --timeout watchdog + a guaranteed '### gstack-detach EXIT=<code> ###' sentinel
      on every terminal path: no silent hang, finished-vs-died always detectable
    
    Guard test pins all four: detached pgid differs + outlives launcher, run-scoped
    log path, watchdog EXIT=timeout, and lock serialization (second run WAITS).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: eval:bg* use run-scoped logs + machine lock + watchdog
    
    Drop the shared /tmp/gstack-evals.log path (the cross-worktree collision that
    contaminated a live run) for gstack-detach's run-scoped default, and add the
    machine-wide gstack-evals lock (concurrent worktrees serialize, no API
    saturation) plus per-tier watchdog timeouts (60/90/120 min). Each eval:bg*
    prints its run-scoped log path to poll.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * docs: wire detached-eval guidance into /ship + correct CLAUDE.md flags
    
    - /ship eval step (sections/tests.md): long eval suites launch via gstack-detach
      (own session, machine lock, EXIT sentinel) so a turn boundary can't kill a
      30+ min run mid-ship — the exact failure observed during this branch's ship.
    - CLAUDE.md: correct the now-stale /tmp reference; document the --lock (serialize
      worktrees, no API saturation), --timeout watchdog, run-scoped log, and the
      guaranteed EXIT sentinel the poller breaks on.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * refactor: extract pure promotedEnv() from conductor-env-shim
    
    Single source of truth for GSTACK_* key promotion semantics. The ambient
    promoteConductorEnv() becomes a wrapper; behavior-preserving. Needed by the
    hermetic env builder which must not mutate process.env.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: hermetic child-env builder for E2E runners
    
    Allowlist scrub (basics/network/named-auth kept; CONDUCTOR_*, CLAUDE_*,
    GSTACK_*, MCP_*, GBRAIN_*, operator credentials dropped), per-runner
    extraAllow, overrides merge last, EVALS_HERMETIC=0 byte-identical escape
    hatch read at call time (ESM-hoist safe). Sync memoized singleton temp dirs
    (<runRoot>/.claude keeps the extractPlanFilePath contract), seeded
    .claude.json for non-interactive first run, pid-aware GC of crashed runs.
    19 free unit tests.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: session-runner spawns hermetic children + isolation canaries
    
    claude -p children now get the allowlist-scrubbed env and a gated
    --strict-mcp-config (EVALS_HERMETIC=0 restores operator env AND args).
    Two gate-tier canaries make the clean room falsifiable: hermetic-canary
    asserts env redirect + scrub + zero MCP servers + nonzero API-key cost
    from the Bash tool_result (never model prose); hermetic-sentinel plants a
    poisoned operator config (user CLAUDE.md + MCP server) and proves the
    child cannot see it. Empirically verified on claude 2.1.175: print mode
    needs no seed config (the seed serves the PTY path); the child CLI sets
    CLAUDECODE for its own tools, so that scrub is pinned in unit tests, not
    E2E. hermetic-env.ts joins GLOBAL_TOUCHFILES.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: PTY runner spawns hermetic claude sessions
    
    launchClaudePty children get the allowlist-scrubbed env, a gated
    --strict-mcp-config, and the session exposes hermeticConfigDir for
    forensics (hermetic plan files live under <dir>/plans/ and still match
    extractPlanFilePath via the /.claude dir-name contract). Seeded trust
    state covers repo-cwd sessions; the 15s trust-watcher stays as fallback.
    Verified foreground via the plan-mode-no-op gate test.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: codex/gemini runners spawn hermetic children
    
    Same allowlist scrub as the claude runners, with each provider's auth
    surface re-admitted via extraAllow (codex: OPENAI_API_KEY/CODEX_* plus
    its tempHome .codex copy; gemini: GEMINI_*/GOOGLE_* with real HOME for
    ~/.gemini auth). The gemini spawn previously inherited the full operator
    env with no env property at all.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * feat: agent-sdk-runner spawns hermetic children via complete Options.env
    
    The historical 'env: breaks SDK auth' failure was partial-env replacement:
    Options.env replaces the child's entire environment, so objects lacking
    ANTHROPIC_API_KEY killed auth. Passing the complete hermetic env (key +
    PATH + redirected CLAUDE_CONFIG_DIR/GSTACK_HOME) works — validated live
    via query() with a Bash tool call (success, real cost, Conductor vars
    scrubbed). Per-test opts.env merges last; ambient key mutation still
    works because the builder reads process.env at call time.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: static tripwire pins hermetic wiring in all five runners
    
    Free-tier invariants: every runner builds child env via hermeticChildEnv,
    no raw ...process.env spread at any spawn site, --strict-mcp-config gated
    on isHermeticEnabled in both claude runners, and no test callsite passes
    the operator env into a runner's override parameter (scoped to runner
    calls — unit tests spawning gstack bin scripts directly are exempt).
    Mirrors the terminal-agent-pid-identity / server-embedder-terminal-port
    tripwire idiom.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * test: refresh codex/factory ship goldens with detached-eval block
    
    a38089aa added the gstack-detach guidance to the ship template and
    updated the claude golden; the codex and factory goldens missed the same
    16-line block. Regenerated via bun run gen:skill-docs.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * docs: hermetic local E2E is the default; retire stale SDK env warning
    
    CLAUDE.md now documents the hermetic clean room (allowlist scrub, fresh
    seeded CLAUDE_CONFIG_DIR, temp GSTACK_HOME, --strict-mcp-config),
    EVALS_HERMETIC=0 as the debug escape hatch, and replaces the 'never pass
    env: to runAgentSdkTest' rule with the verified mechanism (partial-env
    replacement was the failure; complete env is safe).
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * fix: operational-learning fixture copies lib/jsonl-store.ts with the bin
    
    gstack-learnings-log imports $SCRIPT_DIR/../lib/jsonl-store.ts (hasInjection,
    v1.57.5.0) — copying only the bin scripts into the temp fixture broke the
    script with exit 1 since then. Latent because diff-based selection rarely
    runs this test; surfaced when hermetic-env.ts joined GLOBAL_TOUCHFILES and
    selected everything. Reproduced outside the hermetic env to confirm blame.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * fix: ios-qa daemon scenarios use unique pidfiles under --concurrent
    
    All scenarios shared join(workDir, 'daemon.pid') through a module-scope
    workDir binding that beforeEach reassigns mid-flight under bun --concurrent.
    First daemon claims; siblings get already_running against the test process's
    own always-alive pid and fail in milliseconds — the failure mode seen at
    15-way gate concurrency. Per-claim unique pidfiles keep the single-instance
    semantics under test.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * fix: workflow judge re-appends body-carved sections after the marker slice
    
    runWorkflowJudge appended sections/*.md before slicing startMarker..endMarker.
    That handles skills that moved their MARKERS into sections (plan-eng,
    plan-design) but not document-release, which keeps its markers in the
    skeleton and carved the workflow BODY (Steps 2-9 -> sections/release-body.md)
    AFTER the endMarker — so the slice dropped it and the judge scored
    completeness 2 ('Steps 2-9 are in an external file'). Now any carved section
    the marker window excluded is re-appended, so the judge sees the full
    workflow the agent executes. document-release: completeness 2->5, clarity
    3->4. ship/plan-ceo/plan-eng/plan-design judges unchanged (their section
    content is already inside the slice, so the head-dedup skips re-append).
    
    Pre-existing since the v1.57.0.0 carve (#1907); surfaced now because
    hermetic-env.ts is a global touchfile that selects every llm-judge test.
    
    Co-Authored-By: default avatarClaude Fable 5 <noreply@anthropic.com>
    
    * harden: hermetic temp-dir GC grace window + half-seed cleanup
    
    Codex adversarial review (ship) flagged two temp-dir lifecycle edges:
    - GC deleted any dead-pid dir; PID reuse could delete a freshly-created dir
      whose original pid exited and was recycled to a live process. Now requires
      BOTH a dead pid AND mtime older than a 1h floor.
    - A seed-write failure after mkdir left an unseeded dir named with our live
      pid that this process's GC skips, leaking until exit. Now the partial dir
      is torn down before the (still loud) rethrow.
    
    Two findings left as-is by design: HOME stays allowlisted (CLAUDE_CONFIG_DIR
    wins for claude; codex/gemini need ~/.codex|~/.gemini auth; FS sandbox is
    TODOS.md:454 scope; the hermetic-sentinel canary proves config isolation),
    and PTY extraArgs --mcp-config is a deliberate caller opt-in like env overrides.
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * docs: document hermetic-by-default E2E + eval:bg detached runs in CONTRIBUTING
    
    The Testing & evals section now tells contributors that local E2E runners
    spawn children through a sealed clean room (allowlist-scrubbed env, seeded
    CLAUDE_CONFIG_DIR, temp GSTACK_HOME, --strict-mcp-config) so local signal
    matches CI, with EVALS_HERMETIC=0 as the escape hatch. The eval-tools list
    gains the eval:bg* detached-run scripts (gstack-detach: SIGTERM-proof,
    caffeinate-wrapped, machine-locked, run-scoped logs, EXIT= sentinel).
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * chore: sync package.json to 1.58.1.0
    
    The merge took main's package.json (1.58.0.0); gstack-version-bump repair
    fixed the working tree but the change was left uncommitted. Without this the
    committed tree disagrees with VERSION and CI's version-match test fails.
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    * docs: regenerate diagram SKILL.md with Conductor prose preamble
    
    The diagram skill (new from main) was missing the Conductor-session prose
    AskUserQuestion blocks that gen-skill-docs propagates to every SKILL.md.
    Pure generated output; reproduced by bun run gen:skill-docs.
    
    Co-Authored-By: default avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: default avatarClaude Fable 5 <noreply@anthropic.com>
After you've reviewed these contribution guidelines, you'll be all set to contribute to this project.
Loading