Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
name: CI
on:
push:
branches: [main, "feat/**"]
branches: [main, "feat/**", "fix/**"]
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v7
with:
go-version: '1.27.1'
cache: true
Expand All @@ -37,8 +37,8 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.1'
cache: true
Expand All @@ -53,8 +53,8 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.1'
cache: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
- { goos: windows, goarch: arm64 }
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.27.1'
- run: go mod download
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/whythis-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ jobs:
COMMENT_ENABLED: ${{ (github.event_name == 'workflow_dispatch' && inputs.comment) || (github.event_name == 'pull_request' && vars.WHYTHIS_AUTO_COMMENT == 'true') }}
RISK_THRESHOLD: ${{ inputs.risk_threshold || vars.WHYTHIS_RISK_THRESHOLD || '50' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v7
with:
go-version: '1.27.1'
cache: true
Expand Down
7 changes: 3 additions & 4 deletions docs/harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

Harnesses interpret already-collected evidence. They are not evidence collectors and cannot promote their output to FACT.

The table below records the upstream repository/source inspected during the initial implementation. The point is to make integration claims auditable rather than guessed.
The table below records the upstream repository/source inspected during the initial implementation. The point is to make integration claims auditable rather than guessed. Pi repository provenance was refreshed on **2026-09-18** after its upstream ownership move.

| ID | Official/current upstream inspected | Verified mechanism used | Notes |
|---|---|---|---|
| `codex` | `openai/codex` — `codex-rs/exec/src/cli.rs`, `exec_events.rs` | `codex exec --json --ephemeral --skip-git-repo-check -` | CLI source documents non-interactive exec, JSONL, ephemeral sessions and stdin prompt `-`. |
| `claude-code` | `anthropics/claude-code` — release feed and `plugins/plugin-dev` hook docs | print mode with `--output-format stream-json` | Upstream also exposes hooks including PreToolUse/PostToolUse/Stop/Session lifecycle events. WhyThis uses one-shot synthesis, not hooks. |
| `opencode` | `anomalyco/opencode` — `packages/web/src/content/docs/cli.mdx` | `opencode run --format json` | The former `opencode-ai/opencode` repository is archived; the active repository inspected is `anomalyco/opencode`. Docs also expose a headless `serve` backend. |
| `pi` | `earendil-works/pi` — `packages/coding-agent/docs/rpc.md` | `pi --mode rpc --no-session` | Strict JSONL RPC over stdin/stdout; WhyThis sends a `prompt` command and consumes events until `agent_end`. The repository is the current destination of the former `badlogic/pi-mono` project. |
| `pi` | `mitsuhiko/pi-mono` — `packages/coding-agent/docs/rpc.md` | `pi --mode rpc --no-session` | Strict JSONL RPC over stdin/stdout; WhyThis sends a `prompt` command and consumes events until `agent_end`. The current package is `@mariozechner/pi-coding-agent`; the upstream project has moved owners over time, so capability evidence points at the current canonical repository. |
| `gemini` | `google-gemini/gemini-cli` — `docs/reference/configuration.md` | `gemini -p ... --output-format stream-json` | Upstream documents `json` and streaming `stream-json` non-interactive output. |
| `aider` | `Aider-AI/aider` — `aider/args.py` and config docs | `aider --message-file <file> --yes-always` | `--message-file` is documented as send/process/exit. WhyThis runs synthesis in a temporary directory to isolate it from the analyzed repo. |
| `aider` | `Aider-AI/aider` — `aider/website/docs/scripting.md` | `aider --message-file <file> --yes` | `--message-file` is documented as send/process/exit and `--yes` is the documented non-interactive confirmation flag. WhyThis runs synthesis in a temporary directory to isolate it from the analyzed repo. |
| `goose` | `aaif-goose/goose` — `crates/goose-cli/src/cli.rs` and headless docs | `goose run --text ... --output-format stream-json --no-session` | CLI source defines text input, JSON/stream-JSON output and no-session automation mode. |
| `cline` | `cline/cline` — `apps/cli/README.md`, `apps/cli/src/main.ts` | `cline --json <prompt>` | Current Cline CLI explicitly documents one-shot/headless operation and NDJSON output. |
| `roo-code` | `RooCodeInc/Roo-Code` — `README.md` | installed-extension detection and prompt handoff | Upstream README states the extension was shut down on May 15, 2026. WhyThis does **not** invent a maintained headless CLI. |
Expand All @@ -27,7 +27,6 @@ For subprocess-based synthesis WhyThis creates a temporary working directory and

Availability means the integration surface was detected locally; it does not mean provider credentials are configured.


## External adapters

The built-ins are not a closed list. `pkg/harness` exposes the public `Adapter`, `Capabilities`, `AnalysisRequest` and `AnalysisResult` types so external Go modules can integrate additional harnesses without importing internal packages. See [`adapter-development.md`](adapter-development.md).
2 changes: 1 addition & 1 deletion docs/research.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ The GitHub search did not reveal an established project with the exact WhyThis c
2. Historical metrics/risk remain deterministic.
3. GitHub discussion is evidence only when fetched and preserved with provenance.
4. AI is a semantic interpreter behind a `--no-ai`-complete deterministic path.
5. Symbol identity is not overclaimed: current support starts with pickaxe/text history, with language-aware identity tracking left for later work.
5. Symbol identity is not overclaimed. The initial design used pickaxe/text history as the portable fallback; the current implementation also uses Go AST declarations and `git log -L` for Go symbols, while non-Go and deleted-symbol paths retain the textual fallback.
4 changes: 2 additions & 2 deletions internal/harness/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func builtins() []Harness {
commandHarness{id: "opencode", binary: "opencode", integration: "process: opencode run --format json", structured: true, sources: []string{"anomalyco/opencode packages/web/src/content/docs/cli.mdx"}, args: func(_ string, p string) []string { return []string{"run", "--format", "json", p} }},
piHarness{},
commandHarness{id: "gemini", binary: "gemini", integration: "process: gemini -p + stream-json", structured: true, sources: []string{"google-gemini/gemini-cli docs/reference/configuration.md"}, args: func(_ string, p string) []string { return []string{"-p", p, "--output-format", "stream-json"} }},
commandHarness{id: "aider", binary: "aider", integration: "process: --message-file (one-shot)", structured: false, sources: []string{"Aider-AI/aider aider/args.py"}, args: func(f string, _ string) []string { return []string{"--message-file", f, "--yes-always"} }},
commandHarness{id: "aider", binary: "aider", integration: "process: --message-file (one-shot)", structured: false, sources: []string{"Aider-AI/aider aider/website/docs/scripting.md"}, args: func(f string, _ string) []string { return []string{"--message-file", f, "--yes"} }},
commandHarness{id: "goose", binary: "goose", integration: "process: goose run --text + stream-json", structured: true, sources: []string{"aaif-goose/goose crates/goose-cli/src/cli.rs"}, args: func(_ string, p string) []string {
return []string{"run", "--text", p, "--output-format", "stream-json", "--no-session"}
}},
Expand All @@ -34,7 +34,7 @@ type piHarness struct{}
func (piHarness) ID() string { return "pi" }
func (piHarness) Detect(ctx context.Context) (Capabilities, error) {
p, err := exec.LookPath("pi")
return Capabilities{ID: "pi", Available: err == nil, Executable: p, Integration: "JSONL RPC: pi --mode rpc --no-session", StructuredOutput: true, SessionProtocol: true, EvidenceSources: []string{"earendil-works/pi packages/coding-agent/docs/rpc.md"}}, nil
return Capabilities{ID: "pi", Available: err == nil, Executable: p, Integration: "JSONL RPC: pi --mode rpc --no-session", StructuredOutput: true, SessionProtocol: true, EvidenceSources: []string{"mitsuhiko/pi-mono packages/coding-agent/docs/rpc.md"}}, nil
}
func (piHarness) Analyze(ctx context.Context, req AnalysisRequest) (AnalysisResult, error) {
p, err := exec.LookPath("pi")
Expand Down
36 changes: 36 additions & 0 deletions internal/harness/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package harness

import (
"context"
"reflect"
"testing"
)

Expand All @@ -11,6 +12,41 @@ func TestRegistryHasTenHarnesses(t *testing.T) {
t.Fatalf("expected 10 adapters, got %d", len(r.items))
}
}

func TestBuiltinHarnessContracts(t *testing.T) {
var aider commandHarness
var foundAider bool
for _, h := range builtins() {
if h.ID() != "aider" {
continue
}
var ok bool
aider, ok = h.(commandHarness)
if !ok {
t.Fatalf("aider adapter has unexpected type %T", h)
}
foundAider = true
break
}
if !foundAider {
t.Fatal("aider adapter not found")
}

got := aider.args("/tmp/prompt.md", "ignored")
want := []string{"--message-file", "/tmp/prompt.md", "--yes"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("aider args = %#v, want %#v", got, want)
}

piCaps, err := (piHarness{}).Detect(context.Background())
if err != nil {
t.Fatal(err)
}
if len(piCaps.EvidenceSources) != 1 || piCaps.EvidenceSources[0] != "mitsuhiko/pi-mono packages/coding-agent/docs/rpc.md" {
t.Fatalf("unexpected pi evidence sources: %#v", piCaps.EvidenceSources)
}
}

func TestExtractText(t *testing.T) {
got := extractText("{\"type\":\"x\",\"text\":\"hello\"}\n")
if got != "hello" {
Expand Down
Loading