diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83a8fe4..17e5842 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ name: CI on: push: - branches: [main, "feat/**"] + branches: [main, "feat/**", "fix/**"] pull_request: permissions: contents: read @@ -9,10 +9,10 @@ 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 @@ -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 @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fc64ec..8410ff9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/whythis-pr.yml b/.github/workflows/whythis-pr.yml index e695357..253d1ad 100644 --- a/.github/workflows/whythis-pr.yml +++ b/.github/workflows/whythis-pr.yml @@ -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 diff --git a/docs/harnesses.md b/docs/harnesses.md index 871bab0..75164ab 100644 --- a/docs/harnesses.md +++ b/docs/harnesses.md @@ -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 --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 --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 ` | 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. | @@ -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). diff --git a/docs/research.md b/docs/research.md index 3c49992..46668d8 100644 --- a/docs/research.md +++ b/docs/research.md @@ -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. diff --git a/internal/harness/builtin.go b/internal/harness/builtin.go index 4d3e803..bea82b6 100644 --- a/internal/harness/builtin.go +++ b/internal/harness/builtin.go @@ -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"} }}, @@ -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") diff --git a/internal/harness/harness_test.go b/internal/harness/harness_test.go index 1057689..7ff0292 100644 --- a/internal/harness/harness_test.go +++ b/internal/harness/harness_test.go @@ -2,6 +2,7 @@ package harness import ( "context" + "reflect" "testing" ) @@ -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" {