Skip to content

feat(providers): add GitHub Copilot as a community provider#2

Merged
joaobmonteiro merged 7 commits intodevfrom
feat/copilot-sdk-provider
Apr 20, 2026
Merged

feat(providers): add GitHub Copilot as a community provider#2
joaobmonteiro merged 7 commits intodevfrom
feat/copilot-sdk-provider

Conversation

@joaobmonteiro
Copy link
Copy Markdown
Owner

Summary

  • Adds @github/copilot-sdk@0.2.2 as the second community provider (after Pi), wired via registerCommunityProviders(). Each sendQuery() spins up a fresh CopilotClient with per-call cwd, bridges JSON-RPC events into Archon's async-generator contract (reusing Pi's AsyncQueue), and tears down in finally.
  • Conservative v1 capabilities: sessionResume, effortControl, envInjection only. mcp, toolRestrictions, skills, structuredOutput, hooks, sandbox, costControl, fallbackModel, thinkingControl are all false until the plumbing is wired.
  • Auth delegated to Copilot CLI (gh auth login / COPILOT_GITHUB_TOKEN / assistants.copilot.githubToken). Permission callback hard-wired to approveAll — matches the trust model already enforced by worktree isolation.
  • Pins bun@1.3.11 locally via devbox.json to match the production Dockerfile.

Bugs surfaced + fixed during E2E

  1. Loop nodes silently drop provider/model overrides. The DAG schema transform stripped both fields from LoopNode, so provider: copilot on a loop was a no-op. Fix includes them in the loop branch of the transform and lets the existing model-compatibility check run against loops.
  2. Bundled native Copilot CLI wasn't discoverable under Bun. SDK's default cliPath points at @github/copilot/npm-loader.js, which imports node:sea — dies under Bun's node-compat shim. New cli-resolver.ts probes @github/copilot-{platform}-{arch}/copilot via createRequire, with config-cliPath → env → native-binary → SDK-default fallback.
  3. Event payloads read at the wrong nesting level. SDK wraps every typed event under data (see session-events.d.ts); bridge read top-level, so every tool call decayed to toolName: "unknown" / toolInput: {} in the Web UI and session telemetry never populated. New unwrapEventData + split extractToolStart/CompleteFields + per-session toolCallId → toolName map fix the ingestion.

Test plan

  • bun run validate clean across all 10 packages (type-check + lint + format + tests, 153+ copilot/workflow tests green)
  • E2E via Linear webhook → hmq-backend-pipeline-copilot workflow on agentic-worflow repo:
  • Provider dispatched correctly — 12× copilot.session_starting log lines aligned 1:1 with loop iterations on AWF-7; plan step still runs on Claude as expected
  • CLI auto-resolution verified: runs with no cliPath in .archon/config.yaml (hasCliPath: true in logs comes from the native binary probe, not user config)

🤖 Generated with Claude Code

joaobmonteiro and others added 7 commits April 19, 2026 10:57
Introduces `@github/copilot-sdk` (public preview, pinned 0.2.2) as the
second community provider, following the Phase 2 contract established by
Pi: a new provider drops into `packages/providers/src/community/<id>/`
and wires in with a one-line addition to `registerCommunityProviders()`.

The SDK spawns the `copilot` CLI binary over JSON-RPC, so each
`sendQuery()` constructs a fresh `CopilotClient` (cwd is pinned at
construction time, which is required to support different worktrees)
and tears it down in a `finally`. The SDK is event-emitter based; we
reuse the AsyncQueue pattern from Pi's event-bridge to bridge events
into Archon's async-generator contract.

v1 capabilities are intentionally conservative:
  - sessionResume  (SDK exposes listSessions / resumeSession)
  - effortControl  (SDK supports reasoningEffort low|medium|high|xhigh)
  - envInjection   (per-codebase env vars passed to spawned CLI)

Everything else (mcp, toolRestrictions, skills, structuredOutput, hooks,
sandbox, costControl, fallbackModel, thinkingControl) starts false;
flipping a flag requires wiring the corresponding plumbing first so the
dag-executor warnings stay honest.

New:
  - packages/providers/src/community/copilot/ (9 source files + 6 tests)

Modified:
  - packages/providers/package.json: pin @github/copilot-sdk@0.2.2 exact,
    add ./community/copilot export, extend test script
  - packages/providers/src/types.ts: CopilotProviderDefaults interface
  - packages/providers/src/registry.ts: registerCopilotProvider() call
  - packages/providers/src/index.ts: public re-exports
  - packages/providers/src/registry.test.ts: capability + registration
    + collision-with-pi tests
  - packages/core/src/config/config-types.ts: re-export defaults type
    (no intersection change — community providers live behind the
    generic [string] index)
  - .env.example: COPILOT_GITHUB_TOKEN / COPILOT_CLI_PATH hints
  - docs-web/.../ai-assistants.md: full Copilot section

Auth: delegates to Copilot CLI's credential resolution
(`gh auth login` / `COPILOT_GITHUB_TOKEN` / `assistants.copilot.githubToken`).
Permission callback is hard-wired to `approveAll` in v1 — matches the
trust model already enforced by worktree isolation.

SDK pinned exact rather than a range because `0.2.x` is explicitly in
public preview with breaking-change warnings; bumps should be deliberate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop-in reproducible dev environment. `devbox shell` gives contributors
bun, nodejs, git, gh, jq, postgresql-client, python3, and uv at pinned
versions without polluting the host. `bun` is pinned to `1.3.11` to
match the production Dockerfile, so local validation mirrors CI.

- devbox.json: package pins + shell scripts (setup/validate/dev/test)
- .gitignore: ignore the local `.devbox/` profile directory
  (devbox.json and devbox.lock stay tracked so pins are reproducible)
- CONTRIBUTING.md: short "Reproducible toolchain via Devbox" blurb in
  Getting Started

No behavior change — contributors who prefer their own toolchain are
unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`bun install --frozen-lockfile` (used by Dockerfile) requires the
lockfile to match package.json. Adds the SDK and its transitive deps
(@github/copilot platform-binary subpackages, vscode-jsonrpc, zod).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Loop nodes silently drop per-node provider/model/effort overrides
   (schema strips them, dispatcher tries to read them → always falls
   back to workflow-level provider).
2. Copilot provider can't locate its own bundled native CLI when the
   runtime is Bun — forces users to hand-wire a fragile node_modules
   path to work around the Node 24+ JS loader.

Both include repro, root cause, and suggested fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DAG node schema transform silently stripped `provider` and `model`
when constructing a LoopNode, even though the dag-executor's loop
dispatcher (dag-node.ts 2351-2391) reads both fields to pick the
per-iteration AI provider and model. End result: `provider: copilot`
on a loop node was a no-op — every iteration ran on the workflow-level
default.

- Include `provider`, `model`, and `shared` (retry) in the loop branch
  of the transform. Other aiOnly fields (effort, mcp, hooks, etc.) stay
  stripped because the loop executor doesn't consume them per-node —
  that matches LOOP_NODE_AI_FIELDS, which already documents this intent.
- Let the existing superRefine compatibility check (`isModelCompatible`)
  run against loop nodes too, so parse-time errors surface earlier than
  the runtime check in the dispatcher.
- Extend the existing "model/provider on loop nodes doesn't warn" test
  to also assert the fields round-trip through the schema — the
  assertion was missing, which is why the bug hid for so long.

Fixes BUG.md#1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`@github/copilot-sdk`'s default cliPath points at the JS loader in
`@github/copilot/npm-loader.js`, which is spawned with node. In the
stock archon Docker image `node` is Bun's node-compat shim, and the
loader's bundled entry imports `node:sea` (Node 20+) — so the CLI dies
at parse time before the SDK can use it.

The platform-specific native binary
(`@github/copilot-{platform}-{arch}/copilot`) ships as a peer install
and runs without any node runtime. It's been there all along; archon
just never looked for it.

- New `cli-resolver.ts` module with `resolveCopilotCliPath()` —
  three-tier fallback (config cliPath → COPILOT_CLI_PATH env →
  native-binary probe via createRequire). Falls through to `undefined`
  when the platform package isn't installed, so the SDK's existing
  default resolution still runs.
- Provider uses the new resolver instead of the manual two-tier chain.
- Unit tests cover config/env precedence and the
  fileExists-returns-false fallback.

Also reflow BUG.md (prettier) — no content changes.

Fixes BUG.md#2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`@github/copilot-sdk` emits every typed session event with the payload
wrapped under a `data` property (see `dist/generated/session-events.d.ts`).
archon's event-bridge reads at the top level, so every tool event decays
to `toolName: "unknown"` / `toolInput: {}` in the Web UI, and session
resume/token telemetry never gets populated.

- Add `unwrapEventData` — single point that pulls `event.data` with
  defensive fallbacks.
- Replace `extractToolEventFields` with separate start/complete extractors:
  - start: `data.toolName` + `data.arguments` + `data.toolCallId`.
  - complete: `data.toolCallId` + `data.success` (→ isError) +
    `data.result.detailedContent ?? data.result.content` (SDK's result is
    an object, not a string).
- Build a per-session `toolCallId → toolName` map on start events so the
  `tool_result` chunk reports the real tool name. Completion events only
  carry the call id.
- Subscribe to `session.start` for `sessionId` (terminal `session.idle`
  doesn't carry it) and to `assistant.usage` for per-turn token counts.
  `session.idle` becomes a plain "turn over" marker.
- Update bridge + provider tests to match the real wrapped shape
  (`{ type, data: {...} }`). The two previously-failing tests in
  event-bridge.test.ts are fixed along the way — they were failing
  because they asserted against the flat shape that never matched the
  real SDK.
- Pin `Array<T>` → `T[]` and silence a pre-existing
  `no-base-to-string` warning surfaced by lint on the edited file.

Fixes BUG.md#3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joaobmonteiro joaobmonteiro merged commit 12385b4 into dev Apr 20, 2026
3 checks passed
@joaobmonteiro joaobmonteiro deleted the feat/copilot-sdk-provider branch April 26, 2026 07:33
joaobmonteiro pushed a commit that referenced this pull request Apr 26, 2026
* feat: Add Telegram MarkdownV2 formatting for AI responses

- Add telegramify-markdown package to convert GitHub-flavored markdown
  to Telegram's MarkdownV2 format
- Create telegram-markdown utility with:
  - convertToTelegramMarkdown(): Main conversion function
  - stripMarkdown(): Fallback for plain text
  - escapeMarkdownV2(): Manual escaping helper
- Update TelegramAdapter to:
  - Format short messages with MarkdownV2
  - Split long messages by paragraphs for better formatting
  - Fallback to stripped plain text when MarkdownV2 parsing fails
- Add comprehensive tests for markdown conversion

Transforms AI responses from raw markdown (## headers, **bold**) to
properly formatted Telegram messages with bold text, code blocks, etc.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Correct telegram adapter test mocks and assertions

- Add missing stripMarkdown mock to telegram-markdown module mock
- Fix paragraph splitting test to use double newlines (\n\n) matching
  actual implementation behavior
- Use smaller paragraph sizes (3000 chars) to stay within MAX_LENGTH

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
joaobmonteiro pushed a commit that referenced this pull request Apr 26, 2026
When users request multiple issues to be fixed (e.g., "fix issues #1, #2,
and coleam00#3"), the skill now clearly instructs Claude to run each workflow
separately rather than combining them into a single command.
joaobmonteiro pushed a commit that referenced this pull request Apr 26, 2026
* Restructure ~/.archon/ to project-centric layout

Consolidate per-project files under ~/.archon/workspaces/owner/repo/ with
dedicated source/, worktrees/, artifacts/, and logs/ subdirectories.

Key changes:
- Add project-centric path utilities (getProjectRoot, getProjectSourcePath, etc.)
- Clone repos into workspaces/owner/repo/source/ instead of workspaces/owner/repo/
- Register local repos with symlink from source/ to actual path
- Move worktree base under project directory (avoid double-nesting owner/repo)
- Externalize artifacts via $ARTIFACTS_DIR variable (out of git repos)
- Externalize logs via logDir parameter (logger accepts dir directly)
- CLI auto-registers unregistered repos for all workflow runs
- Update all command templates to use $ARTIFACTS_DIR
- Strict owner/repo parsing (reject nested slashes)

* Fix symlink creation for empty source dirs and readonly SQLite results

Two bugs found during end-to-end testing:

1. createProjectSourceSymlink returned early for empty source/ directories
   created by ensureProjectStructure, preventing symlink creation for
   registered repos. Now checks if directory is empty before skipping.

2. registerRepoAtPath tried to assign properties on frozen SQLite result
   objects (getCodebaseCommands returns readonly parsed JSON in Bun's
   SQLite). Fixed by spreading into a mutable copy.

* Address PR review: empty catches, dynamic imports, duplicate DB lookup

- Critical #2-3: Replace empty catches on rm() with ENOENT-specific
  catches in createProjectSourceSymlink and cloneRepository. Permission
  errors and other failures now propagate instead of being swallowed.

- Important coleam00#4: Consolidate resolveArtifactsDir and resolveLogDir into
  a single resolveProjectPaths function that queries the database once
  instead of twice per workflow start.

- Important coleam00#6: Replace dynamic imports (readlink, rm) with static
  imports in archon-paths.ts and clone.ts.

* Add tests for project-centric path resolution and symlink creation

- Test resolveProjectPaths with valid codebase (project-scoped vs fallback)
- Test $ARTIFACTS_DIR substitution in workflow command prompts
- Test ensureProjectStructure creates all 4 subdirectories, idempotent
- Test createProjectSourceSymlink: create, no-op, conflict, clone case
- Test getWorktreeBase/isProjectScopedWorktreeBase with project-scoped paths

* Add path traversal guard to parseOwnerRepo

Reject path traversal characters (.., special chars, spaces) in owner/repo
segments. Inputs flow from user-facing web API (POST /api/codebases) through
parseOwnerRepo to filesystem path construction via join(), making this a
real attack surface, not a theoretical one.

* Delegate local paths in /clone to registerRepository (coleam00#383)

When /clone receives a local filesystem path (starting with /, ~, or .),
delegate to registerRepository instead of treating it as a URL. This
ensures the codebase name comes from the git remote (e.g. Wirasm/kild)
rather than filesystem path segments (e.g. mine/kild).

See coleam00#383 for the broader clone/register identity rethink.
joaobmonteiro pushed a commit that referenced this pull request Apr 26, 2026
…coleam00#1263)

* fix(bundled-defaults): auto-generate import list, emit inline strings

Root-cause fix for bundle drift (15 commands + 7 workflows previously
missing from binary distributions) and a prerequisite for packaging
@archon/workflows as a Node-loadable SDK.

The hand-maintained `bundled-defaults.ts` import list is replaced by
`scripts/generate-bundled-defaults.ts`, which walks
`.archon/{commands,workflows}/defaults/` and emits a generated source
file with inline string literals. `bundled-defaults.ts` becomes a thin
facade that re-exports the generated records and keeps the
`isBinaryBuild()` helper.

Inline strings (via JSON.stringify) replace Bun's
`import X from '...' with { type: 'text' }` attributes. The binary build
still embeds the data at compile time, but the module now loads under
Node too — removing SDK blocker #2.

- Generator: `scripts/generate-bundled-defaults.ts` (+ `--check` mode for CI)
- `package.json`: `generate:bundled`, `check:bundled`; wired into `validate`
- `build-binaries.sh`: regenerates defaults before compile
- Test: `bundle completeness` now derives expected set from on-disk files
- All 56 defaults (36 commands + 20 workflows) now in the bundle

* fix(bundled-defaults): address PR review feedback

Review: coleam00#1263 (comment)

Generator:
- Guard against .yaml/.yml name collisions (previously silent overwrite)
- Add early access() check with actionable error when run from wrong cwd
- Type top-level catch as unknown; print only message for Error instances
- Drop redundant /* eslint-disable */ emission (global ignore covers it)
- Fix misleading CI-mechanism claim in header comment
- Collapse dead `if (!ext) continue` guard into a single typed pass

Scripts get real type-checking + linting:
- New scripts/tsconfig.json extending root config
- type-check now includes scripts/ via `tsc --noEmit -p scripts/tsconfig.json`
- Drop `scripts/**` from eslint ignores; add to projectService file scope

Tests:
- Inline listNames helper (Rule of Three)
- Drop redundant toBeDefined/typeof assertions; the Record<string, string>
  type plus length > 50 already cover them
- Add content-fidelity round-trip assertion (defense against generator
  content bugs, not just key-set drift)

Facade comment: drop dead reference to .claude/rules/dx-quirks.md.

CI: wire `bun run check:bundled` into .github/workflows/test.yml so the
header's CI-verification claim is truthful.

Docs: CLAUDE.md step count four→five; add contributor bullet about
`bun run generate:bundled` in the Defaults section and CONTRIBUTING.md.

* chore(e2e): bump Codex model to gpt-5.2

gpt-5.1-codex-mini is deprecated and unavailable on ChatGPT-account Codex
auth. Plain gpt-5.2 works. Verified end-to-end:

- e2e-codex-smoke: structured output returns {category:'math'}
- e2e-mixed-providers: claude+codex both return expected tokens
joaobmonteiro pushed a commit that referenced this pull request Apr 26, 2026
…gent) (coleam00#1270)

* feat(providers): add Pi community provider (@mariozechner/pi-coding-agent)

Introduces Pi as the first community provider under the Phase 2 registry,
registered with builtIn: false. Wraps Pi's full coding-agent harness the
same way ClaudeProvider wraps @anthropic-ai/claude-agent-sdk and
CodexProvider wraps @openai/codex-sdk.

- PiProvider implements IAgentProvider; fresh AgentSession per sendQuery call
- AsyncQueue bridges Pi's callback-based session.subscribe() to Archon's
  AsyncGenerator<MessageChunk> contract
- Server-safe: AuthStorage.inMemory + SessionManager.inMemory +
  SettingsManager.inMemory + DefaultResourceLoader with all no* flags —
  no filesystem access, no cross-request state
- API key seeded per-call from options.env → process.env fallback
- Model refs: '<pi-provider-id>/<model-id>' (e.g. google/gemini-2.5-pro,
  openrouter/qwen/qwen3-coder) with syntactic compatibility check
- registerPiProvider() wired at CLI, server, and config-loader entrypoints,
  kept separate from registerBuiltinProviders() since builtIn: false is
  load-bearing for the community-provider validation story
- All 12 capability flags declared false in v1 — dag-executor warnings fire
  honestly for any unmapped nodeConfig field
- 58 new tests covering event mapping, async-queue semantics, model-ref
  parsing, defensive config parsing, registry integration

Supported Pi providers (v1): anthropic, openai, google, groq, mistral,
cerebras, xai, openrouter, huggingface. Extend PI_PROVIDER_ENV_VARS as
needed.

Out of scope (v1): session resume, MCP, hooks, skills mapping, thinking
level mapping, structured output, OAuth flows, model catalog validation.
These remain false on PI_CAPABILITIES until intentionally wired.

* feat(providers/pi): read ~/.pi/agent/auth.json for OAuth + api_key passthrough

Replaces the v1 env-var-only auth flow with AuthStorage.create(), which
reads ~/.pi/agent/auth.json. This transparently picks up credentials the
user has populated via `pi` → `/login` (OAuth subscriptions: Claude
Pro/Max, ChatGPT Plus, GitHub Copilot, Gemini CLI, Antigravity) or by
editing the file directly.

Env-var behavior preserved: when ANTHROPIC_API_KEY / GEMINI_API_KEY /
etc. is set (in process.env or per-request options.env), the adapter
calls setRuntimeApiKey which is priority #1 in Pi's resolution chain.
Auth.json entries are priority #2-coleam00#3. Pi's internal env-var fallback
remains priority coleam00#4 as a safety net.

Archon does not implement OAuth flows itself — it only rides on creds
the user created via the Pi CLI. OAuth refresh still happens inside Pi
(auth-storage.ts:369-413) under a file lock; concurrent refreshes
between the Pi CLI and Archon are race-safe by Pi's own design.

- Fail-fast error now mentions both the env-var path and `pi /login`
- 2 new tests: OAuth cred from auth.json; env var wins over auth.json
- 12 existing tests still pass (env-var-only path unchanged)

CI compatibility: no auth.json in CI, no change — env-var (secrets)
flows through Pi's getEnvApiKey fallback identically to v1.

* test(e2e): add Pi provider smoke test workflow

Mirrors e2e-claude-smoke.yaml: single prompt node + bash assert.
Targets `anthropic/claude-haiku-4-5` via `provider: pi`; works in CI
(ANTHROPIC_API_KEY secret) and locally (user's `pi /login` OAuth).

Verified locally with an Anthropic OAuth subscription — full run takes
~4s from session_started to assert PASS, exercising the async-queue
bridge and agent_end → result-chunk assembly under real Pi event timing.

Not yet wired into .github/workflows/e2e-smoke.yml — separate PR once
this lands, to keep the Pi provider PR minimal.

* feat(providers/pi): v2 — thinkingLevel, tool restrictions, systemPrompt

Extends the Pi adapter with three node-level translations, flipping the
corresponding capability flags from false → true so the dag-executor no
longer emits warnings for these fields on Pi nodes.

1. effort / thinking → Pi thinkingLevel (options-translator.ts)
   - Archon EffortLevel enum: low|medium|high|max (from
     packages/workflows/src/schemas/dag-node.ts). `max` maps to Pi's
     `xhigh` since Archon's enum lacks it.
   - Pi-native strings (minimal, xhigh, off) also accepted for
     programmatic callers bypassing the schema.
   - `off` on either field → no thinkingLevel (Pi's implicit off).
   - Claude-shape object `thinking: {type:'enabled', budget_tokens:N}`
     yields a system warning and is not applied.

2. allowed_tools / denied_tools → filtered Pi built-in tools
   - Supports all 7 Pi tools: read, bash, edit, write, grep, find, ls.
   - Case-insensitive normalization.
   - Empty `allowed_tools: []` means no tools (LLM-only), matching
     e2e-claude-smoke's idiom.
   - Unknown names (Claude-specific like `WebFetch`) collected and
     surfaced as a system warning; ignored tools don't fail the run.

3. systemPrompt (AgentRequestOptions + nodeConfig.systemPrompt)
   - Threaded through `DefaultResourceLoader({systemPrompt})`; Pi's
     default prompt is replaced entirely. Request-level wins over
     node-level.

Capability flag changes:
- thinkingControl: false → true
- effortControl:   false → true
- toolRestrictions: false → true

Package delta:
- +1 direct dep: @sinclair/typebox (Pi types reference it; adding as
  direct dep resolves the TS portable-type error).
- +1 test file: options-translator.test.ts (19 tests, 100% coverage).
- provider.test.ts extended with 11 new tests covering all three paths.
- registry.test.ts updated: capability assertion reflects new flags.

Live-verified: `bun run cli workflow run e2e-pi-smoke --no-worktree`
succeeds in 1.2s with thinkingLevel=low, toolCount=0. Smoke YAML updated
to use `effort: low` (schema-valid) + `allowed_tools: []` (LLM-only).

* test(e2e): add comprehensive Pi smoke covering every CI-compatible node type

Exercises every node type Archon supports under `provider: pi`, except
`approval:` (pauses for human input, incompatible with CI):
  1. prompt   — inline AI prompt
  2. command  — named command file (uses e2e-echo-command.md)
  3. loop     — bounded iterative AI prompt (max_iterations: 2)
  4. bash     — shell script with JSON output
  5. script   — bun runtime (echo-args.js)
  6. script   — uv / Python runtime (echo-py.py)

Plus DAG features on top of Pi:
  - depends_on + $nodeId.output substitution
  - when: conditional with JSON dot-access
  - trigger_rule: all_success merge
  - final assert node validates every upstream output is non-empty

Complements the minimal e2e-pi-smoke.yaml — that stays as the fast-path
smoke for connectivity checks; this one is the broader surface coverage.

Verified locally end-to-end against Anthropic OAuth (pi /login): PASS,
all 9 non-final nodes produce output, assert succeeds.

* feat(providers/pi): resolve Archon `skills:` names to Pi skill paths

Flips capabilities.skills: false → true by translating Archon's name-based
`skills:` nodeConfig (e.g. `skills: [agent-browser]`) to absolute directory
paths Pi's DefaultResourceLoader can consume via additionalSkillPaths.

Search order for each skill name (first match wins):
  1. <cwd>/.agents/skills/<name>/      — project-local, agentskills.io
  2. <cwd>/.claude/skills/<name>/      — project-local, Claude convention
  3. ~/.agents/skills/<name>/          — user-global, agentskills.io
  4. ~/.claude/skills/<name>/          — user-global, Claude convention

A directory resolves only if it contains a SKILL.md. Unresolved names are
collected and surfaced as a system-chunk warning (e.g. "Pi could not
resolve skill names: foo, bar. Searched .agents/skills and .claude/skills
(project + user-global)."), matching the semantic of "requested but not
found" without aborting the run.

Pi's buildSystemPrompt auto-appends the agentskills.io XML block for each
loaded skill, so the model sees them — no separate prompt injection needed
(Pi differs from Claude here; Claude wraps in an AgentDefinition with a
preloaded prompt, Pi uses XML block in system prompt).

Ancestor directory traversal above cwd is deliberately skipped in this
pass — matches the Pi provider's cwd-bound scope and avoids ambiguity
about which repo's skills win when Archon runs from a subdirectory.

Bun's os.homedir() bypasses the HOME env var; the resolver uses
`process.env.HOME ?? homedir()` so tests can stage a synthetic home dir.

Tests:
- 11 new tests in options-translator.test.ts cover project/user, .agents/
  vs .claude/, project-wins-over-user, SKILL.md presence check, dedup,
  missing-name collection.
- 2 new integration tests in provider.test.ts cover the missing-skill
  warning path and the "no skills configured → no additionalSkillPaths"
  path.
- registry.test.ts updated to assert skills: true in capabilities.

Live-verified locally: `.claude/skills/archon-dev/SKILL.md` resolves,
pi.session_started log shows `skillCount: 1, missingSkillCount: 0`,
smoke workflow passes in 1.2s.

* feat(providers/pi): session resume via Pi session store

Flips capabilities.sessionResume: false → true. Pi now persists sessions
under ~/.pi/agent/sessions/<encoded-cwd>/<uuid>.jsonl by default — same
pattern Claude and Codex use for their respective stores, same blast
radius as those providers.

Flow:
  - No resumeSessionId → SessionManager.create(cwd) (fresh, persisted)
  - resumeSessionId + match in SessionManager.list(cwd) → open(path)
  - resumeSessionId + no match → fresh session + system warning
    ("⚠️ Could not resume Pi session. Starting fresh conversation.")
    Matches Codex's resume_thread_failed fallback at
    packages/providers/src/codex/provider.ts:553-558.

The sessionId flows back to Archon via the terminal `result` chunk —
bridgeSession annotates it with session.sessionId unconditionally so
Archon's orchestrator can persist it and pass it as resumeSessionId on
the next turn. Same mechanism used for Claude/Codex.

Cross-cwd resume (e.g. worktree switch) is deliberately not supported in
this pass: list(cwd) scans only the current cwd's session dir. A workflow
that changes cwd mid-run lands on a fresh session, which matches Pi's
mental model.

Bridge sessionId annotation uses session.sessionId, which Pi always
populates (UUID) — so no special-case for inMemory sessions is needed.

Factored the resolver into session-resolver.ts (5 unit tests):
  - no id → create
  - id + match → open
  - id + no match → create with resumeFailed: true
  - list() throws → resumeFailed: true (graceful)
  - empty-string id → treated as "no resume requested"

Integration tests in provider.test.ts add 3 cases:
  - resume-not-found yields warning + calls create
  - resume-match calls open with the file path, no warning
  - result chunk always carries sessionId

Verified live end-to-end against Anthropic OAuth:
  - first call → sessionId 019d...; model replies "noted"
  - second call with that sessionId → "resumed: true" in logs; model
    correctly recalls prior turn ("Crimson.")
  - bogus sessionId → "⚠️ Could not resume..." warning + fresh UUID

* refactor(providers,core): generalize community-provider registration

Addresses the community-pattern regression flagged in the PR coleam00#1270 review:
a second community provider should require editing only its own directory,
not seven files across providers/ + core/ + cli/ + server/.

Three changes:

1. Drop typed `pi` slot from AssistantDefaultsConfig + AssistantDefaults.
   Community providers live behind the generic `[string]` index that
   `ProviderDefaultsMap` was explicitly designed to provide. The typed
   claude/codex slots stay — they give IDE autocomplete for built-in
   config access without `as` casts, which was the whole reason the
   intersection exists. Community providers parse their own config via
   Record<string, unknown> anyway, so the typed slot added no real
   parser safety.

2. Loop-based getDefaults + mergeAssistantDefaults. No more hardcoded
   `pi: {}` spreads. getDefaults() seeds from `getRegisteredProviders()`;
   mergeAssistantDefaults clones every slot present in `base`. Adding a
   new provider requires zero edits to this function.

3. New `registerCommunityProviders()` aggregator in registry.ts.
   Entrypoints (CLI, server, config-loader) call ONE function after
   `registerBuiltinProviders()` rather than one call per community
   provider. Adding a new community provider is now a single-line edit
   to registerCommunityProviders().

This makes Pi (and future community providers) actually behave like
Phase 2 (coleam00#1195) advertised: drop the implementation under
packages/providers/src/community/<id>/, export a `register<Id>Provider`,
add one line to the aggregator.

Tests:
- New `registerCommunityProviders` suite (2 tests: registers pi,
  idempotent).
- config-loader.test updated: assert built-in slots explicitly rather
  than exhaustive map shape.

No functional change for Pi end-users. Purely structural.

* fix(providers/pi,core): correctness + hygiene fixes from PR coleam00#1270 review

Addresses six of the review's important findings, all within the same
PR branch:

1. envInjection: false → true
   The provider reads requestOptions.env on every call (for API-key
   passthrough). Declaring the capability false caused a spurious
   dag-executor warning for every Pi user who configured codebase env
   vars — which is the MAIN auth path. Flipping to true removes the
   false positive.

2. toSafeAssistantDefaults: denylist → allowlist
   The old shape deleted `additionalDirectories`, `settingSources`,
   `codexBinaryPath` before sending defaults to the web UI. Any future
   sensitive provider field (OAuth token, absolute path, internal
   metadata) would silently leak via the `[key: string]: unknown` index
   signature. New SAFE_ASSISTANT_FIELDS map lists exactly what to
   expose per provider; unknown providers get an empty allowlist so
   the web UI sees "provider exists" but no config details.

3. AsyncQueue single-consumer invariant
   The type was documented single-consumer but unenforced. A second
   `for await` would silently race with the first over buffer +
   waiters. Added a synchronous guard in Symbol.asyncIterator that
   throws on second call — copy-paste mistakes now fail fast with a
   clear message instead of dropping items.

4. session.dispose() / session.abort() silent catches
   Both catch blocks now log at debug via a module-scoped logger so
   SDK regressions surface without polluting normal output.

5. Type scripted events as AgentSessionEvent in provider.test.ts
   Was `Record<string, unknown>` — Pi field renames would silently
   keep tests passing. Now typed against Pi's actual event union.

6. Leaked /tmp/pi-research/... path in provider.ts comment
   Local-machine path that crept in during research. Replaced with
   the upstream GitHub URL (matches convention at provider.ts:110).

Plus review-flagged simplifications:
  - Extract lookupPiModel wrapper — isolates the `as unknown as` cast
    behind one searchable name.
  - Hoist QueueItem → BridgeQueueItem at module scope (export'd for
    test visibility; not used externally yet but enables unit testing
    the mapping in isolation if needed later).
  - getRegisteredProviderNames: remove side-effecting registration
    calls. `loadConfig()` already bootstraps the registry before any
    caller can observe this helper — the hidden coupling was
    misleading.

Plus missing-coverage tests from the review (pr-test-analyzer):
  - session.prompt() rejection → error surfaces to consumer
  - pre-aborted signal → session.abort() called
  - mid-stream abort → session.abort() called
  - modelFallbackMessage → system chunk yielded
  - AsyncQueue second-consumer → throws synchronously

No behavioral changes for end users beyond the envInjection warning
fix.

* docs: Pi provider + community-provider contributor guide

Addresses the PR coleam00#1270 review's docs-impact findings: the original Pi
PR had no user-facing or contributor-facing documentation, and
architecture.md still referenced the pre-Phase-2 factory.ts pattern
(factory.ts was deleted in coleam00#1195).

1. packages/docs-web/src/content/docs/reference/architecture.md
   - Replace stale factory.ts references with the registry pattern.
   - Update inline IAgentProvider block: add getCapabilities, add
     options parameter.
   - Rewrite MessageChunk block as the actual discriminated union
     (was a placeholder with optional fields that didn't match the
     current type).
   - "Adding a New AI Agent Provider" checklist now distinguishes
     built-in (register in registerBuiltinProviders) from community
     (separate guide). Links to the new contributor guide.

2. packages/docs-web/src/content/docs/contributing/adding-a-community-provider.md (new)
   - Step-by-step guide using Pi as the reference implementation.
   - Covers: directory layout, capability discipline (start false,
     flip one at a time), provider class skeleton, registration via
     aggregator, test isolation (Bun mock.module pollution), what
     NOT to do (no edits to AssistantDefaultsConfig, no direct
     registerProvider from entrypoints, no overclaiming capabilities).

3. packages/docs-web/src/content/docs/getting-started/ai-assistants.md
   - New "Pi (Community Provider)" section: install, OAuth +
     API-key table per Pi backend, model ref format, workflow
     examples, capability matrix showing what Pi supports (session
     resume, tool restrictions, effort/thinking, skills, system
     prompt, envInjection) and what it doesn't (MCP, hooks,
     structured output, cost control, fallback model, sandbox).

4. .env.example
   - New Pi section with commented env vars for each supported
     backend (ANTHROPIC_API_KEY through HUGGINGFACE_API_KEY), each
     paired with its Pi provider id. OAuth flow (pi /login → auth.json)
     is explicitly called out — Archon reads that file too.

5. CHANGELOG.md
   - Unreleased entry for Pi, registerCommunityProviders aggregator,
     and the new contributor guide.
joaobmonteiro added a commit that referenced this pull request Apr 26, 2026
feat(providers): add GitHub Copilot as a community provider
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

doesn't know what o1-mini is, or how to route to openrouter.ai

2 participants