| name | terminal-hygiene |
| description | Terminal and test execution guardrails plus session-cost discipline for Agent Orchestra workflows. Use when choosing sync/async terminal mode, scoping Pester runs, retrying background commands, recovering from multiline-prompt stalls, wrapping subagent diagnostics, avoiding terminal/subagent batching mistakes, or applying session-cost discipline; also when a PowerShell expression returns an unexpected type, an array collapses to a scalar or a single element, a parameter binding or splat behaves unlike the call you wrote, or an ordered literal, pipeline, or here-string parses differently than written. DO NOT USE FOR: application-level debugging (use systematic-debugging), post-merge archival (use post-pr-review), or cost telemetry/measurement setup (use copilot-cost-collection). |
Terminal Hygiene
Terminal and validation rules that keep workflow execution predictable.
When to Use
- When choosing targeted versus full-suite Pester runs
- When deciding between
mode: sync and mode: async / isBackground: true
- When validating at step boundaries without overflowing terminal state
- When retrying background terminal commands safely
- When applying session-cost discipline (parent-side diagnostics, targeted edits, call batching, extract-don't-dump) in a long-context orchestrated session
Scope
These rules supplement, not replace, any agent-specific terminal guidance such as Code-Conductor's non-interactive guardrails. Scope also extends to session-cost discipline for long-context agent sessions — not terminal execution alone.
PowerShell and Pester traps
references/powershell-traps.md collects the language-level and runner-level traps that have cost this repository real time. Most share one property: they do not throw — they return a wrong shape, a wrong count, or a green verdict; the few that do throw fire somewhere other than where you would look. Read it before writing a verification script, a guard, or a test whose purpose is to detect something, since most of the file is a way for such a check to report success while checking nothing.
Highest-frequency entries: -eq and Should -Be are case-insensitive (so an "identical text" criterion implemented with either cannot detect a case-only difference — the exact divergence such a criterion is usually written to catch); return ,$array plus a caller's @() collapses to one element; ?? does not guard an absent property under StrictMode; and the sharded runner's fail=N is not a count of failing tests.
Pester Scope
When iterating on a specific test during red-green-refactor within an implementation step, use targeted Pester:
Invoke-Pester 'path/to/specific.Tests.ps1' -Output Minimal
The full-suite runner is .github/scripts/run-pester-sharded.ps1 (authored in issue #740 s4); invoke it at step boundaries as the standard validation gate. Do not run the full suite during inner-loop iteration.
Note: CI's pester.yml runs a glob minus an explicit quarantine — every *.Tests.ps1 in .github/scripts/Tests/ except the files listed in ci-quarantine.json — through that same sharded runner, driven by that selection rather than by a directory glob (issue #1037). It is not an allowlist and there is no list to register a new suite into: a new suite is gated the moment it is written. Derive the current selection from Get-CISuiteSelection rather than reading a count out of any document; the quarantine shrinks as suites are triaged.
That divergence from the full local suite is intentional, and it runs the other way too: run-pester-sharded.ps1 -TestsPath <dir> globs, so it measures every suite on disk including the quarantined ones. To reproduce what CI runs, pass the selection instead:
. .github/scripts/lib/ci-suite-selection-core.ps1
. .github/scripts/lib/pester-sharded-core.ps1
$selection = Get-CISuiteSelection -TestsRoot '.github/scripts/Tests' -QuarantinePath '.github/scripts/Tests/ci-quarantine.json'
Invoke-PesterSharded -SuitePath $selection.Selected -FanOutWidth 10
Since issue #1037 the runner reports two totals that name their units and are never summed — TOTAL suites (unit: files) with a per-outcome tally, and TOTAL tests (unit: test cases) — plus a reconciliation line against the caller's selection. Do not subtract a per-file phantom from the failure count: that increment is gone. It also means ExitCode=1 with zero failing tests is now a shape that occurs on its own (a crashed worker, a suite that discovered nothing, a suite whose tests were all skipped, a selected suite with no result at all) rather than only alongside failing tests, so read the suite tally or the exit code, never the test-failure total alone. Build any baseline measurement in a detached worktree (git worktree add --detach <path> <commit>): a git stash cannot prove a failure is pre-existing, and a git archive extract has no repository metadata and fails a large number of tests for that reason alone. Documents/Design/test-suite-baseline-948.md records the last full-suite baseline, the runner's counting and false-green traps in detail, and the disposition of every test that was red at it.
isBackground Default
Use isBackground: false for Pester, PSScriptAnalyzer, markdownlint-cli2, structural checks, and any command expected to complete in under 60 seconds. Reserve isBackground: true for dev servers and watch-mode builds.
Exceptions:
- When diagnosing a terminal stall, the
process-troubleshooting skill guidance to switch to isBackground: true for diagnostics takes precedence.
- Final-gate full suite in live-refresh mode (
PESTER_LIVE_GH=1): treat as long-running, run with isBackground: true, and poll with get_terminal_output. In fixture mode, keep isBackground: false.
Pester 5 writes pass/fail output to the terminal buffer rather than redirected file streams, so *> only captures advisory output such as Write-Warning. Do not use await_terminal for the live-refresh full-suite case; the PowerShell prompt returning on the last line signals completion.
No Terminal/Subagent Batching
Do not batch run_in_terminal and subagent dispatch calls in the same parallel tool-call set. Sequential use is fine. Parallel subagent dispatch remains allowed when no terminal command shares that batch.
Session-Cost Discipline
Four rules that keep long-context orchestrated sessions from spending money re-reading their own transcript instead of doing work. They apply to any long-context agent session, not terminal execution alone.
1. Parent-side diagnostics
Rule 1 (parent-side diagnostics): never dispatch a subagent for a check the parent could do in ≤2 tool calls. Reserve subagent dispatch for substantive specialist work.
2. Targeted edits, split by target
Local files: use targeted in-place Edit calls; never rewrite a whole file to change part of it.
GitHub issue/PR bodies: bodies are edited concurrently across sessions and phases, so follow this sequence:
- Compose the new body once from content already in context.
- Precede the write with a freshness check: re-read the live body via structured JSON extraction —
gh issue view {N} --json body --jq '.body' (or gh pr view {N} --json body --jq '.body' for PR bodies), never gh view console text output and never a >-redirected tmp file — and compare it against your in-context snapshot.
- If they diverge, halt-and-reconcile: stop, re-fetch the live body via that same clean channel, and re-compose the write payload from the reconciled content already in context — never merge the re-read output directly into the payload.
- Post the composed body with a single
--body-file write.
The prohibition is scoped to payload reuse, not to reading: never let gh view output become the write payload — on Windows, gh view output OEM-mangles non-ASCII (em-dashes, section signs, emoji); never write a tmp copy and then edit it before posting. This check narrows but does not eliminate the inherent check-then-write race window between the freshness read and the write itself.
Content destined to become a write payload — including the freshness-comparison read above — must be read in full and fidelity-verified — the extract-don't-dump rule (rule 4 below) does not apply to read-modify-write payloads.
See ## Scratch & Temp-File Hygiene above for where scratch files belong; that section is this skill's single source of truth for scratch-file location and is not restated here.
3. Batch independent tool calls
Rule 3 (call batching): batch independent tool calls in one message when they have no dependency between them. Each avoidable sequential call re-reads the whole session context (~$0.08/call, measured on PR #857, 2026-07-16). Carve-out: ## No Terminal/Subagent Batching above always wins — never batch a terminal command and a subagent dispatch in the same parallel set.
4. Extract, don't dump
Rule 4 (extract, don't dump): extract at the tool boundary (--jq, grep, Select-String, targeted Read offsets) instead of dumping full structured payloads into context. Exception: read-modify-write payloads (rule 2 above) must be read in full. Measured on PR #857 (2026-07-16): one careless 36,000-character dump cost approximately $7, because every later call re-carried it — see the #476 cost-analysis comment (2026-07-16). This figure is historical evidence, not a standing rate.
Scope acknowledgment: nine agent bodies currently load this skill unscoped (Code-Conductor, Code-Critic, Code-Smith, Doc-Keeper, Process-Review, Refactor-Specialist, Specification, Test-Writer, UI-Iterator); rules 2-4 are generic session discipline that benefits any of them, not just the bodies that carry an explicit Session-Cost Discipline load reference — this is a scope acknowledgment, not a call to edit those other bodies.
Terminal Cleanup
Code-Conductor manages background terminal lifecycle with its Terminal Lifecycle Protocol. At phase boundaries such as post-step, post-implementation, and post-PR, it sweeps tracked isBackground: true terminal IDs, kills confirmed-completed terminals, and preserves active or unknown-state ones. Cleanup is always non-fatal.
Root cause context:
- Agent Orchestra sessions generate high terminal command volume, especially around repeated structural checks.
- When the shared terminal buffer overflows at roughly 16 KB, commands appear to stall and later commands often shift to new background terminals.
- At roughly 30 or more idle terminals, shells can enter CPU-spin states.
- The consolidated
quick-validate.ps1 reduces per-pass command count and lowers overflow risk.
Logging contract:
Terminal cleanup: killed N completed, preserved M active, K unknown/already-gone
Subagent gap: subagent-spawned background terminals are not tracked by Code-Conductor. Subagents should follow the isBackground: false preference unless a documented exception applies.
Terminal Retry Hygiene
When retrying a failed command that ran in a background terminal (isBackground: true or mode: async), use this kill-before-retry protocol:
- Record the terminal ID returned by
run_in_terminal.
- Kill that terminal via
kill_terminal using the same terminal ID, loading the tool first with tool_search_tool_regex if needed.
- If
kill_terminal fails, log it and proceed. This is non-fatal.
- For dev servers, run
pwsh -NoProfile -NonInteractive -File skills/terminal-hygiene/scripts/check-port.ps1 -Port {PORT} before restart to verify the port was released. If the port is still in use, log the diagnostic and proceed.
- Start the retry in a fresh terminal.
Scope notes:
- This protocol applies to within-step retries for terminals with trackable background IDs.
- Phase-boundary cleanup of accumulated terminals remains governed by Terminal Cleanup.
- Kill-before-retry and Terminal Cleanup are complementary, not substitutes. If both target the same terminal ID, the first successful kill wins and later attempts are harmless no-ops.
- Both
kill_terminal failures and check-port.ps1 errors are non-blocking. Degrade gracefully to retry-without-kill when necessary.
Scratch & Temp-File Hygiene
Single source of truth for where agents write scratch/output files. All other skills cross-reference this section; do NOT duplicate the rule.
Rule: use repo-relative .tmp/ — never host-native absolute paths
When a Bash-tool shell command writes a scratch or output file:
- DO: write to a relative
.tmp/ path, e.g. .tmp/issue-643-body.md, .tmp/643-comments.json
- DO NOT: construct a Windows-style absolute path (
C:\Users\...\Temp\...) or pass it to a POSIX/git-bash shell — the drive letter and backslashes do not translate and the path collapses to a repo-root filename
If a Windows-native tool requires an absolute path (e.g. a screenshot tool that cannot accept a relative save target):
- Use forward-slash git-bash form:
/c/Users/.../Temp/...
- Or use the PowerShell tool with
$env:TEMP — never C:\... inside a bash redirect
Consumer snippet — add these lines to your repo .gitignore to keep .tmp/ and collapsed-mangle-literal shapes out of git status:
# Agent scratch — keep out of git status
.tmp/
/[A-Za-z][A-Za-z]sers*
/[A-Za-z]:*
# /*[Tt]emp* intentionally omitted: over-matched template.md, templates/, attempt.js.
# Primary mangle shapes are covered by /[A-Za-z]:* and /[A-Za-z][A-Za-z]sers* above.
/var*folders*
/[Rr][Uu][Nn][Nn][Ee][Rr]*[Tt][Ee][Mm][Pp]*
These patterns cover the Windows default-temp mangle (UsersXAppDataLocalTempfoo.png) and a root-anchored set of other shapes. They are best-effort — the author-time grep guard (see skills/terminal-hygiene/SKILL.md ## Scratch & Temp-File Hygiene) is the authoritative prevention, not the gitignore net.
Multiline Continuation-Prompt Hazard
PowerShell enters a continuation prompt (>>) and bash enters a > prompt when a command is syntactically incomplete: unclosed here-strings (@'/@"), parentheses, braces, or backtick line continuations in PowerShell; unclosed heredocs, quotes, or backslash continuations in bash.
Symptom: the terminal appears to hang — no output, no PS prompt. The buffer tail shows >> or > rather than a prompt.
Agent-side detection: if the prior command contained an unclosed multiline construct (a here-string, unclosed parenthesis, or continuation backslash) and the terminal returns no new output, presume a continuation prompt rather than a frozen process. When the terminal buffer is inspectable, a trailing >> or > line with no surrounding command output confirms this.
Recovery: do not attempt ^C — from the agent side, sending literal "^C" adds more input to the here-string rather than interrupting the shell. Use kill_terminal on the stalled terminal ID, then open a fresh terminal. This extends the existing kill-before-retry pattern from ## Terminal Retry Hygiene.
Prevention: prefer one-line commands. When a multiline construct is genuinely required, write it to a temporary .ps1 or .sh file and invoke the file, rather than passing the block inline to the terminal. Those temporary .ps1 and .sh files must themselves land under .tmp/ per ## Scratch & Temp-File Hygiene above.
Non-Fatal Diagnostic Wrapper Pattern
When a subagent needs to run a diagnostic check (linting, structural validation, schema inspection) without risking an orchestration halt on a non-zero exit code, the wrapper script should emit a structured status line as its final stdout output and always exit 0.
Shape:
- Readable line:
VALIDATION_STATUS=pass or VALIDATION_STATUS=fail as any line in stdout; emit it last for unambiguous parsing
- Optional preceding lines: evidence such as diff output, line counts, or error messages
- Exit code: always
exit 0 so the orchestrator continues regardless of findings
Worked examples:
PowerShell:
if ($findings.Count -eq 0) {
Write-Output 'VALIDATION_STATUS=pass'
} else {
$findings | ForEach-Object { Write-Output $_ }
Write-Output 'VALIDATION_STATUS=fail'
}
exit 0
Bash:
if [ "$finding_count" -eq 0 ]; then
echo 'VALIDATION_STATUS=pass'
else
echo "$error_details"
echo 'VALIDATION_STATUS=fail'
fi
exit 0
Scope: this pattern applies to diagnostic wrappers only. Real validation gates (Pester, PSScriptAnalyzer, markdownlint) retain their non-zero exits — do not apply exit 0 to gates that must halt on failure. Criterion: if the orchestrator should stop when this tool reports failure, it is a gate; if it should continue and log findings, it is a diagnostic.
Residual non-zero sources: existing third-party tools (e.g., grep -q) exit non-zero on no-match by design. Wrap calls to such tools explicitly if their exit codes would be misread as failures.
Consumer: the VALIDATION_STATUS token is readable by the operator in the terminal buffer and future-greppable as ^VALIDATION_STATUS= in stdout. No automated consumer exists today — the value is operator-facing structured evidence at a glance.
Gotchas
| Trigger | Gotcha | Fix |
|---|
| Running full Pester repeatedly during inner-loop work | Large, repetitive output increases terminal buffer pressure and slows iteration | Use targeted Pester until the step boundary, then run the full validation gate |
| Trigger | Gotcha | Fix |
|---|
| Retrying a failed async server without killing the old terminal | The old shell or port can stay alive and make the retry look flaky | Kill the prior terminal ID first, check the port for dev servers, then restart in a fresh terminal |
| Trigger | Gotcha | Fix |
|---|
| Terminal sits silently after a multiline command | The shell entered a continuation prompt (>> or >); the terminal is not frozen | Intervene immediately — the shell waits indefinitely; use kill_terminal and open a fresh terminal — do not send ^C |
| Trigger | Gotcha | Fix |
|---|
| Subagent diagnostic check causes orchestration to halt unexpectedly | The diagnostic script exited non-zero and the orchestrator treated it as a blocking failure | Wrap the diagnostic in the Non-Fatal Diagnostic Wrapper Pattern: emit VALIDATION_STATUS=pass/fail, exit 0 |