diff --git a/docs/README.md b/docs/README.md index 89d4458e4..ff1924438 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,10 +30,12 @@ Architecture documentation scoped to a single package lives in that package's ## trackers/ -Provider-specific host-side agent-tool contracts: +Provider-specific compact adapter profiles and host-side agent-tool contracts: -- [GitHub](trackers/github.md) — `github_graphql` -- [Linear](trackers/linear.md) — `linear_graphql` +- [GitHub Project](trackers/github-project.md) — configuration, normalization, and `github_graphql` +- [GitHub tool](trackers/github.md) — standalone `github_graphql` tool contract +- [Linear](trackers/linear.md) — configuration, normalization, and `linear_graphql` +- [File](trackers/file.md) — local/Docker E2E fixture adapter profile ## designs/ diff --git a/docs/architecture.md b/docs/architecture.md index 1dcdde2e0..18f92a0d4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -85,13 +85,14 @@ touches a layer, check that its slice (and the linked documents) still holds. ### 5. Integration — tracker adapters (tracker-specific code lives only here) -- GitHub Project V2: `packages/tracker-github` (including the adapter-owned linked-PR canonical-subject extension; opaque `nativeRef` data never crosses into orchestration). Source issue state and linked-PR metadata remain distinct from Project workflow status; it returns all active scoped items and derives GitHub assignment, repository-scope, pickup-label, and fork-PR eligibility as `dispatchable` with an explainable reason. +- GitHub Project V2: `packages/tracker-github` (including the adapter-owned linked-PR canonical-subject extension; opaque `nativeRef` data never crosses into orchestration). Source issue state and linked-PR metadata remain distinct from Project workflow status; candidate polling excludes terminal states and can include other non-terminal items. It derives GitHub assignment, repository-scope, pickup-label, and fork-PR eligibility as `dispatchable` with an explainable reason. - Linear: `packages/tracker-linear`; it derives provider-native assignment eligibility as the normalized `dispatchable` contract. Its pickup labels instead filter the candidate list before dispatch, so label-ineligible Linear items are not retained for explain surfaces as `dispatchable: false` records. This adapter-side label filtering is a repository-level divergence from the upstream scheduler-owned label boundary and differs from the GitHub adapter's retained, reason-bearing records. - File-based (E2E only): `packages/tracker-file`; fixtures may set `dispatchable` and `dispatchReason` directly to exercise the adapter-neutral scheduler gate. - GitHub-specific planning/approval/PR-reporting extensions: `packages/extension-github-workflow` -- GitHub adapter profile: `packages/tracker-github/README.md`. Its synthetic - `Archived` state is a GitHub-specific implementation choice, not normalized - Symphony core behavior. +- Compact adapter profiles: [GitHub Project](trackers/github-project.md), + [Linear](trackers/linear.md), and [file](trackers/file.md). GitHub's + synthetic `Archived` state is a GitHub-specific implementation choice, not + normalized Symphony core behavior. ### 6. Observability — events and status surfaces diff --git a/docs/trackers/file.md b/docs/trackers/file.md new file mode 100644 index 000000000..b3140b117 --- /dev/null +++ b/docs/trackers/file.md @@ -0,0 +1,49 @@ +# File adapter profile + +This compact §11.2 profile documents the `file` adapter used by local and +Docker E2E environments. It is intentionally a fixture adapter, not a +production tracker provider. + +## Configuration and scope + +| Item | Contract | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tracker.kind` | `file` | +| Provider scope | A local JSON issue array selected by `tracker.settings.issuesPath`; it has no project, board, team, or remote repository selection. | +| Provider keys | `tracker.provider.path` is accepted and validated for forward compatibility but is currently inert. The effective required path is `tracker.settings.issuesPath`. Unknown provider keys are preserved by core parsing. | +| Defaults | Lifecycle defaults are `Status`, active `Ready`/`In Progress`, terminal `Done`/`Cancelled`, and no planning states. Unless explicitly configured, blocker checks use the first active state (`Ready` with these defaults). | +| Credentials | `secretEnvironmentNames()` is empty: file fixtures require no secret or environment variable. | +| Validation | A supplied `path` must be a non-empty string and fails configuration parsing with `WorkflowValidationError` when invalid. A missing runtime `issuesPath` instead fails each adapter operation with a plain `Error`. | + +There is no provider request pagination, page size, timeout, or rate limit: the +adapter reads one local JSON file per operation. A missing file yields an empty +candidate list. An unreadable file, malformed JSON, or a top-level non-array is +a file/response failure rather than a silent partial read. + +## Normalized issue contract + +| Field or condition | File mapping | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `id` and `native_ref` | Fixture `id` is retained unchanged. `native_ref` is retained when supplied; otherwise it defaults to `{ itemId: tracker.itemId }` only for a string item ID, or `{ itemId: null }`. | +| State, labels, priority, timestamps | Valid fixture values are preserved; fixture authors supply their normalized form. The adapter does not invent provider-specific state, label, priority, or timestamp mappings. | +| `dispatchable` | An omitted value defaults to `true`; `false` and `dispatchReason` can be supplied to exercise the adapter-neutral scheduler gate. Pickup-label filtering remains available through the shared normalized filter. | +| Malformed and optional fields | Entries lacking the checked shape (`id`, `identifier`, `state`, object `repository`, object `tracker`, or a boolean `dispatchable` when supplied) are skipped with a diagnostic. This check is not exhaustive: for example, a missing `title` is currently not rejected. Optional `assigneeId` defaults to `null`. | + +## Native tools and errors + +The file adapter exposes no provider-native agent tool. + +This table records the Symphony §11.4 target mapping required by §11.2. The +file adapter does not currently emit any structured error `category`; its +configuration parse failure is `WorkflowValidationError` and its runtime file +failures are plain errors. + +| Provider-native failure | §11.4 target category | Current adapter surface | +| ---------------------------------------------- | -------------------------------------- | ------------------------------------ | +| File read failure or invalid JSON payload | `tracker_request` / `tracker_response` | Plain read/JSON error | +| No corresponding provider HTTP status | `tracker_status` | Not applicable | +| No pagination | `tracker_pagination` | Not applicable | +| No remote quota | `tracker_rate_limited` | Not applicable | +| Invalid `tracker.provider.path` | `invalid_tracker_config` | Parse-time `WorkflowValidationError` | +| Missing required `tracker.settings.issuesPath` | `invalid_tracker_config` | Per-operation plain `Error` | +| No credentials required | `missing_tracker_secret` | Not applicable | diff --git a/docs/trackers/github-project.md b/docs/trackers/github-project.md new file mode 100644 index 000000000..8345b459a --- /dev/null +++ b/docs/trackers/github-project.md @@ -0,0 +1,67 @@ +# GitHub Project adapter profile + +This is the compact profile required by Symphony specification §11.2 for the +`github-project` tracker adapter. It describes the repository's current +Integration-layer behavior; it does not add provider semantics to core. + +## Configuration and scope + +| Item | Contract | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tracker.kind` | `github-project` | +| Provider scope | `tracker.provider.project_id` selects one GitHub Project V2. `repository` is derived from each issue; the optional runtime repository filter and `--assigned-only` are adapter dispatchability rules. | +| Provider keys | `project_id`, `endpoint`, `state_field`, `priority_field`, `priority`, `pickup_labels`, `active_states`, `terminal_states`, `blocker_check_states`, and `planning_states`. Unknown provider keys are preserved by core configuration parsing. Flat `tracker.*` keys are deprecated compatibility aliases. | +| Defaults | Lifecycle defaults are `Status`, active `Todo`/`In Progress`, terminal `Done`, and no planning states. Unless explicitly configured, blocker checks use the first active state (`Todo` with these defaults). Priority is `null` unless the configured `priority` policy or deprecated `priority_field` resolves a value. | +| Credentials | `GITHUB_GRAPHQL_TOKEN` is the polling credential. `secretEnvironmentNames()` declares `GH_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GITHUB_TOKEN`, and `GITHUB_GRAPHQL_TOKEN`; these names are removed from agent-child inheritance. | +| Validation | Declared string keys must be non-empty; `endpoint` must be an HTTP(S) URL; state lists must contain non-empty strings; `priority` and `pickup_labels` must be objects. Missing `project_id` or `GITHUB_GRAPHQL_TOKEN` prevents adapter use. See the error table for the §11.4 target mapping and current unnormalized surfaces. | + +Candidate polling excludes configured terminal states with the GitHub `query` +argument; it can therefore return non-terminal items outside `active_states`. +State and ID refreshes are unfiltered so terminal items remain reconcilable. The +adapter uses cursor pagination with a default page size of 25; a next-page +response without a cursor is an integrity failure. Each GraphQL request has a +30-second default timeout (or the configured positive `timeoutMs`). It records +GraphQL rate-limit metadata and applies the shared GitHub policy: it honors the +later of `Retry-After` and a known primary reset. If that required wait exceeds +60 seconds, the request is not retried and the rate-limit error is surfaced. + +The [2026-07-19 GitHub API rate-limit audit](../reports/2026-07-19-github-api-rate-limit-audit.md) +records why the profile keeps the page size at 25 and measures query cost by +returned `rateLimit` data rather than guessing from page count. + +## Normalized issue contract + +| Field or condition | GitHub Project mapping | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | GitHub Issue or Pull Request **node ID** (not the Project item ID). | +| `native_ref` | Opaque adapter data containing Project `itemId`, content type, source state, and linked-PR metadata. The Project item ID is used for Project mutations and never replaces `id`. | +| State | The configured Project status field is used. An archived Project item becomes synthetic `Archived`; this is a GitHub-specific extension, not a core state. | +| Labels and priority | Labels are trimmed, lowercased, deduplicated, and sorted. Priority is explicit Project-field or label policy output; unmapped/disabled values are `null`. | +| Timestamps | Issue and Project-item timestamps are parsed to canonical RFC 3339/ISO 8601; invalid or absent values become `null`. The newer item timestamp wins. | +| `dispatchable` | Starts `true`, then the adapter applies assignment, repository, fork-PR, pickup-label, and configured blocker rules. Ineligible items remain listed with `dispatchable: false` and an explainable `dispatchReason`. | +| Malformed and optional fields | Candidate polling silently drops non-Issue/PR Project content. Items missing the configured state are skipped with a structured event and recorded in `skippedItems`; requested ID refreshes fail instead. Optional body, labels, assignee, timestamps, priority, and linked metadata normalize to `null`, empty collections, or documented defaults rather than leaking provider shape. | + +## Native tool + +The host-side [`github_graphql`](github.md) tool accepts one GraphQL query or +mutation plus optional variables/operation name. It is always advertised for +repository and PR operations, but its credential and opaque `native_ref` stay +in the host process. See the linked tool document for scope and safe-use rules. + +## Error-category mapping + +This table records the Symphony §11.4 target mapping required by §11.2. Today, +only `tracker_pagination` is emitted as a structured adapter `category`. +The other rows describe the intended normalized category; current failures are +surfaced as `GitHubTrackerHttpError`, `GitHubTrackerQueryError`, or +`WorkflowValidationError` and are not yet normalized. + +| Provider-native failure | §11.4 target category | Current adapter surface | +| ------------------------------------------------------ | ------------------------ | --------------------------------------------------------------------------- | +| Network/transport failure or timeout | `tracker_request` | `GitHubTrackerHttpError` | +| Non-rate-limited HTTP status | `tracker_status` | `GitHubTrackerHttpError` | +| Invalid JSON, GraphQL errors, or missing expected data | `tracker_response` | `GitHubTrackerQueryError` | +| Missing cursor or incomplete cursor traversal | `tracker_pagination` | Structured `tracker_pagination` | +| GitHub quota exhaustion or `Retry-After` response | `tracker_rate_limited` | `GitHubGraphQLRateLimitError` (core `TrackerRateLimitError`, no `category`) | +| Invalid Project/provider setting or configured shape | `invalid_tracker_config` | `WorkflowValidationError` | +| No resolved GitHub credential | `missing_tracker_secret` | Adapter initialization/configure error | diff --git a/docs/trackers/linear.md b/docs/trackers/linear.md index cd0a23ac5..f8197f851 100644 --- a/docs/trackers/linear.md +++ b/docs/trackers/linear.md @@ -1,4 +1,54 @@ -# Linear tracker tools +# Linear adapter profile and tools + +This compact profile required by Symphony specification §11.2 describes the +current `linear` adapter before documenting its native tool. + +## Configuration and scope + +| Item | Contract | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tracker.kind` | `linear` | +| Provider scope | `tracker.provider.project_slug` selects the Linear project; the adapter polls its issues by workflow state. Team IDs and board IDs are not configuration substitutes. | +| Provider keys | Required `project_slug`; optional `endpoint`, environment-reference `api_key`, and `pickup_labels` (`include`/`exclude`). `project_id`, `projectId`, `teamId`, and `team_id` are rejected. Unknown provider keys are preserved by core parsing. | +| Defaults | Endpoint defaults to `https://api.linear.app/graphql`; lifecycle defaults are `Status`, active `Todo`/`In Progress`, terminal `Done`, and no planning states. Unless explicitly configured, blocker checks use the first active state (`Todo` with these defaults). | +| Credentials | `api_key`, if given, must be `$NAME`, `env:NAME`, or `${NAME}`; otherwise polling uses `LINEAR_API_KEY`. `secretEnvironmentNames()` declares `LINEAR_API_KEY` and `LINEAR_AUTHORIZATION`, which stay out of agent-child inheritance. | +| Validation | `project_slug` is required; optional strings must be non-empty; label rules must be string lists. See the error table for the §11.4 target mapping and current unnormalized surfaces. | + +The adapter uses cursor pagination with a default page size of 50, a default +maximum of 100 pages (capped at 1,000), and a 10-second per-page timeout +(capped at 60 seconds). Missing cursors and page-limit exhaustion are +pagination-integrity failures. Linear rate-limit headers are preserved as +tracker metadata, including `Retry-After`; callers must avoid bursts and honor +provider throttling because the adapter does not impose a separate scheduler. + +## Normalized issue contract + +| Field or condition | Linear mapping | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` and `native_ref` | `id` is the Linear issue ID. `native_ref` contains that `itemId` and the configured `projectSlug`. | +| State, labels, priority, timestamps | State is the Linear workflow-state name. Labels are trimmed, lowercased, and deduplicated. Priority `0` (No priority) becomes `null`; other Linear values are retained. Timestamps parse to canonical RFC 3339/ISO 8601 or `null`. | +| `dispatchable` | Candidate polling includes unassigned issues. With `--assigned-only`, only the authenticated viewer's assigned issues are dispatchable; configured non-terminal blockers also make an item non-dispatchable with a reason. `pickup_labels` instead filters label-ineligible candidates from the list (a documented repository divergence from GitHub's retained records). | +| Malformed and optional fields | Malformed required data (`id`, `identifier`, or state name) aborts the whole listing in both candidate polling and ID refresh; there is no per-record skip. Optional labels, assignee, timestamps, priority, and relations normalize to empty collections or `null`. | + +## Error-category mapping + +This table records the Symphony §11.4 target mapping required by §11.2. Today, +only `tracker_pagination` is emitted as a structured adapter `category`. +The other rows describe the intended normalized category; current configuration +failures are `WorkflowValidationError` and request/response failures are native +errors rather than a normalized category. + +| Provider-native failure | §11.4 target category | Current adapter surface | +| ----------------------------------------------- | ------------------------ | -------------------------------------- | +| Network failure or page timeout | `tracker_request` | Native request/timeout error | +| Non-rate-limited HTTP status | `tracker_status` | Native HTTP error | +| Invalid JSON, GraphQL errors, or absent data | `tracker_response` | Native GraphQL/response error | +| Missing cursor or maximum-page truncation | `tracker_pagination` | Structured `tracker_pagination` | +| Linear throttle / `Retry-After` response | `tracker_rate_limited` | Native HTTP/GraphQL error | +| Invalid provider keys or unsupported scope keys | `invalid_tracker_config` | `WorkflowValidationError` | +| No resolved Linear credential | `missing_tracker_secret` | Adapter initialization/configure error | + +## Native tool The Linear tracker adapter exposes `linear_graphql` to an agent runtime as a host-side dynamic tool. The coding-agent child receives the schema and result @@ -7,15 +57,15 @@ process. ## `linear_graphql` -| Property | Contract | -| --- | --- | -| Name | `linear_graphql` | -| Input | An object with required `query: string`; optional `variables: object` and `operationName: string`; no additional top-level properties. | -| Mutations | Permitted. A mutation must be intentionally scoped to the active Linear issue or its team. | -| Scope | The worker supplies the normalized active issue `{ id, identifier, nativeRef }` to the adapter. `nativeRef` remains host-internal and is never added to the GraphQL payload. The tool validates that a request has exactly one GraphQL operation, but it is an arbitrary Linear GraphQL transport: callers must constrain the operation and variables to the active issue/team; it does not infer or rewrite a target. | -| Result | The provider GraphQL response payload. | -| Errors | Empty or multi-operation documents, invalid GraphQL syntax, missing host authentication, HTTP failures, and GraphQL errors are returned to the runtime as structured tool failures. Unknown tool names are rejected. | -| Rate limits | Linear's GraphQL response is returned unchanged. No Symphony-side rate-limit scheduler is currently applied, so callers must avoid bursts and honor provider throttling responses. | +| Property | Contract | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Name | `linear_graphql` | +| Input | An object with required `query: string`; optional `variables: object` and `operationName: string`; no additional top-level properties. | +| Mutations | Permitted. A mutation must be intentionally scoped to the active Linear issue or its team. | +| Scope | The worker supplies the normalized active issue `{ id, identifier, nativeRef }` to the adapter. `nativeRef` remains host-internal and is never added to the GraphQL payload. The tool validates that a request has exactly one GraphQL operation, but it is an arbitrary Linear GraphQL transport: callers must constrain the operation and variables to the active issue/team; it does not infer or rewrite a target. | +| Result | The provider GraphQL response payload. | +| Errors | Empty or multi-operation documents, invalid GraphQL syntax, missing host authentication, HTTP failures, and GraphQL errors are returned to the runtime as structured tool failures. Unknown tool names are rejected. | +| Rate limits | Linear's GraphQL response is returned unchanged. No Symphony-side rate-limit scheduler is currently applied, so callers must avoid bursts and honor provider throttling responses. | The tool is advertised only when the selected tracker adapter is Linear. It is not an MCP subprocess and must not be recreated by the coding-agent child. diff --git a/packages/tracker-github/README.md b/packages/tracker-github/README.md index d73da65b0..7504eb803 100644 --- a/packages/tracker-github/README.md +++ b/packages/tracker-github/README.md @@ -1,25 +1,7 @@ # @gh-symphony/tracker-github -GitHub Project polling, issue normalization, and tracker-facing configuration validation that stay behind the core tracker adapter contract. +GitHub Project polling, issue normalization, and tracker-facing configuration +validation behind the core tracker adapter contract. -Adapter profile note: labels are trimmed, lowercased, deduplicated, and sorted; timestamps are parsed to canonical ISO 8601 or `null`. A pagination response with `hasNextPage: true` and no cursor fails with the `tracker_pagination` category and a structured integrity event. - -## Adapter profile - -- Candidate polling returns every active, in-scope Project item, including items - that cannot be dispatched. GitHub-specific eligibility is expressed through - `dispatchable: false` and `dispatchReason`, so `repo explain` can report it. -- `assigneeId` is the login of the first GitHub issue assignee, or `null` when - an issue has no assignee. It is a provider-native identity and is not a - cross-tracker identifier. -- With `--assigned-only`, items assigned to another user stay visible but are - non-dispatchable. Repository scope and fork PR heads are handled the same - way. -- Blocker eligibility is derived here, not by orchestration core. For states - selected by the workflow's `blocker_check_states`, unresolved GitHub - `blockedBy` issues produce `dispatchable: false` and a `dispatchReason`. - `blockedBy` remains best-effort provider metadata; closed blockers do not - prevent dispatch. -- The adapter additionally owns GitHub-only lifecycle extensions: a synthetic - `Archived` state. This is a repository-specific integration choice, not a - normalized Symphony core state. +The canonical compact adapter profile is +[docs/trackers/github-project.md](../../docs/trackers/github-project.md). diff --git a/packages/tracker-linear/README.md b/packages/tracker-linear/README.md index b990a3a75..4e1f3c54c 100644 --- a/packages/tracker-linear/README.md +++ b/packages/tracker-linear/README.md @@ -1,28 +1,7 @@ # @gh-symphony/tracker-linear -Linear tracker adapter for GitHub Symphony. +Linear polling and normalized tracker integration behind the core tracker +adapter contract. -Adapter profile note: labels are trimmed, lowercased, and deduplicated; timestamps are parsed to canonical ISO 8601 or `null`; Linear priority `0` (No priority) maps to `null`. Cursor integrity loss and max-page truncation fail with the `tracker_pagination` category and a structured integrity event. - -The MVP is read-side only: it polls Linear issues by `project.slugId` and -workflow state names, normalizes them into `TrackedIssue`, and injects Linear -context into worker environments. - -Blocker eligibility is derived by this adapter. When an issue is in a -workflow-selected `blocker_check_states` state, unresolved `blockedBy` Linear -issues (those outside the workflow terminal states) produce -`dispatchable: false` with a provider-specific `dispatchReason`. The -`blockedBy` field remains best-effort metadata rather than an orchestration -core gate. - -Candidate polling intentionally includes unassigned issues. When -`--assigned-only` is enabled, the adapter derives `dispatchable` from the -returned `assignee.id` compared with the authenticated Linear viewer instead -of adding an `assignee.isMe` GraphQL filter. This keeps unassigned and -other-user issues observable while preventing their dispatch. The same query -and pagination request count applies; only the returned candidate set can grow -to include unassigned issues. Pickup labels are applied by this adapter as a -candidate-listing filter rather than retained `dispatchable: false` records, so -label-ineligible Linear issues are not available to explain surfaces. This -repository-level adapter behavior diverges from the upstream scheduler-owned -label boundary and from GitHub's retained, reason-bearing pickup-label records. +The canonical compact adapter profile and `linear_graphql` tool contract are +in [docs/trackers/linear.md](../../docs/trackers/linear.md).