From f0c078102c84af5566cbc88eebd9e283824b2e91 Mon Sep 17 00:00:00 2001 From: "poe-code-agent[bot]" <254571472+poe-code-agent[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 00:31:43 +0000 Subject: [PATCH 1/7] docs: document toolcraft group help parameters --- packages/toolcraft/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/toolcraft/README.md b/packages/toolcraft/README.md index 4e7d3a683..6dbb7cf22 100644 --- a/packages/toolcraft/README.md +++ b/packages/toolcraft/README.md @@ -178,6 +178,8 @@ The same `root` flows into all three. No duplication. **Tree**: the `root` group is a `defineGroup` whose children are commands and sub-groups. Any depth. CLI flags, MCP tool names, and SDK methods are derived from the path. +**CLI help**: group help lists visible child commands with their parameter tokens inline. Required options appear as `--name `, optional options and defaults appear in brackets like `[--limit ]`, and positional parameters stay unbracketed even when the underlying schema field is optional. Command-specific `--help` still shows the detailed parameter table. + ## Secrets Declare env-backed secrets on a command or group. Toolcraft reads `process.env` at command-run time and passes the values to the handler: From d9e72eea2e7e0fa38f536a53113c2fdf8d2d9195 Mon Sep 17 00:00:00 2001 From: "poe-code-agent[bot]" <254571472+poe-code-agent[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 00:31:05 +0000 Subject: [PATCH 2/7] docs: update provider and goose auth notes --- packages/agent-spawn/README.md | 2 +- packages/providers/README.md | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/agent-spawn/README.md b/packages/agent-spawn/README.md index 0a0eca55f..12f9adcdb 100644 --- a/packages/agent-spawn/README.md +++ b/packages/agent-spawn/README.md @@ -73,4 +73,4 @@ vi.mock("@poe-code/agent-spawn", spawnMock.factory); ## Environment variables -This package does not expose public environment variables. It inherits `process.env` for child processes and may add agent-specific env overrides from declarative spawn config, such as `GOOSE_MODE` for Goose modes or `OPENCODE_CONFIG_CONTENT` for OpenCode MCP injection. +This package does not expose public environment variables. It inherits `process.env` for child processes and may add agent-specific env overrides from declarative spawn config, such as `GOOSE_MODE` for Goose modes, `GOOSE_DISABLE_KEYRING=1` for Goose file-backed credentials, or `OPENCODE_CONFIG_CONTENT` for OpenCode MCP injection. diff --git a/packages/providers/README.md b/packages/providers/README.md index 4e859421b..89c1df15b 100644 --- a/packages/providers/README.md +++ b/packages/providers/README.md @@ -69,9 +69,14 @@ const apiKey = await apiKeyAuthStrategy.resolveCredential(anthropic, { secretSto `ProviderRegistry.login()` resolves API keys in this order: -1. Explicit `options.apiKey` -2. The provider's declared `auth.envVar` from `context.envVars` -3. `promptForSecret` +1. `context.resolvePreferredLogin` when the provider declares `auth.preferredLogin` +2. Explicit `options.apiKey` +3. The provider's declared `auth.envVar` from `context.envVars` +4. `promptForSecret` + +`preferredLogin` is optional. It lets a provider prefer a custom login flow, such as OAuth, +while still storing the resulting API key through the same provider secret store. If no +`resolvePreferredLogin` callback is supplied, login falls back to the generic API-key flow. `ProviderRegistry.isLoggedIn()` also treats a non-empty declared env var as logged in, matching what `login()` would use in CI. @@ -92,5 +97,15 @@ Declared environment variables: ## Configuration options No runtime configuration; everything is declared per-provider via the `AuthProvider` -manifest. Supported provider config fields include `baseUrl`, `baseUrlEnvVar`, -`requiresBaseUrl`, `modelInput`, `auth`, `apiShapes`, and `env`. +manifest. + +Provider manifest options: + +- `id`, `label`, `summary`, `baseUrl`, `baseUrlEnvVar`, `requiresBaseUrl`, and + `modelInput` +- `auth.kind: "api-key"` with `envVar`, `storageKey`, `prompt`, and optional + `preferredLogin: "oauth"` +- `auth.kind: "oauth"` for OAuth-native providers +- `apiShapes` for provider API compatibility and shape-specific URL suffixes/defaults +- `env` for provider-specific environment values derived from literals, credentials, base + URLs, or provider fields From 61d49ad03181198579d175aac9bf5eba55eeb18b Mon Sep 17 00:00:00 2001 From: "poe-code-agent[bot]" <254571472+poe-code-agent[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 00:27:33 +0000 Subject: [PATCH 3/7] docs: document config self-discovery handling --- packages/config-extends/README.md | 16 ++++++++++++++-- packages/poe-code-config/README.md | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/config-extends/README.md b/packages/config-extends/README.md index 6764f70ca..ae059c39b 100644 --- a/packages/config-extends/README.md +++ b/packages/config-extends/README.md @@ -1,10 +1,22 @@ # @poe-code/config-extends -Shared document-inheritance types for layered config resolution. +Shared document-inheritance utilities for layered config resolution. ## API -Currently this package exposes shared TypeScript types only. +- `resolve(chain, options)`: resolves exactly one document layer with surrounding data and base layers. +- `findBase(name, basePaths, fs)`: discovers a base document by name across configured base paths. +- `parseDocument(content, filePath)`: parses a document and separates inheritance metadata from data. +- `mergeLayers(layers)`: merges data layers and tracks the source of each resolved key. + +## Resolution behavior + +A chain must contain exactly one document layer. Data layers before and after the document are merged around the resolved document, and base layers define directories that can be inherited from. + +- Documents that set `extends: true` must resolve a base and still report circular inheritance as an error. +- With `autoExtend: true`, documents that do not set `extends` try to inherit from matching bases automatically. +- Optional auto-extend discovery is ignored when it finds the document itself, so a document can safely live in a configured base directory without creating a circular extends error. +- Prompt values can compose with the `{{yield}}` token across resolved base layers. ## Environment variables diff --git a/packages/poe-code-config/README.md b/packages/poe-code-config/README.md index 96670707d..804b83d56 100644 --- a/packages/poe-code-config/README.md +++ b/packages/poe-code-config/README.md @@ -78,6 +78,8 @@ Project config is read as an override on top of global config. - Keys inside a scope are merged. - When the same key exists in both places, the project value wins. - Missing or `undefined` project keys do not remove global values. +- If the project config path resolves to the global config path, only the global document is read and no self-merge is attempted. +- Project config reads auto-extend from the global config directory, but self-discovered optional bases are ignored by `@poe-code/config-extends`. Example: From bbaec2831b310e7d71413f0cb5f9ea21b2587cb6 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Tue, 5 May 2026 11:51:32 -0500 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: poe-code-agent[bot] <254571472+poe-code-agent[bot]@users.noreply.github.com> --- packages/toolcraft/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolcraft/README.md b/packages/toolcraft/README.md index 6dbb7cf22..b1d2afd9f 100644 --- a/packages/toolcraft/README.md +++ b/packages/toolcraft/README.md @@ -178,7 +178,7 @@ The same `root` flows into all three. No duplication. **Tree**: the `root` group is a `defineGroup` whose children are commands and sub-groups. Any depth. CLI flags, MCP tool names, and SDK methods are derived from the path. -**CLI help**: group help lists visible child commands with their parameter tokens inline. Required options appear as `--name `, optional options and defaults appear in brackets like `[--limit ]`, and positional parameters stay unbracketed even when the underlying schema field is optional. Command-specific `--help` still shows the detailed parameter table. +**CLI help**: group help lists visible child commands with their parameter tokens inline. Required options appear as `--name `, optional options and defaults appear in brackets like `[--limit ]`, and positional parameters render as positional tokens like `` or `[name]` depending on whether they are required. Command-specific `--help` still shows the detailed parameter table. ## Secrets From 096bb6877159d3b0c2a635b86bb2ca948fce613e Mon Sep 17 00:00:00 2001 From: "poe-code-agent[bot]" <254571472+poe-code-agent[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 00:33:23 +0000 Subject: [PATCH 5/7] docs: document github workflow run failures --- packages/agent-spawn/README.md | 38 +++++----- packages/github-workflows/README.md | 105 +++++++++++++++++----------- packages/toolcraft/README.md | 59 +++++++++------- 3 files changed, 114 insertions(+), 88 deletions(-) diff --git a/packages/agent-spawn/README.md b/packages/agent-spawn/README.md index 12f9adcdb..efa06e318 100644 --- a/packages/agent-spawn/README.md +++ b/packages/agent-spawn/README.md @@ -13,8 +13,8 @@ const result = await spawn("codex", { mode: "edit", model: "openai/gpt-5.5", mcpServers: { - fs: { command: "node", args: ["./mcp/fs.js"], timeout: 30 }, - }, + fs: { command: "node", args: ["./mcp/fs.js"], timeout: 30 } + } }); console.log(result.exitCode, result.stdout); @@ -23,11 +23,11 @@ console.log(listMcpSupportedAgents()); ## Spawn modes -| Mode | Purpose | -|------|---------| -| `yolo` | Full automation for trusted tasks. | +| Mode | Purpose | +| ------ | ------------------------------------------------------------- | +| `yolo` | Full automation for trusted tasks. | | `edit` | File-editing mode when the agent supports scoped permissions. | -| `read` | Read-only/research mode when the agent supports it. | +| `read` | Read-only/research mode when the agent supports it. | Mode-specific args and env vars are declared in each agent config. Goose uses `GOOSE_MODE` internally for mode selection; callers do not need to set it manually. @@ -48,7 +48,7 @@ import { createSpawnMock } from "@poe-code/agent-spawn/testing"; const spawnMock = createSpawnMock({ spawnResult: { stdout: "ok" }, - autonomousResult: { text: "done" }, + autonomousResult: { text: "done" } }); vi.mock("@poe-code/agent-spawn", spawnMock.factory); @@ -58,18 +58,18 @@ vi.mock("@poe-code/agent-spawn", spawnMock.factory); ## Config options -| Option | Type | Description | -|--------|------|-------------| -| `prompt` | `string` | Prompt sent to the agent. | -| `cwd` | `string` | Working directory. Defaults to the caller's process cwd. | -| `model` | `string` | Optional model override. Provider prefixes are stripped or preserved per agent config. | -| `mode` | `"yolo" \| "edit" \| "read"` | Permission mode. Defaults are chosen by the caller. | -| `args` | `string[]` | Extra args forwarded to the agent process. | -| `mcpServers` | `Record` | MCP servers injected into the spawned agent. | -| `useStdin` | `boolean` | Send the prompt through stdin when the agent supports it. | -| `interactive` | `boolean` | Spawn the agent in interactive TUI mode. | -| `activityTimeoutMs` | `number` | Kill/retry inactive streaming processes after this many milliseconds. | -| `logPath` / `logDir` / `logFileName` | `string` | Persist spawn logs. `logPath` takes precedence. | +| Option | Type | Description | +| ------------------------------------ | -------------------------------- | -------------------------------------------------------------------------------------- | +| `prompt` | `string` | Prompt sent to the agent. | +| `cwd` | `string` | Working directory. Defaults to the caller's process cwd. | +| `model` | `string` | Optional model override. Provider prefixes are stripped or preserved per agent config. | +| `mode` | `"yolo" \| "edit" \| "read"` | Permission mode. Defaults are chosen by the caller. | +| `args` | `string[]` | Extra args forwarded to the agent process. | +| `mcpServers` | `Record` | MCP servers injected into the spawned agent. | +| `useStdin` | `boolean` | Send the prompt through stdin when the agent supports it. | +| `interactive` | `boolean` | Spawn the agent in interactive TUI mode. | +| `activityTimeoutMs` | `number` | Kill/retry inactive streaming processes after this many milliseconds. | +| `logPath` / `logDir` / `logFileName` | `string` | Persist spawn logs. `logPath` takes precedence. | ## Environment variables diff --git a/packages/github-workflows/README.md b/packages/github-workflows/README.md index d923b731b..af9424229 100644 --- a/packages/github-workflows/README.md +++ b/packages/github-workflows/README.md @@ -18,11 +18,11 @@ npm run lint:mcp-sdk-deps --workspace @poe-code/github-workflows Go to your repository **Settings → Secrets and variables → Actions** and add: -| Secret | Description | -|--------|-------------| -| `POE_API_KEY` | Your Poe API key — required for all automations | -| `POE_CODE_AGENT_APP_ID` | GitHub App ID — used to generate a scoped token for each run | -| `POE_CODE_AGENT_PRIVATE_KEY` | GitHub App private key (PEM format) | +| Secret | Description | +| ---------------------------- | ------------------------------------------------------------ | +| `POE_API_KEY` | Your Poe API key — required for all automations | +| `POE_CODE_AGENT_APP_ID` | GitHub App ID — used to generate a scoped token for each run | +| `POE_CODE_AGENT_PRIVATE_KEY` | GitHub App private key (PEM format) | ### 2. Install a workflow @@ -33,6 +33,7 @@ poe-code gh install ``` This creates: + - `.github/workflows/poe-code-.yml` — the GitHub Actions workflow - `.github/workflows/variables.yaml` — optional shared prompt variable overrides - `.github/workflows/README.md` — a local command reference for workflow helpers @@ -47,20 +48,24 @@ The workflow triggers automatically on the configured event. ## Built-in Automations -| Name | Trigger | Description | -|------|---------|-------------| -| `github-issue-opened` | Issue opened | Reads the issue and implements the requested changes | -| `github-issue-comment-created` | Issue comment created | Acts on prefixed issue comments, restricted to allowed roles | -| `github-pull-request-opened` | PR opened | Reviews the pull request | -| `github-pull-request-synchronized` | PR updated | Re-reviews the PR after new commits are pushed | -| `fix-vulnerabilities` | Scheduled | Fetches open Dependabot alerts and fixes them one by one | -| `update-dependencies` | Manual / scheduled | Updates all dependencies to latest compatible versions | -| `update-documentation` | Manual / scheduled | Reviews code changes and updates documentation | +| Name | Trigger | Description | +| ---------------------------------- | --------------------- | ---------------------------------------------------------------------------------------- | +| `github-issue-opened` | Issue opened | Reads the issue, asks for missing details when needed, and implements actionable changes | +| `github-issue-comment-created` | Issue comment created | Acts on prefixed issue comments, restricted to allowed roles | +| `github-pull-request-opened` | PR opened | Reviews the pull request | +| `github-pull-request-synchronized` | PR updated | Re-reviews the PR after new commits are pushed | +| `fix-vulnerabilities` | Scheduled | Fetches open Dependabot alerts and fixes them one by one | +| `update-dependencies` | Manual / scheduled | Updates all dependencies to latest compatible versions | +| `update-documentation` | Manual / scheduled | Reviews code changes and updates documentation | The built-in GitHub issue, issue-comment, pull-request, pull-request-comment, and pull-request-synchronized automations allow `OWNER`, `MEMBER`, `COLLABORATOR`, and `CONTRIBUTOR` author associations by default. +The built-in issue-opened automation always leaves a visible response. When the +issue lacks enough detail for a code change, it asks for the missing details and +leaves the issue open instead of closing it. + ### List available automations ```bash @@ -85,6 +90,7 @@ poe-code gh uninstall Use `--eject` when you need full control over the workflow YAML itself. With `--eject`, poe-code writes: + - `.github/workflows/poe-code-.yml` - `.github/workflows/poe-code-.md` @@ -99,6 +105,7 @@ You can override specific automation fields (agent, allowed roles, prompt, etc.) ### Change the agent `.github/workflows/poe-code-github-issue-opened.md`: + ```yaml --- extends: true @@ -111,6 +118,7 @@ This keeps the built-in prompt, template variables, and all other settings — o ### Override multiple fields `.github/workflows/poe-code-github-issue-opened.md`: + ```yaml --- extends: true @@ -124,6 +132,7 @@ allow: ### Add a custom prompt while inheriting config `.github/workflows/poe-code-github-issue-opened.md`: + ```yaml --- extends: true @@ -143,6 +152,7 @@ When using `extends`, you can use the `{{yield}}` token to compose prompts inste **Wrap the built-in prompt with extra instructions:** `.github/workflows/poe-code-github-issue-opened.md`: + ```yaml --- extends: true @@ -165,6 +175,7 @@ Result: "Read {{url}}.\n\nFocus on test coverage.\n\nAlways explain what changed ``` Rules: + - Only one `{{yield}}` per prompt is allowed. - If neither side uses `{{yield}}`, the child prompt replaces the base (existing behavior). - If the child has no prompt body, `{{yield}}` resolves to an empty string. @@ -172,12 +183,12 @@ Rules: ### `extends` vs `--eject` -| | `extends` | `--eject` | -|---|---|---| -| Receives upstream prompt updates | Yes (unless you provide a body) | No | -| Receives upstream workflow YAML updates | Yes | No | -| Can change agent/allow/prefix/mcp | Yes | Yes | -| Can modify the workflow YAML | No | Yes | +| | `extends` | `--eject` | +| --------------------------------------- | ------------------------------- | --------- | +| Receives upstream prompt updates | Yes (unless you provide a body) | No | +| Receives upstream workflow YAML updates | Yes | No | +| Can change agent/allow/prefix/mcp | Yes | Yes | +| Can modify the workflow YAML | No | Yes | --- @@ -187,27 +198,28 @@ Automation prompts support the following frontmatter (both in ejected files and ```yaml --- -label: "GitHub: Issue Handler" # Display label (defaults to formatted name) +label: "GitHub: Issue Handler" # Display label (defaults to formatted name) -agent: "claude-code" # Agent to spawn (default: "codex") +agent: "claude-code" # Agent to spawn (default: "codex") -allow: # GitHub author associations allowed to trigger +allow: # GitHub author associations allowed to trigger - OWNER - MEMBER - COLLABORATOR - CONTRIBUTOR -prefix: # Required comment prefix or aliases (comment workflows only) +prefix: # Required comment prefix or aliases (comment workflows only) - "poe-code" - "poe-code-agent" - "@poe-code-agent" -source: "gh api repos/{owner}/{repo}/dependabot/alerts --jq '[.[]]'" - # Command to fetch items; must output a JSON array. - # The automation runs once per item. - # Supports {owner} and {repo} placeholders. +source: + "gh api repos/{owner}/{repo}/dependabot/alerts --jq '[.[]]'" + # Command to fetch items; must output a JSON array. + # The automation runs once per item. + # Supports {owner} and {repo} placeholders. -mcp: # MCP servers to make available to the agent +mcp: # MCP servers to make available to the agent github: command: "npx" args: @@ -233,14 +245,14 @@ mcp: # MCP servers to make available to the age Variables available in the prompt body depend on the trigger: -| Variable | Available in | -|----------|-------------| -| `{{url}}` | All | -| `{{repo}}` | All | -| `{{issue.number}}`, `{{issue.title}}` | Issue workflows | -| `{{pr.number}}`, `{{pr.title}}`, `{{pr.author}}` | PR workflows | -| `{{comment.author}}`, `{{comment.body}}` | Issue-comment workflows | -| `{{}}` | Sourced automations — any field from the JSON item | +| Variable | Available in | +| ------------------------------------------------ | -------------------------------------------------- | +| `{{url}}` | All | +| `{{repo}}` | All | +| `{{issue.number}}`, `{{issue.title}}` | Issue workflows | +| `{{pr.number}}`, `{{pr.title}}`, `{{pr.author}}` | PR workflows | +| `{{comment.author}}`, `{{comment.body}}` | Issue-comment workflows | +| `{{}}` | Sourced automations — any field from the JSON item | --- @@ -259,13 +271,22 @@ The workflow also: --- +## Run Failure Behavior + +`poe-code gh run ` fails when any spawned agent run exits non-zero. This +applies to direct automations and sourced automations that fan out across JSON +items. The error reports the success count, the first failed exit code, and the +first failed run's stderr/stdout when present. + +--- + ## Environment Variables -| Variable | Required | Description | -|----------|----------|-------------| -| `POE_API_KEY` | Yes | Poe API key — must be set as a repository secret | -| `POE_CODE_AGENT_APP_ID` | Yes | GitHub App ID — must be set as a repository secret | -| `POE_CODE_AGENT_PRIVATE_KEY` | Yes | GitHub App private key (PEM) — must be set as a repository secret | +| Variable | Required | Description | +| ---------------------------- | -------- | ----------------------------------------------------------------- | +| `POE_API_KEY` | Yes | Poe API key — must be set as a repository secret | +| `POE_CODE_AGENT_APP_ID` | Yes | GitHub App ID — must be set as a repository secret | +| `POE_CODE_AGENT_PRIVATE_KEY` | Yes | GitHub App private key (PEM) — must be set as a repository secret | `GITHUB_TOKEN` is generated at runtime from the GitHub App credentials using `actions/create-github-app-token`. Pass it to MCP servers via `${{ GITHUB_TOKEN }}` in the frontmatter `mcp.env` block when needed. diff --git a/packages/toolcraft/README.md b/packages/toolcraft/README.md index b1d2afd9f..54425ed5b 100644 --- a/packages/toolcraft/README.md +++ b/packages/toolcraft/README.md @@ -59,12 +59,12 @@ export const greet = defineCommand({ description: "Say hello", params: S.Object({ name: S.String({ description: "Who to greet" }), - loud: S.Optional(S.Boolean({ default: false })), + loud: S.Optional(S.Boolean({ default: false })) }), handler: async ({ params }) => { const message = `Hello, ${params.name}`; return { message: params.loud ? message.toUpperCase() : message }; - }, + } }); ``` @@ -75,7 +75,7 @@ import { greet } from "./commands/greet.js"; export const root = defineGroup({ name: "mytool", - children: [greet], + children: [greet] }); ``` @@ -190,12 +190,12 @@ const deploy = defineCommand({ params: S.Object({ service: S.String() }), secrets: { apiKey: { env: "DEPLOY_API_KEY", description: "Required for /deploy endpoint" }, - debugToken: { env: "DEPLOY_DEBUG", optional: true }, + debugToken: { env: "DEPLOY_DEBUG", optional: true } }, handler: async ({ params, secrets }) => { // secrets.apiKey: string // secrets.debugToken: string | undefined - }, + } }); ``` @@ -207,13 +207,14 @@ Required secrets that aren't set produce a `UserError` with the env var name and defineCommand({ // ... requires: { - auth: true, // fails if POE_API_KEY (or runner-specified env) is missing - apiVersion: ">=1.2.0", // fails if runner reports older apiVersion - check: async (ctx) => ({ // arbitrary async gate + auth: true, // fails if POE_API_KEY (or runner-specified env) is missing + apiVersion: ">=1.2.0", // fails if runner reports older apiVersion + check: async (ctx) => ({ + // arbitrary async gate ok: ctx.fs.exists(".lock") === false, - message: ".lock present, refusing to run", - }), - }, + message: ".lock present, refusing to run" + }) + } }); ``` @@ -241,7 +242,7 @@ defineCommand({ handler: async ({ params, db, logger }) => { logger.info("running"); return db.query(params.id); - }, + } }); ``` @@ -259,8 +260,8 @@ defineCommand({ rich: (result, { renderTable }) => console.log(renderTable({ rows: result.rows, columns: ["id"] })), markdown: (result) => `Found ${result.rows.length} rows`, - json: (result) => result, - }, + json: (result) => result + } }); ``` @@ -275,13 +276,13 @@ defineGroup({ name: "github", mcp: { transport: "stdio", - command: "github-mcp-server", + command: "github-mcp-server" }, tools: ["create_issue", "list_issues"], rename: { - create_issue: "issues.create", + create_issue: "issues.create" }, - children: [], + children: [] }); ``` @@ -303,21 +304,21 @@ defineGroup({ name: "deploy", humanInLoop: { mode: "async", - message: ({ commandPath, params }) => `Run ${commandPath} for ${params.target}?`, + message: ({ commandPath, params }) => `Run ${commandPath} for ${params.target}?` }, children: [ defineCommand({ name: "prod", params: S.Object({ target: S.String() }), - handler: async ({ params }) => ({ target: params.target }), + handler: async ({ params }) => ({ target: params.target }) }), defineCommand({ name: "preview", params: S.Object({ target: S.String() }), - humanInLoop: null, // opt out - handler: async ({ params }) => ({ target: params.target }), - }), - ], + humanInLoop: null, // opt out + handler: async ({ params }) => ({ target: params.target }) + }) + ] }); ``` @@ -331,7 +332,7 @@ Wire the same `humanInLoop` options into every entrypoint: ```ts const humanInLoop = { provider: slackApprovalProvider({ channel: "#deploys", client }), - taskList: { dir: ".toolcraft/approvals.yaml", format: "yaml-file" as const }, + taskList: { dir: ".toolcraft/approvals.yaml", format: "yaml-file" as const } }; await runCLI(root, { humanInLoop }); @@ -356,7 +357,11 @@ Async results must be JSON-serializable; non-serializable returns mark the appro A minimal Slack-style provider: ```ts -import type { ApprovalRequest, ApprovalResult, HumanInLoopProvider } from "@poe-code/agent-human-in-loop"; +import type { + ApprovalRequest, + ApprovalResult, + HumanInLoopProvider +} from "@poe-code/agent-human-in-loop"; export function slackApprovalProvider(opts: { channel: string; @@ -382,7 +387,7 @@ export function slackApprovalProvider(opts: { } return { outcome: "declined" }; - }, + } }; } ``` @@ -470,7 +475,7 @@ If you have an existing MCP server you want to keep running, use the MCP proxy: type HumanInLoopRuntimeOptions = { provider?: HumanInLoopProvider; taskList?: TaskList | { dir: string; format: "markdown-dir" | "yaml-file" }; - listName?: string; // defaults to "approvals" + listName?: string; // defaults to "approvals" binPath?: { execPath: string; entryArgs: readonly string[] }; }; ``` From a69370dc704ccea7f43768966cc37c48180911a9 Mon Sep 17 00:00:00 2001 From: "poe-code-agent[bot]" <254571472+poe-code-agent[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 00:31:00 +0000 Subject: [PATCH 6/7] docs: document recent package behavior --- packages/acp-telemetry/README.md | 25 +++++++++- packages/agent-harness-tools/README.md | 8 +++ packages/agent-harness/README.md | 33 ++++++++++-- packages/agent-spawn/README.md | 17 +++++++ packages/task-list/README.md | 69 ++++++++++++++++---------- packages/toolcraft/README.md | 12 +++-- 6 files changed, 130 insertions(+), 34 deletions(-) diff --git a/packages/acp-telemetry/README.md b/packages/acp-telemetry/README.md index aa80a0156..18349bd0d 100644 --- a/packages/acp-telemetry/README.md +++ b/packages/acp-telemetry/README.md @@ -1,10 +1,31 @@ # @poe-code/acp-telemetry -Pure ACP -> trace converters plus Braintrust/OTEL emitters. +Pure ACP event-to-trace conversion plus Braintrust and OpenTelemetry emitters. ## Public Exports -- `@poe-code/acp-telemetry` +- `acpToTrace(ctx)` converts an `@poe-code/agent-spawn` ACP spawn context into an `AcpTrace`. +- `emitToBraintrust(trace, parent)` writes the trace as nested Braintrust task/tool spans. +- `emitToOtel(trace, tracer)` writes the trace as OpenTelemetry-style spans and attributes. +- `redact(value)` removes sensitive prompt, tool, and metadata fields before emission. +- Types: `AcpTrace`, `AcpTraceSpan`, `BraintrustSpanLike`, `OtelSpanLike`, `OtelTracerLike`. + +## Trace shape + +`acpToTrace` creates one root `agent::` span with redacted prompt input, accumulated assistant output, token/cost/duration metrics when present, session/thread metadata, and one child span per ACP tool call. Tool child spans include redacted inputs, assembled tool outputs, tool call metadata, and start/end timestamps when the ACP event metadata includes them. + +```ts +import { acpToTrace, emitToOtel } from "@poe-code/acp-telemetry"; + +const trace = acpToTrace(spawnContext); +emitToOtel(trace, tracer); +``` + +## Emitters + +Braintrust emission expects a parent span-like object with `startSpan`, `log`, and `end`. The root is emitted as a `task`; children are emitted as `tool` spans. + +OpenTelemetry emission expects a tracer-like object with `startSpan`. Agent spans set `gen_ai.system`, request model, agent name, token usage, and Poe Code session/thread attributes. Tool spans set tool name and tool-call id attributes. Non-primitive inputs and outputs are serialized as JSON attributes. ## Configuration diff --git a/packages/agent-harness-tools/README.md b/packages/agent-harness-tools/README.md index 8a145998c..44eed8047 100644 --- a/packages/agent-harness-tools/README.md +++ b/packages/agent-harness-tools/README.md @@ -64,6 +64,14 @@ Notes: - Cancellation is only possible in the interactive prompt path. If `select(...)` returns a value that `isCancel(...)` recognizes, the function returns `{ cancelled: true }` and does not throw. Callers are responsible for showing the cancellation message and stopping the command cleanly. - `pipeline`, `experiment`, `ralph`, and `superintendent` all route their single-agent loop selection through this function so the precedence stays aligned across commands. +## Plan document helpers + +`openPlanList`, `discoverPlans`, and `archivePlan` expose numbered Markdown plan folders through `@poe-code/task-list`. They resolve the configured plan directory with the same cwd/home rules as workflow docs, open it as a `markdown-dir` single-list named `plans`, and use `frontmatterMode: "passthrough"` so plan-specific metadata survives task updates. + +- `discoverPlans({ cwd, homeDir, planDirectory, kinds? })` returns plan ids, names, kinds, absolute paths, and display paths. Numeric filename prefixes such as `04-api-shape-providers.md` are stripped from ids. +- `archivePlan({ cwd, homeDir, planDirectory, id })` fires the task-list `archive` event, moves the document under `archive/`, and repacks active plan prefixes. +- `openPlanList(...)` returns the underlying `TaskList` for commands that need direct task operations. + ## Configuration This package does not read config files directly, but Poe Code callers commonly pass `configuredDefaultAgent` from merged `core.defaultAgent`. diff --git a/packages/agent-harness/README.md b/packages/agent-harness/README.md index 9a223e024..b06036881 100644 --- a/packages/agent-harness/README.md +++ b/packages/agent-harness/README.md @@ -1,8 +1,35 @@ # @poe-code/agent-harness -Shared package for agent harness types and runtime orchestration APIs. +Shared harness loader, template, schema, and runtime orchestration APIs for `.md` + `.ajs` agent-script harness pairs. -This package is currently a scaffold. Loader, module, template, codegen, and CLI behavior will be added in later tasks. +## Public API + +- `runHarnessPair(mdPath, options)` resolves the matching `.ajs` file, validates frontmatter against any exported `schema`, lints the script, and runs it through `@poe-code/agent-script`. +- `listBuiltinTemplates()` returns bundled template pairs: `ralph-demo`, `coverage-demo`, `experiment-demo`, `pipeline-demo`, and `superintendent-demo`. +- `extractSchema(source, filename)` reads a harness script's exported schema for frontmatter validation. +- `resolvePair(mdPath)` resolves the Markdown/script pair for a harness document. +- `LintError` wraps lint diagnostics raised before execution. + +## Harness pairs + +A harness is a Markdown document plus a sibling `.ajs` script. The Markdown frontmatter configures the run; the body is passed to the harness import metadata. The `.ajs` file must export a default entry point and may export `schema` to validate frontmatter before execution. + +`runHarnessPair` locks the Markdown file while it runs, injects the `schema` module, wraps host modules for deterministic replay across resumes, writes snapshots, and cleans up completed snapshots after successful runs. + +## Snapshots and resume + +Pass `snapshotPath` to control where snapshots are read and written. `resume` defaults to `true`; set `resume: false` to remove a completed snapshot and force a fresh run. If a snapshot exists, the underlying agent-script source hash must still match the `.ajs` source. + +The CLI mirrors these options: + +```sh +poe-code harness run harness.md --snapshot-path .poe-code/harnesses/demo/snapshot.json --resume +poe-code harness new coverage-demo coverage.md +``` + +## Built-in templates + +`listBuiltinTemplates()` exposes template metadata with `kind`, `mdPath`, and `ajsPath`. `poe-code harness new ` copies both files into a new harness pair. ## Environment Variables @@ -10,4 +37,4 @@ This package does not read any environment variables. ## Configuration -This package does not read any configuration options. +This package does not read package-level configuration. Runtime behavior is supplied through `runHarnessPair` options: `modulesFor`, `allowedGlobals`, `resume`, `signal`, and `snapshotPath`. diff --git a/packages/agent-spawn/README.md b/packages/agent-spawn/README.md index efa06e318..5d0000b17 100644 --- a/packages/agent-spawn/README.md +++ b/packages/agent-spawn/README.md @@ -39,6 +39,22 @@ Pass `mcpServers` as a map of server names to `{ command, args?, env?, timeout? `spawnAutonomous(streamSpawn, options)` drives a streaming ACP spawn to completion, renders events through the design-system ACP writer, and retries activity timeouts. It is shared by SDK autonomous spawn flows and loop runners. +## ACP middlewares + +Pass `middlewares` to `spawnStreaming` or `spawnAcp` to wrap the ACP session lifecycle. A middleware receives a mutable `SpawnContext` with session id, agent id, prompt/model/mode/cwd, accumulated events, usage, optional event stream, and any log file selected by the middleware. Middlewares must call `next()` at most once. + +```ts +import { spawnStreaming, type AcpMiddleware } from "@poe-code/agent-spawn"; + +const telemetry: AcpMiddleware = async (ctx, next) => { + await next(); + console.log(ctx.agent, ctx.threadId, ctx.usage); +}; + +const run = spawnStreaming({ agentId: "codex", prompt: "Summarize", middlewares: [telemetry] }); +await run.done; +``` + ## Testing helper The `./testing` export provides a Vitest helper for code that depends on `spawn`: @@ -66,6 +82,7 @@ vi.mock("@poe-code/agent-spawn", spawnMock.factory); | `mode` | `"yolo" \| "edit" \| "read"` | Permission mode. Defaults are chosen by the caller. | | `args` | `string[]` | Extra args forwarded to the agent process. | | `mcpServers` | `Record` | MCP servers injected into the spawned agent. | +| `middlewares` | `AcpMiddleware[]` | Wrap `spawnStreaming`/`spawnAcp` execution for telemetry, logging, or post-processing. | | `useStdin` | `boolean` | Send the prompt through stdin when the agent supports it. | | `interactive` | `boolean` | Spawn the agent in interactive TUI mode. | | `activityTimeoutMs` | `number` | Kill/retry inactive streaming processes after this many milliseconds. | diff --git a/packages/task-list/README.md b/packages/task-list/README.md index bbe696b56..b56605452 100644 --- a/packages/task-list/README.md +++ b/packages/task-list/README.md @@ -6,11 +6,11 @@ Multi-list task manager with pluggable storage backends. `@poe-code/task-list` exposes one API over these backends: -| Backend | Storage | -| --- | --- | -| `markdown-dir` | One Markdown file per task, organized into subdirectories per list. | -| `yaml-file` | One YAML document with a top-level `lists:` mapping. | -| `gh-issues` | GitHub Issues in one repository, ordered and state-tracked through a GitHub Project v2 Status field. | +| Backend | Storage | +| -------------- | ---------------------------------------------------------------------------------------------------- | +| `markdown-dir` | One Markdown file per task, organized into subdirectories per list. | +| `yaml-file` | One YAML document with a top-level `lists:` mapping. | +| `gh-issues` | GitHub Issues in one repository, ordered and state-tracked through a GitHub Project v2 Status field. | The task lifecycle is `draft -> planned -> in-progress -> done -> archived`. `archived` is terminal. @@ -18,13 +18,13 @@ The task lifecycle is `draft -> planned -> in-progress -> done -> archived`. `ar - `openTaskList(options)`: opens a task store and returns a `TaskList` - `TaskList`: top-level interface for listing lists, querying all tasks, and resolving qualified IDs -- `Tasks`: per-list interface for create, update, `fire`, `canFire`, `events`, delete, and list operations +- `Tasks`: per-list interface for create, update, `fire`, `canFire`, `events`, delete, `move`, `reorder`, and list operations - `Task`: normalized task record with `list`, `id`, `qualifiedId`, `name`, `state`, `description`, and `metadata` - `TaskState`: `"draft" | "planned" | "in-progress" | "done" | "archived"` - `TaskDefaults`: default `metadata` applied when creating new tasks - `StateMachineDef` / `EventDef`: exported types for custom task lifecycle definitions passed via `openTaskList({ stateMachine })` - `defaultStateMachine`: exported default lifecycle with `plan`, `start`, `complete`, and `archive` events -- Error classes: `TaskNotFoundError`, `TaskAlreadyExistsError`, `InvalidTransitionError`, `MalformedTaskError` +- Error classes: `TaskNotFoundError`, `TaskAlreadyExistsError`, `InvalidTransitionError`, `MalformedTaskError`, `OrderMismatchError`, `AnchorNotFoundError` ## State Machines @@ -48,21 +48,23 @@ Pass a custom machine with `openTaskList({ stateMachine })`. If omitted, the pac ## Options -| Option | Type | Default | Behavior | -| --- | --- | --- | --- | -| `type` | `"markdown-dir" \| "yaml-file" \| "gh-issues"` | required | Selects the backend implementation. | -| `path` | `string` | required | Root directory for `markdown-dir` or YAML file path for `yaml-file`. | -| `defaults` | `TaskDefaults` | `{ metadata: {} }` | Seeds omitted metadata on new tasks only. New tasks always start at the configured state machine's initial state. | -| `create` | `boolean` | `false` | Creates missing storage for the selected backend when enabled. | -| `lockStaleMs` | `number` | `30_000` | Stale threshold passed to backend file locking. | -| `lockRetries` | `number` | `20` | Retry count passed to backend file locking. | -| `fs` | `TaskListFs` | `node:fs/promises` adapter | Injectable filesystem, primarily for tests. | -| `stateMachine` | `StateMachineDef` | `defaultStateMachine` | Overrides the task lifecycle used by `create`, `fire`, `canFire`, and `events`. | +| Option | Type | Default | Behavior | +| ----------------- | ---------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `type` | `"markdown-dir" \| "yaml-file" \| "gh-issues"` | required | Selects the backend implementation. | +| `path` | `string` | required | Root directory for `markdown-dir` or YAML file path for `yaml-file`. | +| `defaults` | `TaskDefaults` | `{ metadata: {} }` | Seeds omitted metadata on new tasks only. New tasks always start at the configured state machine's initial state. | +| `create` | `boolean` | `false` | Creates missing storage for the selected backend when enabled. | +| `singleList` | `string` | unset | `markdown-dir` only. Treats `path` as one list with this name instead of one subdirectory per list. | +| `frontmatterMode` | `"strict" \| "passthrough"` | `"strict"` | `markdown-dir` only. `passthrough` preserves non-task frontmatter and allows files without a frontmatter block. | +| `lockStaleMs` | `number` | `30_000` | Stale threshold passed to backend file locking. | +| `lockRetries` | `number` | `20` | Retry count passed to backend file locking. | +| `fs` | `TaskListFs` | `node:fs/promises` adapter | Injectable filesystem, primarily for tests. | +| `stateMachine` | `StateMachineDef` | `defaultStateMachine` | Overrides the task lifecycle used by `create`, `fire`, `canFire`, and `events`. | ## Env vars -| Env var | Behavior | -| --- | --- | +| Env var | Behavior | +| --------- | --------------------------------------------------------------- | | `GH_HOST` | Defers to gh CLI's host configuration; set GH_HOST to override. | ## Usage @@ -88,6 +90,21 @@ await planning.create({ await planning.fire("ship-readme", "plan"); ``` +By default, `markdown-dir` stores tasks as `//.md`. Set `singleList` to treat the root directory as one list, which is how plan folders such as `docs/plans` are exposed through the task API: + +```ts +const plans = await openTaskList({ + type: "markdown-dir", + path: "/repo/docs/plans", + singleList: "plans", + frontmatterMode: "passthrough" +}); + +await plans.list("plans").reorder(["api-shape-providers", "memory"]); +``` + +`frontmatterMode: "strict"` expects full task frontmatter (`kind`, `version`, `name`, `state`, and related task fields). `frontmatterMode: "passthrough"` keeps unrelated frontmatter keys as task metadata and writes back only the task-owned fields (`name`, `description`, `state`), so existing plan metadata is preserved. + ### `yaml-file` ```ts @@ -225,14 +242,14 @@ poe-code tasks sync --workflow ./WORKFLOW.md --repo octo-org/octo-repo -- `` and `--project` both use `/` project syntax. `--workflow` defaults to `./WORKFLOW.md`. `--repo` overrides the task repository from workflow frontmatter. `--states` overrides the required state list from workflow frontmatter. `--json` prints the report object as JSON. `--yes` confirms non-interactive sync; it is only used by `poe-code tasks sync`. -| Option | Commands | Behavior | -| --- | --- | --- | -| `--workflow ` | `verify`, `sync` | Workflow file path. Defaults to `./WORKFLOW.md`. | -| `--repo ` | `verify`, `sync` | GitHub repository owner/name. | +| Option | Commands | Behavior | +| -------------------------- | ---------------- | --------------------------------------------------- | +| `--workflow ` | `verify`, `sync` | Workflow file path. Defaults to `./WORKFLOW.md`. | +| `--repo ` | `verify`, `sync` | GitHub repository owner/name. | | `--project ` | `verify`, `sync` | GitHub Project v2 owner/number. Overrides ``. | -| `--states ` | `verify`, `sync` | Required task state names. | -| `--json` | `verify`, `sync` | Prints the report as JSON. | -| `--yes` | `sync` | Confirms non-interactive provisioning. | +| `--states ` | `verify`, `sync` | Required task state names. | +| `--json` | `verify`, `sync` | Prints the report as JSON. | +| `--yes` | `sync` | Confirms non-interactive provisioning. | The `Status` field name and option names are matched case-sensitively. The field must be named `Status`, and required option names must match exactly. For example, `status` is treated as a missing field, and `Done` is treated as missing when the required state is `done`. diff --git a/packages/toolcraft/README.md b/packages/toolcraft/README.md index 54425ed5b..bd180adaf 100644 --- a/packages/toolcraft/README.md +++ b/packages/toolcraft/README.md @@ -394,7 +394,11 @@ export function slackApprovalProvider(opts: { ## Errors -Throw `UserError` for expected, user-facing failures. The CLI prints the message without a stack trace and sets exit code 1; MCP and SDK surface the message as the error body. Any other thrown error is treated as unexpected and shows a stack with `--debug`. +Throw `UserError` for expected, user-facing failures. The CLI prints the message without a stack trace and sets exit code 1; MCP and SDK surface the message as the error body. Usage mistakes include a pointer to the relevant command help. Any other thrown error is treated as unexpected and shows a trimmed stack with `--debug`; use `--debug=raw` to include framework and runtime frames. + +HTTP-style errors with request/response context print the request, status, and a response-body snippet by default. `--verbose` or `--debug` prints headers and the full request/response bodies, with authorization headers redacted. + +Enable structured error reports with `errorReports: true`, `errorReports: { dir }`, or `TOOLCRAFT_ERROR_REPORTS=1`. Reports are written under `.toolcraft/errors` by default, include argv, parsed params, resolved secret presence, structured error fields, stack/cause chains, and HTTP transcripts, and redact declared secrets plus parameter names that look sensitive. ## Migrating from a folder of scripts @@ -412,6 +416,7 @@ If you have an existing MCP server you want to keep running, use the MCP proxy: ## Environment variables - `TOOLCRAFT_MCP_REFRESH` — MCP proxy cache refresh (`unset` = use cache, `1`/`true` = refresh all, comma-separated names = refresh those). +- `TOOLCRAFT_ERROR_REPORTS=1` — enables structured error report files for CLI, MCP, and SDK surfaces that wire `errorReports`. - Per-command `secrets` declarations name additional env vars. They are read at command run time and passed to the handler. ## API reference @@ -451,19 +456,20 @@ If you have an existing MCP server you want to keep running, use the MCP proxy: - `presets?: boolean` — enables `--preset ` for loading parameter defaults from JSON files. - `apiVersion?: string` — for `requires.apiVersion`. - `humanInLoop?: HumanInLoopRuntimeOptions` +- `errorReports?: boolean | { dir?: string }` - `projectRoot?: string` — root used for MCP proxy cache files (`.toolcraft/mcp/*.json`). ### `createSDK(root, options)` - `casing?: "camel"` — generated SDK member style. -- `services?` / `humanInLoop?` / `apiVersion?` +- `services?` / `humanInLoop?` / `apiVersion?` / `errorReports?` - `projectRoot?: string` — root used for MCP proxy cache files (`.toolcraft/mcp/*.json`). ### `createMCPServer(root, options)` / `runMCP(root, options)` - `name: string` - `version: string` -- `services?` / `humanInLoop?` / `apiVersion?` +- `services?` / `humanInLoop?` / `apiVersion?` / `errorReports?` - `projectRoot?: string` — root used for MCP proxy cache files (`.toolcraft/mcp/*.json`). - `tools?: string[]` — allowlist of MCP tool names or group prefixes. Tool names are `__`-joined snake_case path segments (`root__bot__create`); a prefix like `root__bot` includes every descendant tool. - `omitRootToolNamePrefix?: boolean` — defaults to `false`. Set to `true` to omit the root group name from single-root MCP tool names (`bot__create`). From 815a86976e0c93fbe874c8608a77dd51e2344c07 Mon Sep 17 00:00:00 2001 From: "poe-code-agent[bot]" <254571472+poe-code-agent[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 00:36:17 +0000 Subject: [PATCH 7/7] docs: document recent provider configuration changes --- docs/development/e2e-assertions.md | 19 ++++++++++++++----- packages/providers/README.md | 23 +++++++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/development/e2e-assertions.md b/docs/development/e2e-assertions.md index dc9335ded..09d94fef7 100644 --- a/docs/development/e2e-assertions.md +++ b/docs/development/e2e-assertions.md @@ -158,25 +158,34 @@ This is important: we don't need to match the entire stdout, just one line. | Config dir exists | `container.exec('test -d /root/.codex')` | Exit code `0` | | Config file exists | `container.fileExists('/root/.codex/config.toml')` | `true` | | Config: model_provider | parsed TOML | `"poe"` | -| Config: model | parsed TOML | Default: `gpt-5.2-codex` | +| Config: model | parsed TOML | Default: `gpt-5.5` | | Config: model_reasoning_effort | parsed TOML | Default: `medium` | +| Config: model_verbosity | parsed TOML | Default: `medium` | +| Config: profiles.gpt-5.5.model | parsed TOML | `gpt-5.5` | | Config: model_providers.poe.base_url | parsed TOML | Non-empty URL | | Config: model_providers.poe.experimental_bearer_token | parsed TOML | Non-empty string (API key) | **Expected config structure (TOML):** ```toml model_provider = "poe" -model = "gpt-5.2-codex" +model = "gpt-5.5" model_reasoning_effort = "medium" +model_verbosity = "medium" + +[profiles."gpt-5.5"] +model = "gpt-5.5" +model_provider = "poe" +model_reasoning_effort = "medium" +model_verbosity = "medium" [model_providers.poe] name = "poe" -base_url = "https://api.poe.com" +base_url = "https://api.poe.com/v1" wire_api = "responses" experimental_bearer_token = "" ``` -**Default model:** `openai/gpt-5.2-codex` (stripped to `gpt-5.2-codex`) +**Default model:** `openai/gpt-5.5` (stripped to `gpt-5.5`) #### opencode @@ -246,7 +255,7 @@ max_context_size = 256000 [providers.poe] type = "openai_legacy" -base_url = "https://api.poe.com" +base_url = "https://api.poe.com/v1" api_key = "" ``` diff --git a/packages/providers/README.md b/packages/providers/README.md index 89c1df15b..e1d36f7eb 100644 --- a/packages/providers/README.md +++ b/packages/providers/README.md @@ -44,8 +44,8 @@ whose model names must be typed by the user rather than selected from an agent-o import { ProviderRegistry } from "@poe-code/providers"; const registry = new ProviderRegistry([anthropic, poe]); -registry.list(); // all providers, construction order -registry.get("anthropic"); // AuthProvider | undefined +registry.list(); // all providers, construction order +registry.get("anthropic"); // AuthProvider | undefined registry.forAgent({ id: "claude-code", apiShapes: ["anthropic-messages"] }); ``` @@ -81,6 +81,25 @@ while still storing the resulting API key through the same provider secret store `ProviderRegistry.isLoggedIn()` also treats a non-empty declared env var as logged in, matching what `login()` would use in CI. +## CLI integration + +`poe-code provider login ` stores the provider credential and any provider endpoint +metadata needed later by `poe-code configure`. `--base-url ` must be an `http` or +`https` URL; for providers with `apiShapes[].baseUrlPath`, Poe Code derives and stores the +shape-specific endpoint URLs from that root. `--shape-base-url =` can set or +override individual API-shape endpoints directly. + +Providers with `requiresBaseUrl: true`, such as Cloudflare AI Gateway, require a gateway +root URL before they can configure an agent. Interactive login prompts for the URL when no +`--base-url`, `--shape-base-url`, or `baseUrlEnvVar` value is available. Non-interactive +`--yes` login must receive the URL through one of those inputs. + +During `poe-code configure --provider `, the active API-shape base URL resolves +from explicit shape URLs, then `--base-url`, then the provider `baseUrlEnvVar`, then stored +login metadata, then the shape's default URL. Providers with `modelInput: { kind: "freeform" }` +use the configured model, an explicit `--model`, or an interactive text prompt; `--yes` +requires one of those model sources. + ## Environment variables This package does not read `process.env` directly. Consumers pass environment variables in