WinHarness is a Windows-native, terminal-first AI coding harness designed around Native AOT, MCP stdio tools, and OpenAI-compatible model endpoints.
v0.2 adds persistent tree-structured sessions, full turn artifacts (including tool calls and results), project context files (AGENTS.md, SYSTEM.md), and manual compaction. Interactive winharness chat continues the most recent workspace session by default.
v0.1 provider scope is unchanged — OpenAI-compatible endpoints only:
- OpenAI
- Ollama
/v1 - LM Studio
/v1 - vLLM
/v1 - OpenRouter-compatible endpoints
- other compatible
/v1chat-completions style APIs
Anthropic Messages and OpenAI Codex Responses are supported via hand-rolled AOT-safe clients (ADR-0005); use winharness login --provider anthropic or openai for subscription auth.
Phase 0 is a dependency spike, not application feature work. It validates that the selected v0.1 dependency set can:
- restore and build on .NET 10,
- publish as a single
win-x64Native AOT executable, - emit zero IL/trimming/AOT warnings,
- run representative paths in the published binary.
The spike is located in spikes/WinHarness.AotSpike.
- .NET 10 SDK
- Windows x64 for the Native AOT publish/run gate
dotnet restore .\spikes\WinHarness.AotSpike\WinHarness.AotSpike.csproj
dotnet build .\spikes\WinHarness.AotSpike\WinHarness.AotSpike.csproj -c Release --no-restore
dotnet publish .\spikes\WinHarness.AotSpike\WinHarness.AotSpike.csproj -c Release -r win-x64 --no-build /p:PublishAot=true
.\spikes\WinHarness.AotSpike\bin\Release\net10.0\win-x64\publish\winharness-aot-spike.exedotnet publish .\src\WinHarness.Cli\WinHarness.Cli.csproj -c Release -r win-x64 --no-build /p:PublishAot=true
.\src\WinHarness.Cli\bin\Release\net10.0\win-x64\publish\winharness.exe diagnostics aot
.\src\WinHarness.Cli\bin\Release\net10.0\win-x64\publish\winharness.exe tools list
.\src\WinHarness.Cli\bin\Release\net10.0\win-x64\publish\winharness.exe tools call --name read_file --arguments-json '{"path":"README.md"}'winharness versionwinharness diagnostics aotwinharness diagnostics write --message "..."winharness config initwinharness config wizardfor guided, interactive provider/model setupwinharness chat --prompt "..." [--render-markdown true] [--continue-session]— piped stdin is prepended as a fenced block (Get-Content README.md | winharness chat --prompt "Summarize"); attach files with--files src\Program.cs(repeatable) or inline@pathtokens in the promptwinharness chat --prompt "..." --output json— emit LF-delimited JSONL events (turn/tool/usage/error) on stdout for scripting; seedocs/design/json-events.mdwinharness rpc— long-lived process integration over stdin/stdout JSON (prompt, steer, abort, session ops); seedocs/design/rpc.mdandsamples/rpc-client.ps1winharness chatfor the terminal REPL (continues the most recent workspace session by default; see Sessions)winharness chat --verboseto show one persistent line per tool event instead of compact batch summarieswinharness chat --tools read_file,grep,globto allowlist tools for the run;--exclude-tools run_commandto deny specific tools;--no-toolsto disable all tools (applies to built-in and MCP tools; unknown names warn instead of failing)winharness providers listwinharness providers add --id openai-main --base-url https://api.openai.com/v1 [--api-key sk-... --set-default]winharness providers remove --id openai-mainwinharness providers use --provider-id local-ollamawinharness models list --provider-id local-ollama(add--filter "local*"for a case-insensitive wildcard over model ids across all providers)winharness models discover --base-url http://localhost:11434/v1 [--api-key sk-...]to query the endpoint'sGET /v1/modelswinharness models add --provider-id openai-main --id gpt-primary --provider-model-id gpt-4.1 [--tool-calling --vision --set-default]winharness models set-capabilities --provider-id openai-main --model-id gpt-primary [--tool-calling --vision --reasoning ...]winharness models use --model-id local-coderwinharness tools listwinharness tools call --name read_file --arguments-json '{"path":"README.md"}'winharness docs listwinharness docs get --topic sessionswinharness mcp listwinharness mcp add-stdio --id filesystem --command filesystem-mcp-server.exe [--arguments-json '["--root","C:\\src"]' --environment-json '{}' --enabled true]winharness mcp add-http --id remote --endpoint https://example.com/mcp [--transport http|sse --headers-json '{"X-Client-Name":"WinHarness"}' --enabled true]winharness mcp enable --id filesystemwinharness mcp disable --id filesystemwinharness mcp remove --id filesystemwinharness mcp toolswinharness login --provider copilot [--enterprise-domain ghe.example.com]— GitHub Copilot subscription auth via device code flow (see Subscription auth)winharness login --provider anthropic— Claude Pro/Max OAuth (PKCE + loopback; paste fallback)winharness login --provider openai(alias:codex) — ChatGPT Plus/Pro Codex OAuthwinharness login status/winharness logout --provider copilotwinharness credentials set|get|list|delete
WinHarness persists multi-turn work as append-only JSONL files with a tree structure (id / parentId). Each turn stores full artifacts — user messages, assistant text, tool calls, and tool results — not just the final assistant reply.
Storage path:
%APPDATA%\WinHarness\sessions\{workspaceKey}\
workspaceKey is the normalized absolute working directory (for example c-users-dperez-documents-github-winharness). Files are named {yyyyMMdd-HHmmss}_{sessionId}.jsonl. See samples/session.example.jsonl for the on-disk JSON shape.
winharness chat session flags:
| Flag | Short | Behavior |
|---|---|---|
| (default) | Continue the most recent session for the current directory, or create a new file if none exist | |
--no-session |
Ephemeral in-memory session (v0.1 behavior) | |
--continue-session |
-c |
Persist the chat (required for one-shot --prompt to write a session file) |
--resume |
-r |
Interactive session picker for this workspace |
--session <path|id> |
Open a specific file path or match by session id suffix | |
--name <name> |
-n |
Set the display name on a newly created session (session_info entry) |
The published CLI registers the long option names; use --continue-session, --resume, and --name in scripts.
One-shot winharness chat --prompt "..." is ephemeral by default. Pass --continue-session (or --session) to append that turn to a persisted session.
Slash commands (sessions):
| Command | Behavior |
|---|---|
/session |
Show file path, id, display name, leaf id, message count, provider/model |
/name <name> |
Append a session_info display name |
/new |
Create a new session file (previous file remains on disk) |
/resume |
In-session picker (same as --resume) |
/tree |
Navigate session branches (numbered picker; * marks the active branch) |
/fork |
Copy the active branch into a new session file in the same workspace folder |
/clone |
Same copy as /fork at the current position (alias with pi-compatible naming) |
/export [file] |
Export the active branch to a self-contained HTML page (default) or JSONL (.jsonl extension) |
/import <file.jsonl> |
Validate a JSONL session file and switch to a new session containing its messages |
/compact [instructions] |
Summarize older context; recent messages stay in the active branch |
/usage |
Show model, estimated context %, last-turn and session token totals |
/trust [always|never] |
Show or save the project trust decision for this folder |
/templates |
List discovered prompt templates |
/t <name> [key=value …] [text] |
Expand a prompt template and run it ({{input}} receives the trailing text) |
Automatic compaction is enabled by default for persisted sessions. Before each turn, WinHarness estimates the active-branch tokens against the model's configured contextWindow (fallback 8192 when unset) and compacts proactively when within reserveTokens (default 4096) of the limit. When a provider rejects a request for context overflow, WinHarness compacts and retries the turn once. Configure via the compaction block in config.json:
{
"compaction": { "autoCompact": true, "reserveTokens": 4096 }
}Set "autoCompact": false to rely on manual /compact only. Ephemeral sessions (--no-session) are never auto-compacted.
Per-request timeout. Chat provider requests default to an infinite HTTP timeout so high-effort reasoning models can spend arbitrarily long thinking before their first token without WinHarness canceling the request. To bound hangs instead, set requestTimeoutSeconds (top-level in config.json) to a positive number of seconds:
{
"requestTimeoutSeconds": 600
}A value of 0 (or omitted) means infinite.
Existing commands still work: /provider, /model, /skills, /skill, /markdown, /clear, /help, /exit. /provider and /model append model_change entries when the session is persisted.
While a turn is running you can keep typing in the REPL:
| Input | Behavior |
|---|---|
some text + Enter |
Follow-up: queued and run as the next prompt after the current turn completes |
>> some text + Enter |
Steering: delivered to the model after the current tool calls finish, within the same turn |
/abort + Enter |
Cancels the turn; unsent steering messages become follow-up input |
| Shift+Enter or Ctrl+J | Insert a newline without submitting (idle prompt and in-turn typing) |
Steering messages are persisted in the session as ordinary user messages. Steering that never finds a tool-round injection point is promoted to a follow-up when the turn ends.
| Input | Behavior |
|---|---|
""" |
Start multi-line input; finish with """ on its own line (text after the opening marker becomes the first line) |
| Shift+Enter / Ctrl+J | Soft newline in the single-line prompt (grows the fixed footer when region mode is on) |
!command |
Run via cmd.exe /c (captured), print the output, and send it to the model as a user message |
!!command |
Run and print only — nothing is sent to the model |
@path |
Attach a file: the token stays in the prompt text and the file contents are appended as a fenced block (missing paths are left alone; files over 256 KB are skipped with a notice) |
Project instructions are loaded at startup and injected into the system prompt chain (not written to the session file):
| File | Scope |
|---|---|
AGENTS.md |
Global (%APPDATA%\WinHarness\), then each ancestor directory down to cwd |
CLAUDE.md |
Same walk as AGENTS.md (both filenames are supported) |
.winharness/SYSTEM.md |
Replaces the built-in runtime system prompt (project first, then global) |
.winharness/APPEND_SYSTEM.md |
Appended after the base system prompt (project first, then global) |
The REPL startup banner includes a context: line when any of these files are loaded.
Workspaces containing project-local resources (.winharness\ or .agents\skills) require a trust decision before those resources are loaded, since project SYSTEM.md and skills can steer the model. Interactive chat prompts once (always / once / never; always/never persist to %APPDATA%\WinHarness\trust.json, and a decision covers all child folders). Non-interactive runs (--prompt, --output json) never prompt: they use the defaultProjectTrust setting (ask — default, treated as untrusted — always, or never), overridable per run with --approve / --no-approve. Untrusted workspaces still load plain AGENTS.md/CLAUDE.md context and all global resources. Use /trust always|never in the REPL to save a decision.
Reusable prompts as Markdown files with optional YAML frontmatter (name, description) and {{placeholder}} slots. Discovered from .winharness/prompts/ and .agents/prompts/ in the workspace (trust-gated) and %APPDATA%\WinHarness\prompts\:
---
name: review
description: Code review with a focus area
---
Review this code for bugs and {{focus}} issues.
{{input}}In the REPL: /t review focus=security look at the parser. One-shot: winharness chat --template review --template-args "focus=security" --prompt "look at the parser" (--prompt fills {{input}}). Unfilled placeholders are reported instead of silently sent.
WinHarness can load Markdown skill instructions and apply one selected skill to each chat turn. Skills are discovered from:
.winharness/skills/**/SKILL.mdunder the current workspace.agents/skills/**/SKILL.mdunder the current workspace%APPDATA%\WinHarness\skills/**/SKILL.md%USERPROFILE%\.agents\skills/**/SKILL.md
A skill can use optional YAML frontmatter:
---
name: tdd
description: Test-driven development workflow
---
# TDD
Write a failing test first, then implement the minimum code to pass it.Use /skills to list discovered skills, /skill <name> to activate one for the session, and /skill off to clear it. The selected skill is injected as an additional system message for each turn; it does not rewrite the saved conversation history.
Runtime configuration will live under:
%APPDATA%\WinHarness
Set the WINHARNESS_CONFIG_DIR environment variable to redirect the entire configuration directory (config, sessions, logs, skills) to another path — useful for smoke tests and CI so runs never touch the real per-user configuration. Note that setting APPDATA alone has no effect: the directory is resolved through the Windows known-folder API, not the environment variable.
API keys must be stored in Windows Credential Manager, not configuration files.
WinHarness credential target names must use the WinHarness: prefix, for example WinHarness:openai-main.
winharness login --provider copilot signs in with a GitHub Copilot subscription using the OAuth device code flow: visit the printed URL, enter the code, and WinHarness stores the token set in Windows Credential Manager under WinHarness:oauth:copilot. The command creates (or updates) a copilot provider pointing at your account's Copilot API endpoint; short-lived bearers refresh automatically during chat.
winharness login --provider anthropic signs in with Claude Pro/Max via PKCE on a fixed-port loopback (http://localhost:53692/). Press Enter while waiting to paste the redirect URL instead (SSH/remote). Tokens are stored under WinHarness:oauth:anthropic; the command seeds an anthropic provider with kind: anthropic-messages.
winharness login --provider openai (or codex) signs in with ChatGPT Plus/Pro via PKCE on http://localhost:1455/auth/callback. Tokens are stored under WinHarness:oauth:openai; the command configures an openai provider with kind: openai-codex-responses (Responses API) and discovers the account's current Codex model catalog. See docs/adr/ADR-0005-oauth-subscription-providers.md.
Note: subscription auth rides the unofficial endpoints the vendors ship for their own CLIs. They can change or be revoked at any time (ADR-0005 records this risk acceptance).
Example configuration files are in samples/, including session.example.jsonl for the persisted session format and a separate model-capabilities.example.json shape for model capability metadata.
Create a starter configuration:
winharness config initThe generated file configures a local OpenAI-compatible Ollama endpoint at http://localhost:11434/v1. Edit %APPDATA%\WinHarness\config.json to add hosted endpoints.
The fastest way to add providers is the guided wizard, which prompts for the
base URL and API key, stores the key in Windows Credential Manager, then queries
the endpoint's GET /v1/models route so you can multi-select real model ids
(instead of typing them), assign capabilities, and pick defaults:
winharness config wizardYou can also list an endpoint's advertised models directly:
winharness models discover --base-url https://api.openai.com/v1 --api-key $env:OPENAI_API_KEYThe same operations are available non-interactively. Adding a provider with
--api-key stores the secret in Windows Credential Manager under
WinHarness:<id> automatically:
winharness providers add --id openai-main --base-url https://api.openai.com/v1 --api-key $env:OPENAI_API_KEY --set-default
winharness models add --provider-id openai-main --id gpt-primary --provider-model-id gpt-4.1 --tool-calling --vision --set-defaultKeyless local endpoints (Ollama, LM Studio, vLLM) simply omit --api-key:
winharness providers add --id local-ollama --base-url http://localhost:11434/v1 --set-default
winharness models add --provider-id local-ollama --id local-coder --provider-model-id qwen2.5-coder:latestOpenAI-compatible providers use the official OpenAI SDK plus Microsoft.Extensions.AI.OpenAI for the IChatClient adapter. Each configured model declares its own capabilities instead of relying on provider-wide hardcoding.
Example hosted provider:
{
"id": "openai-main",
"kind": "openai-compatible",
"baseUrl": "https://api.openai.com/v1",
"credentialName": "WinHarness:openai-main",
"models": [
{
"id": "gpt-primary",
"providerModelId": "gpt-4.1",
"capabilities": {
"streaming": true,
"toolCalling": true,
"vision": true,
"promptCaching": false,
"structuredOutput": true,
"reasoning": true
}
}
]
}Store the API key:
winharness credentials set --target-name WinHarness:openai-main --secret $env:OPENAI_API_KEYSelect defaults:
winharness providers use --provider-id openai-main
winharness models use --model-id gpt-primaryMCP servers can use stdio, Streamable HTTP, or HTTP with SSE. WinHarness discovers tools with explicit client-side tools/list and calls them with tools/call; it does not use assembly scanning or attribute-based discovery in the AOT path.
Stdio example:
winharness mcp add-stdio --id filesystem --command filesystem-mcp-server.exe --arguments-json '["--root","C:\\src"]' --enabled trueStreamable HTTP example:
winharness mcp add-http --id remote --endpoint https://example.com/mcp --transport http --enabled trueHTTP with SSE example:
winharness mcp add-http --id legacy-sse --endpoint https://example.com/sse --transport sse --enabled trueNon-secret headers are supplied as JSON when needed:
winharness mcp add-http --id remote --endpoint https://example.com/mcp --headers-json '{"X-Client-Name":"WinHarness"}'Inspect MCP tools:
winharness mcp list
winharness mcp toolsBuilt-in tools are exposed through the same ITool registry as MCP tools:
read_filewrite_fileedit_filerun_commandglobgrep
run_command defaults to captured stdout/stderr for clean agent-readable output. Set mode to interactive only when a command needs a real terminal; that path uses the Windows ConPTY executor. Captured mode is non-interactive; to feed data to a command that reads stdin, use the input field (UTF-8, no BOM). The field writes the text and then closes stdin so the process exits cleanly.
Example:
winharness tools call --name run_command --arguments-json '{"command":"dotnet","arguments":["--info"],"timeoutSeconds":30}'Stdin input example:
winharness tools call --name run_command --arguments-json '{"command":"cmd.exe","arguments":["/c","findstr .*"],"input":"hello\\n","timeoutSeconds":10}'WinHarness writes structured local JSONL diagnostics under:
%APPDATA%\WinHarness\logs
Diagnostics include provider calls, tool execution timing, failures, and selected command execution mode. No external telemetry is emitted.
ADRs are stored in docs/adr/.