diff --git a/.claude/skills/project-manager-init/SKILL.md b/.claude/skills/project-manager-init/SKILL.md index 95f55edfe..50308b944 100644 --- a/.claude/skills/project-manager-init/SKILL.md +++ b/.claude/skills/project-manager-init/SKILL.md @@ -17,12 +17,14 @@ Create a `PROJECT_MANAGE.md` file in the repository root that stores project man Before doing anything else, run these checks **in order**: 1. **gh CLI authentication**: Run `gh auth status`. If not authenticated, stop and tell the user: + ``` GitHub CLI is not authenticated. Please run: $ gh auth login ``` 2. **Project scope permission**: Attempt `gh project list --owner --limit 1`. If it fails with a 403 or permission error, stop and tell the user: + ``` The GitHub CLI token lacks the "project" scope. Please run: $ gh auth refresh -s project @@ -88,6 +90,7 @@ Have a brief interactive discussion to establish: Write the file with YAML frontmatter containing all machine-readable IDs, followed by human-readable convention rules in markdown. **Frontmatter** must include: + ```yaml --- project_url: @@ -114,6 +117,7 @@ status_options: ``` **Body** must include the agreed-upon conventions in clear markdown sections: + - Issue Template - Splitting Rules - Priority Definitions diff --git a/.claude/skills/project-manager-new/SKILL.md b/.claude/skills/project-manager-new/SKILL.md index c50833d97..951fbf2fe 100644 --- a/.claude/skills/project-manager-new/SKILL.md +++ b/.claude/skills/project-manager-new/SKILL.md @@ -17,12 +17,14 @@ Create one or more GitHub issues that follow the repository's project management Run these checks **in order** before proceeding: 1. **gh CLI authentication**: Run `gh auth status`. If not authenticated, stop and instruct: + ``` GitHub CLI is not authenticated. Please run: $ gh auth login ``` 2. **Project scope permission**: Attempt a lightweight project API call using the project info from `PROJECT_MANAGE.md` frontmatter. If permission error, stop and instruct: + ``` The GitHub CLI token lacks the "project" scope. Please run: $ gh auth refresh -s project @@ -38,12 +40,14 @@ Run these checks **in order** before proceeding: ### Step 1: Parse Configuration Read `PROJECT_MANAGE.md` and extract: + - **Frontmatter**: project_node_id, owner, field_ids, priority_options, size_options, status_options, backlog_status, backlog_status_id - **Body**: Issue template format, splitting rules, priority/size definitions ### Step 2: Understand the Request The user provides a natural-language description of the work as `$ARGUMENTS`. Analyze it to understand: + - What needs to change - Which packages/files are affected - The motivation (bug fix, feature, refactoring, spec compliance, etc.) @@ -51,6 +55,7 @@ The user provides a natural-language description of the work as `$ARGUMENTS`. An ### Step 3: Explore the Codebase Based on the description, explore the codebase to: + - Identify affected files and their current state - Understand package boundaries and dependency relationships - Assess the scope of changes needed @@ -59,11 +64,13 @@ Based on the description, explore the codebase to: ### Step 4: Draft the Issue(s) Using the issue template from PROJECT_MANAGE.md, draft the issue body with: + - All required template sections filled in - Concrete file paths and change descriptions (not vague) - Verification steps that can actually be run Assign **Priority**, **Size**, and **Estimate** based on: + - The definitions in PROJECT_MANAGE.md - The actual codebase analysis from Step 3 @@ -87,6 +94,7 @@ Proceed? [create all / modify / cancel] ``` Splitting guidelines: + - Each sub-issue should be independently deliverable (single PR) - Respect package boundaries when possible - Maintain clear blocking relationships between split issues @@ -97,6 +105,7 @@ Splitting guidelines: Fetch **all open (non-Done) issues** from the project board — not just Backlog items. An issue currently In Progress (e.g., a large refactoring) can block the new issue, or the new issue might block existing in-flight work. Analyze whether the new issue(s): + - **Are blocked by** any existing open issue (e.g., depends on a type change, refactoring, or spec migration that's already planned or in progress) - **Block** any existing open issue (e.g., the new issue introduces something an existing issue depends on) @@ -130,12 +139,20 @@ Upon user approval: 4. **Connect blocking relationships**: For split issues and existing backlog relationships, use GraphQL `addBlockedBy` mutation: ```graphql mutation { - addBlockedBy(input: { - issueId: "", - blockingIssueId: "" - }) { - issue { number title } - blockingIssue { number title } + addBlockedBy( + input: { + issueId: "" + blockingIssueId: "" + } + ) { + issue { + number + title + } + blockingIssue { + number + title + } } } ``` diff --git a/.gh-symphony/context.yaml b/.gh-symphony/context.yaml index da7ae0fbd..8bab7b971 100644 --- a/.gh-symphony/context.yaml +++ b/.gh-symphony/context.yaml @@ -55,8 +55,7 @@ text_fields: name: Linked pull requests data_type: LINKED_PULL_REQUESTS -repositories: - [] +repositories: [] detected_environment: packageManager: pnpm diff --git a/.gh-symphony/reference-workflow.md b/.gh-symphony/reference-workflow.md index 86df7d380..08266d960 100644 --- a/.gh-symphony/reference-workflow.md +++ b/.gh-symphony/reference-workflow.md @@ -1,62 +1,67 @@ # Reference WORKFLOW.md — gh-symphony + # This file is a reference template for authoring WORKFLOW.md. + # AI agents reference this file (via the /gh-symphony skill) when designing WORKFLOW.md. + # Do not edit this file directly. --- # ═══ FRONT MATTER FIELD REFERENCE ═══ + # All front matter fields supported by the gh-symphony parser are listed below. tracker: - kind: github-project - provider: - project_id: PVT_REPLACE_WITH_YOUR_PROJECT_ID - state_field: Status - blocker_check_states: [{first active state}] +kind: github-project +provider: +project_id: PVT_REPLACE_WITH_YOUR_PROJECT_ID +state_field: Status +blocker_check_states: [{first active state}] - active_states: [{active column names}] - terminal_states: [{terminal column names}] +active_states: [{active column names}] +terminal_states: [{terminal column names}] polling: - interval_ms: 30000 +interval_ms: 30000 workspace: - root: .runtime/symphony-workspaces +root: .runtime/symphony-workspaces hooks: - after_create: hooks/after_create.sh # npm/yarn/pnpm install script - before_run: null - after_run: null - before_remove: null - timeout_ms: 60000 +after_create: hooks/after_create.sh # npm/yarn/pnpm install script +before_run: null +after_run: null +before_remove: null +timeout_ms: 60000 agent: - max_concurrent_agents: 10 - max_retry_backoff_ms: 30000 - retry_base_delay_ms: 1000 - max_turns: 20 +max_concurrent_agents: 10 +max_retry_backoff_ms: 30000 +retry_base_delay_ms: 1000 +max_turns: 20 codex: - command: codex app-server - read_timeout_ms: 5000 - turn_timeout_ms: 3600000 - stall_timeout_ms: 300000 +command: codex app-server +read_timeout_ms: 5000 +turn_timeout_ms: 3600000 +stall_timeout_ms: 300000 --- # ═══ PROMPT BODY REFERENCE ═══ + # GitHub Project adaptation of the Elixir Symphony reference prompt. ## Status Map -| Status | Role | Agent Action | -| ------ | ---- | ------------ | -| Backlog | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | -| Ready | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | +| Status | Role | Agent Action | +| ----------- | ----- | --------------------------------------------------------- | +| Backlog | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | +| Ready | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | | In progress | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | -| In review | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | -| Done | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | +| In review | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | +| Done | unset | Role unset. Must be explicitly configured in WORKFLOW.md. | ## Default Posture diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..f2dfd3def --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# Dependencies and generated build/test output +node_modules/ +dist/ +coverage/ + +# Runtime state and package-manager output +.runtime/ +pnpm-lock.yaml + +# Prettier's configuration is intentionally maintained by hand +prettier.config.mjs diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json index 1330a17a4..697664079 100644 --- a/.sisyphus/boulder.json +++ b/.sisyphus/boulder.json @@ -7,4 +7,4 @@ ], "plan_name": "init-workflow-ecosystem", "agent": "atlas" -} \ No newline at end of file +} diff --git a/.sisyphus/notepads/gh-cli-auth-migration/decisions.md b/.sisyphus/notepads/gh-cli-auth-migration/decisions.md index 0d1e86b16..b577347b9 100644 --- a/.sisyphus/notepads/gh-cli-auth-migration/decisions.md +++ b/.sisyphus/notepads/gh-cli-auth-migration/decisions.md @@ -1,16 +1,20 @@ # Decisions — gh-cli-auth-migration ## Token Caching Strategy + - Orchestrator caches token at startup (1x `gh auth token` call), not per-poll - Avoids subprocess overhead in 30s polling hot path ## Scope Check Behavior + - Fine-grained PATs report empty scopes → treat as valid (skip scope check) - Required scopes: `["repo", "read:org", "project"]` ## Token Broker + - `GITHUB_TOKEN_BROKER_URL/SECRET` pattern MUST NOT be modified - Keep existing broker code intact ## control-plane + - Explicitly out of scope — separate auth system, do not touch diff --git a/.sisyphus/notepads/gh-cli-auth-migration/learnings.md b/.sisyphus/notepads/gh-cli-auth-migration/learnings.md index 85758883b..72fd59463 100644 --- a/.sisyphus/notepads/gh-cli-auth-migration/learnings.md +++ b/.sisyphus/notepads/gh-cli-auth-migration/learnings.md @@ -126,6 +126,7 @@ ## Task 11: Full Verification Suite ### Key Findings + 1. **Lint**: Fixed one unused eslint-disable directive in ansi.ts - The `// eslint-disable-next-line no-control-regex` comment was unnecessary - The regex pattern doesn't trigger the rule it was disabling @@ -146,7 +147,9 @@ - Verified by testing on HEAD~1 commit ### Verification Scope + The gh-cli-auth-migration project includes: + - packages/cli (primary) - packages/core (dependency) - packages/orchestrator (dependency) @@ -159,7 +162,9 @@ The gh-cli-auth-migration project includes: All of these pass lint, test, typecheck, and build. ### Control-Plane Status + The control-plane app has pre-existing failures unrelated to gh-cli-auth-migration: + - Missing export in orchestrator-status-client - Missing module '../../../packages/worker/src/runtime' - Type mismatch in workspace-orchestrator @@ -168,8 +173,8 @@ The control-plane app has pre-existing failures unrelated to gh-cli-auth-migrati These are separate concerns and should be addressed in a separate task. ### Commit -- ba6e149: fix: remove unused eslint-disable directive in ansi.ts +- ba6e149: fix: remove unused eslint-disable directive in ansi.ts ## F4 Scope Fidelity Audit (2026-03-13) diff --git a/.sisyphus/notepads/init-workflow-ecosystem/learnings.md b/.sisyphus/notepads/init-workflow-ecosystem/learnings.md index 0375d475e..c7d1169d3 100644 --- a/.sisyphus/notepads/init-workflow-ecosystem/learnings.md +++ b/.sisyphus/notepads/init-workflow-ecosystem/learnings.md @@ -43,6 +43,7 @@ - Test files: \*.test.ts alongside source files ## [Task 6] Core Skill Templates + - gh-symphony.ts: generateGhSymphonySkill() — design/refine/validate WORKFLOW.md - gh-project.ts: generateGhProjectSkill() — GitHub Project v2 status management - Dynamic Column ID table from ctx.statusColumns @@ -50,6 +51,7 @@ - Gotcha: "No unsupported `{{variable}}` patterns" in Validate Mode section triggered the double-brace test; replaced with prose description ## [Task 7] Workflow Skill Templates + - commit.ts: generateCommitSkill() — conventional commit format, logical units, test before commit - push.ts: generatePushSkill() — git push workflow, no --force, verify CI starts - pull.ts: generatePullSkill() — git fetch + merge, conflict resolution, record evidence @@ -62,6 +64,7 @@ - Commit: feat(cli): add workflow skill templates (commit, push, pull, land) ## [Task 8] Wire Ecosystem into Init Command + - writeEcosystem() helper: orchestrates detectEnvironment → buildContextYaml → generateReferenceWorkflow → writeAllSkills - writeContextYaml(outputDir, ctx) expects repo root as outputDir — it appends .gh-symphony/context.yaml internally - `as const` on test fixtures causes readonly array incompatibility with mutable ProjectDetail types — use explicit type annotations instead diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 66fdfdee2..1b8b60f9c 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -11,7 +11,11 @@ services: volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-github_symphony}"] + test: + [ + "CMD-SHELL", + "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-github_symphony}", + ] interval: 5s timeout: 5s retries: 20 diff --git a/docker-compose.yml b/docker-compose.yml index d75e10ec9..fb357603f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,11 @@ services: volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-github_symphony}"] + test: + [ + "CMD-SHELL", + "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-github_symphony}", + ] interval: 5s timeout: 5s retries: 20 diff --git a/docs/README.md b/docs/README.md index 961c6a73a..e0da91415 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,14 +50,14 @@ Provider-specific compact adapter profiles and host-side agent-tool contracts: ## reports/ -| Document | Status | -| ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| [2026-09-05-maintainability-and-reliability-review.md](reports/2026-09-05-maintainability-and-reliability-review.md) | Review complete — proposed fixes and trade-offs for CI, orchestration, storage, and Linear normalization | -| [2026-05-04-single-repo-orchestrator-feasibility.md](reports/2026-05-04-single-repo-orchestrator-feasibility.md) | Concluded — promoted to an ADR | -| [2026-06-25-spec-gap-analysis.md](reports/2026-06-25-spec-gap-analysis.md) | Retired — living-map upkeep stopped, final snapshot | -| [2026-07-06-risk-audit-report.md](reports/2026-07-06-risk-audit-report.md) | Awaiting review (issues not filed) | -| [2026-07-19-github-api-rate-limit-audit.md](reports/2026-07-19-github-api-rate-limit-audit.md) | Partially implemented (R1.5 shipped) | -| [2026-08-28-upstream-spec-drift-research.md](reports/2026-08-28-upstream-spec-drift-research.md) | Complete (Epic #651 scope) — see its documented carve-outs; C1–C13/D1–D8 follow-up shipped in [#675](https://github.com/hojinzs/github-symphony/issues/675) | +| Document | Status | +| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [2026-09-05-maintainability-and-reliability-review.md](reports/2026-09-05-maintainability-and-reliability-review.md) | Review complete — proposed fixes and trade-offs for CI, orchestration, storage, and Linear normalization | +| [2026-05-04-single-repo-orchestrator-feasibility.md](reports/2026-05-04-single-repo-orchestrator-feasibility.md) | Concluded — promoted to an ADR | +| [2026-06-25-spec-gap-analysis.md](reports/2026-06-25-spec-gap-analysis.md) | Retired — living-map upkeep stopped, final snapshot | +| [2026-07-06-risk-audit-report.md](reports/2026-07-06-risk-audit-report.md) | Awaiting review (issues not filed) | +| [2026-07-19-github-api-rate-limit-audit.md](reports/2026-07-19-github-api-rate-limit-audit.md) | Partially implemented (R1.5 shipped) | +| [2026-08-28-upstream-spec-drift-research.md](reports/2026-08-28-upstream-spec-drift-research.md) | Complete (Epic #651 scope) — see its documented carve-outs; C1–C13/D1–D8 follow-up shipped in [#675](https://github.com/hojinzs/github-symphony/issues/675) | ## adr/ diff --git a/docs/reports/2026-09-05-maintainability-and-reliability-review.md b/docs/reports/2026-09-05-maintainability-and-reliability-review.md index d6df74650..392be1064 100644 --- a/docs/reports/2026-09-05-maintainability-and-reliability-review.md +++ b/docs/reports/2026-09-05-maintainability-and-reliability-review.md @@ -11,12 +11,12 @@ Address the CI test-path mismatch first, then isolate malformed Linear list records. Measure historical-run read cost before changing storage architecture. Extract orchestrator responsibilities incrementally after the test baseline is consistent. -| Finding | Classification | Impact | Recommended action | Relative change risk | -| --- | --- | --- | --- | --- | -| R1: CI and package test execution differ | Confirmed verification gap | Three React test files are excluded; package setup and execution settings are bypassed | Make package-aware execution authoritative in CI, then consolidate coverage | Low for the CI gate; medium for coverage migration | -| R2: Orchestrator responsibility concentration | Confirmed maintainability concern, not a reproduced runtime defect | State, process, tracker, workspace, and retry changes are difficult to isolate | Extract pure decisions first, then small effect-owning collaborators | Medium; high for a wholesale rewrite | -| R3: Repeated full run-history reads | Confirmed access pattern; production performance impact unmeasured | Work per tick grows with retained history, including unrelated projects in shared/legacy layouts | Establish a baseline, add project-scoped bounded reads, then optimize measured hot paths | Medium; high for a new persistent index or database | -| R4: Linear malformed-record blast radius | Reproduced availability limitation | One invalid list record prevents otherwise valid candidates from being returned | Tolerant state-list normalization with diagnostics; strict ID refresh | Low to medium | +| Finding | Classification | Impact | Recommended action | Relative change risk | +| --------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------- | +| R1: CI and package test execution differ | Confirmed verification gap | Three React test files are excluded; package setup and execution settings are bypassed | Make package-aware execution authoritative in CI, then consolidate coverage | Low for the CI gate; medium for coverage migration | +| R2: Orchestrator responsibility concentration | Confirmed maintainability concern, not a reproduced runtime defect | State, process, tracker, workspace, and retry changes are difficult to isolate | Extract pure decisions first, then small effect-owning collaborators | Medium; high for a wholesale rewrite | +| R3: Repeated full run-history reads | Confirmed access pattern; production performance impact unmeasured | Work per tick grows with retained history, including unrelated projects in shared/legacy layouts | Establish a baseline, add project-scoped bounded reads, then optimize measured hot paths | Medium; high for a new persistent index or database | +| R4: Linear malformed-record blast radius | Reproduced availability limitation | One invalid list record prevents otherwise valid candidates from being returned | Tolerant state-list normalization with diagnostics; strict ID refresh | Low to medium | These priorities reflect certainty and breadth of impact, not measured incident frequency. If Linear is unused, R4 can follow the storage baseline. If a deployment already has measured polling delays, advance R3 ahead of structural extraction. @@ -28,13 +28,13 @@ These priorities reflect certainty and breadth of impact, not measured incident Actual discovery on the reviewed revision: -| Control-plane test file | Root discovery | Package discovery | -| --- | --- | --- | -| `src/server.test.ts` | Included | Included | -| `client/src/lib/api.test.ts` | Included | Included | -| `client/src/issueDetail.test.tsx` | Excluded | Included | -| `client/src/components/components.test.tsx` | Excluded | Included | -| `client/src/routes/-index.test.tsx` | Excluded | Included | +| Control-plane test file | Root discovery | Package discovery | +| ------------------------------------------- | -------------- | ----------------- | +| `src/server.test.ts` | Included | Included | +| `client/src/lib/api.test.ts` | Included | Included | +| `client/src/issueDetail.test.tsx` | Excluded | Included | +| `client/src/components/components.test.tsx` | Excluded | Included | +| `client/src/routes/-index.test.tsx` | Excluded | Included | The excluded files contain 17 tests for rendering, status badges, retry errors, stale-data warnings, links, and component behavior. A regression confined to these assertions can escape the CI unit-test gate even though package tests would catch it. Compilation and linting do not execute these assertions; these tests are primarily static-render tests, not full browser interaction coverage. @@ -42,11 +42,11 @@ The mismatch extends beyond filename patterns. [The orchestrator configuration]( ### Alternatives -| Option | Benefit | Cost or limitation | -| --- | --- | --- | -| Expand the root include to `.test.{ts,tsx}` | Small diff; closes the immediately visible omission | Does not preserve package setup, aliases, defines, or serialization; incomplete as the final fix | -| Use package scripts as the authoritative CI gate | Aligns with `pnpm test`, preserves existing package configuration, straightforward rollback | A transitional separate coverage run duplicates work; coverage collection still needs consolidation | -| Introduce one root Vitest project configuration referencing package configs | One coordinated discovery and coverage entry point | Must validate project roots, relative setup paths, aliases, package-specific scheduling, and packages without a local config; larger migration | +| Option | Benefit | Cost or limitation | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| Expand the root include to `.test.{ts,tsx}` | Small diff; closes the immediately visible omission | Does not preserve package setup, aliases, defines, or serialization; incomplete as the final fix | +| Use package scripts as the authoritative CI gate | Aligns with `pnpm test`, preserves existing package configuration, straightforward rollback | A transitional separate coverage run duplicates work; coverage collection still needs consolidation | +| Introduce one root Vitest project configuration referencing package configs | One coordinated discovery and coverage entry point | Must validate project roots, relative setup paths, aliases, package-specific scheduling, and packages without a local config; larger migration | ### Recommended direction @@ -76,11 +76,11 @@ The practical concern is the number of invariants a maintainer must understand f ### Alternatives -| Option | Benefit | Cost or limitation | -| --- | --- | --- | -| Keep one service and reorganize methods/comments | Minimal behavioral risk | Improves navigation but leaves shared mutable state and coupling intact | -| Incrementally extract decisions, then cohesive effects | Smaller review scope, focused tests, no data migration | Requires careful interfaces and several changes; some façade complexity remains | -| Replace with a new workflow engine, actor model, or distributed services | Potentially clearer ownership at larger scale | Changes execution ordering, persistence, deployment, and failure modes simultaneously; benefits are not established by this review | +| Option | Benefit | Cost or limitation | +| ------------------------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| Keep one service and reorganize methods/comments | Minimal behavioral risk | Improves navigation but leaves shared mutable state and coupling intact | +| Incrementally extract decisions, then cohesive effects | Smaller review scope, focused tests, no data migration | Requires careful interfaces and several changes; some façade complexity remains | +| Replace with a new workflow engine, actor model, or distributed services | Potentially clearer ownership at larger scale | Changes execution ordering, persistence, deployment, and failure modes simultaneously; benefits are not established by this review | ### Recommended direction @@ -108,12 +108,12 @@ One isolated diagnostic confirmed that an inventory containing an old success, a ### Alternatives -| Option | Benefit | Cost or limitation | -| --- | --- | --- | -| Reuse one snapshot throughout a tick | Reduces repeated scans with little storage change | Can hide asynchronous worker or tracker writes; an immutable snapshot reused blindly may make concurrency or completion decisions stale | -| Add project-scoped reads and bounded I/O, then reduce redundant reads at explicit boundaries | Limits unrelated work and read fan-out; preserves JSON storage and rollback | Still scales with that project's history; requires legacy-layout compatibility and careful refresh points | -| Maintain an active/latest-run index and incremental historical aggregates | Can make steady-state cost depend mainly on active runs | Introduces index consistency, crash recovery, rebuild, and multi-writer questions; metrics must remain exact | -| Move state to SQLite | Indexed queries and transactional updates | Migration, packaging, backup, lock-contention, and rollback work; does not by itself fix inefficient query patterns | +| Option | Benefit | Cost or limitation | +| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| Reuse one snapshot throughout a tick | Reduces repeated scans with little storage change | Can hide asynchronous worker or tracker writes; an immutable snapshot reused blindly may make concurrency or completion decisions stale | +| Add project-scoped reads and bounded I/O, then reduce redundant reads at explicit boundaries | Limits unrelated work and read fan-out; preserves JSON storage and rollback | Still scales with that project's history; requires legacy-layout compatibility and careful refresh points | +| Maintain an active/latest-run index and incremental historical aggregates | Can make steady-state cost depend mainly on active runs | Introduces index consistency, crash recovery, rebuild, and multi-writer questions; metrics must remain exact | +| Move state to SQLite | Indexed queries and transactional updates | Migration, packaging, backup, lock-contention, and rollback work; does not by itself fix inefficient query patterns | ### Recommended direction @@ -143,11 +143,11 @@ Important correction to severity: upstream §11.1 says a state-list call **MAY** ### Alternatives -| Option | Benefit | Cost or limitation | -| --- | --- | --- | -| Keep strict lists and improve diagnostics | Smallest change, clearly exposes data problems | One bad candidate continues blocking valid candidates | -| Tolerate known record-validation failures in state lists, keep ID refresh strict | Valid work continues; preserves the meaning of refresh omission | Needs bounded diagnostics and metadata preservation; skipped data requires operator visibility | -| Catch all failures and return whatever was collected | Appears resilient | Can hide programming, auth, or paging failures and misrepresent an incomplete response as complete; reject this option | +| Option | Benefit | Cost or limitation | +| -------------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| Keep strict lists and improve diagnostics | Smallest change, clearly exposes data problems | One bad candidate continues blocking valid candidates | +| Tolerate known record-validation failures in state lists, keep ID refresh strict | Valid work continues; preserves the meaning of refresh omission | Needs bounded diagnostics and metadata preservation; skipped data requires operator visibility | +| Catch all failures and return whatever was collected | Appears resilient | Can hide programming, auth, or paging failures and misrepresent an incomplete response as complete; reject this option | ### Recommended direction @@ -174,17 +174,17 @@ No precise calendar estimate is warranted without agreeing on the coverage strat ## Test cases and verification -| TC | Verification | Result or implementation acceptance | -| --- | --- | --- | -| TC-01 | Compare root and control-plane package discovery | Executed: root finds 2 files; package finds 5, including the 3 TSX files | -| TC-02 | Mixed valid/malformed Linear state list | Executed diagnostic: current implementation rejects the entire list | -| TC-03 | Malformed requested-ID refresh | Executed diagnostic: rejects rather than implying omission | -| TC-04 | Historical and cross-project run inventory | Executed diagnostic: all 3 fixture records are returned | -| TC-05 | Complete repository unit suite | Fresh verification result recorded below | -| TC-06 | CI-equivalent failing TSX sentinel, setup isolation, coverage merge | Required when implementing R1; not executed as a repository mutation here | -| TC-07 | Retry reservation, bounded recovery, shutdown, hook order, unpublished work | Preserve façade regressions for R2; run relevant Docker scenarios after execution changes | -| TC-08 | History growth, update freshness, legacy layout, cumulative metrics | Required when implementing R3; no performance claim from this report | -| TC-09 | Mixed/all-invalid lists, strict refresh, page failure, label metadata | Required when implementing R4; use mocked Linear responses and the existing live-provider acceptance procedure when needed | +| TC | Verification | Result or implementation acceptance | +| ----- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| TC-01 | Compare root and control-plane package discovery | Executed: root finds 2 files; package finds 5, including the 3 TSX files | +| TC-02 | Mixed valid/malformed Linear state list | Executed diagnostic: current implementation rejects the entire list | +| TC-03 | Malformed requested-ID refresh | Executed diagnostic: rejects rather than implying omission | +| TC-04 | Historical and cross-project run inventory | Executed diagnostic: all 3 fixture records are returned | +| TC-05 | Complete repository unit suite | Fresh verification result recorded below | +| TC-06 | CI-equivalent failing TSX sentinel, setup isolation, coverage merge | Required when implementing R1; not executed as a repository mutation here | +| TC-07 | Retry reservation, bounded recovery, shutdown, hook order, unpublished work | Preserve façade regressions for R2; run relevant Docker scenarios after execution changes | +| TC-08 | History growth, update freshness, legacy layout, cumulative metrics | Required when implementing R3; no performance claim from this report | +| TC-09 | Mixed/all-invalid lists, strict refresh, page failure, label metadata | Required when implementing R4; use mocked Linear responses and the existing live-provider acceptance procedure when needed | The three diagnostic cases were written in an external temporary directory and executed against current source: 3 passed. They describe existing behavior, not implemented fixes. No production credentials or network calls were used by these probes. diff --git a/docs/trackers/github-project.md b/docs/trackers/github-project.md index 6a59e1cb0..4d47729b9 100644 --- a/docs/trackers/github-project.md +++ b/docs/trackers/github-project.md @@ -6,14 +6,14 @@ 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. | +| 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 rejected; run `gh-symphony doctor` for migration guidance. | -| 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. | +| 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`. diff --git a/e2e/scenarios/02-multi-issue.md b/e2e/scenarios/02-multi-issue.md index 6600a0ca3..d9f913e65 100644 --- a/e2e/scenarios/02-multi-issue.md +++ b/e2e/scenarios/02-multi-issue.md @@ -10,27 +10,32 @@ curl --retry 10 --retry-delay 2 http://localhost:4680/healthz ## Steps 1. **Inject multi-issue fixture** (3 issues, concurrency_limit=2 in WORKFLOW.md) + ```bash cp e2e/fixtures/multi-issue.json e2e/fixtures/issues.json ``` 2. **Trigger reconciliation** + ```bash curl -X POST http://localhost:4680/api/v1/refresh ``` 3. **Verify concurrency cap** (poll within 10s) + ```bash curl -s http://localhost:4680/api/v1/status | jq '.activeRuns | length' # Expected: 2 (third issue is queued, not dispatched) ``` 4. **Wait for first batch completion** (~7s) + ```bash curl -s http://localhost:4680/api/v1/status | jq '.summary' ``` 5. **Trigger another reconciliation** to dispatch the queued issue + ```bash curl -X POST http://localhost:4680/api/v1/refresh ``` diff --git a/e2e/scenarios/05-before-remove-hook-failure.md b/e2e/scenarios/05-before-remove-hook-failure.md index fff4909bf..d2d24246f 100644 --- a/e2e/scenarios/05-before-remove-hook-failure.md +++ b/e2e/scenarios/05-before-remove-hook-failure.md @@ -11,16 +11,17 @@ curl --retry 10 --retry-delay 2 http://localhost:4680/healthz ## Steps 1. Seed a failing `before_remove` hook into the E2E repository. + ```bash docker compose -f docker-compose.e2e.yml exec symphony-e2e sh -lc ' cd /e2e/work/test-repo && mkdir -p hooks && cat > hooks/before_remove.sh <<'"'"'EOF'"'"' -#!/usr/bin/env bash -set -eu -printf "cleanup hook failed" >&2 -exit 1 -EOF + #!/usr/bin/env bash + set -eu + printf "cleanup hook failed" >&2 + exit 1 + EOF chmod +x hooks/before_remove.sh && awk ' !inserted && /^polling:$/ { @@ -37,12 +38,14 @@ EOF ``` 2. Inject an active issue and trigger reconciliation. + ```bash cp e2e/fixtures/happy-path.json e2e/fixtures/issues.json curl -X POST http://localhost:4680/api/v1/refresh ``` 3. Wait until the issue workspace is created. + ```bash docker compose -f docker-compose.e2e.yml exec symphony-e2e sh -lc ' for i in $(seq 1 20); do @@ -54,6 +57,7 @@ EOF ``` 4. Mark the same issue as terminal and trigger reconciliation again. + ```bash cat > e2e/fixtures/issues.json <<'EOF' [{ @@ -93,12 +97,12 @@ EOF record=$(find /e2e/work/test-repo/.runtime/orchestrator -name workspace.json | head -n 1) [ -n "$record" ] || { sleep 1; continue; } python3 - "$record" <<'"'"'PY'"'"' -import json, sys -with open(sys.argv[1]) as fh: + import json, sys + with open(sys.argv[1]) as fh: data = json.load(fh) -print(data["status"]) -sys.exit(0 if data["status"] == "removed" else 1) -PY + print(data["status"]) + sys.exit(0 if data["status"] == "removed" else 1) + PY [ $? -eq 0 ] && exit 0 sleep 1 done diff --git a/e2e/scenarios/07-release-missing-retry.md b/e2e/scenarios/07-release-missing-retry.md index 8339fe7cc..0ce96766d 100644 --- a/e2e/scenarios/07-release-missing-retry.md +++ b/e2e/scenarios/07-release-missing-retry.md @@ -13,12 +13,14 @@ curl --fail --retry-all-errors --retry 10 --retry-delay 2 http://localhost:4680/ ## Steps 1. Inject a single active issue and trigger reconciliation. + ```bash cp e2e/fixtures/happy-path.json e2e/fixtures/issues.json curl -s -X POST http://localhost:4680/api/v1/refresh ``` 2. Wait until the worker fails once and the orchestrator reports a queued retry. + ```bash curl -s http://localhost:4680/api/v1/test-owner%2Ftest-repo%231 | jq '{ status, @@ -29,6 +31,7 @@ curl --fail --retry-all-errors --retry 10 --retry-delay 2 http://localhost:4680/ ``` 3. Remove the issue before the queued retry becomes due. + ```bash echo "[]" > e2e/fixtures/issues.json ``` diff --git a/eslint.config.mjs b/eslint.config.mjs index 4c9390324..dbe74966e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,8 +9,8 @@ export default tseslint.config( "coverage/**", "dist/**", "node_modules/**", - "openspec/**" - ] + "openspec/**", + ], }, js.configs.recommended, ...tseslint.configs.recommended, @@ -19,17 +19,17 @@ export default tseslint.config( languageOptions: { globals: { ...globals.node, - ...globals.browser - } + ...globals.browser, + }, }, rules: { "@typescript-eslint/no-unused-vars": [ "error", { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_" - } - ] - } + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + }, } ); diff --git a/packages/cli/src/commands/help.test.ts b/packages/cli/src/commands/help.test.ts index b2b06c254..038bdb43b 100644 --- a/packages/cli/src/commands/help.test.ts +++ b/packages/cli/src/commands/help.test.ts @@ -1,11 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { setNoColor, stripAnsi } from "../ansi.js"; import { runCli } from "../index.js"; -import { - COMMAND_COLUMN_WIDTH, - HELP_SECTIONS, - renderHelp, -} from "./help.js"; +import { COMMAND_COLUMN_WIDTH, HELP_SECTIONS, renderHelp } from "./help.js"; function captureWrites(stream: NodeJS.WriteStream): { output: () => string; diff --git a/packages/cli/src/commands/upgrade.ts b/packages/cli/src/commands/upgrade.ts index b4bc4a4af..f33047acc 100644 --- a/packages/cli/src/commands/upgrade.ts +++ b/packages/cli/src/commands/upgrade.ts @@ -70,12 +70,7 @@ export async function fetchLatestCliVersion( const runExecFile = deps?.execFileImpl ?? execFileAsync; const { stdout } = await runExecFile( resolvePackageManagerExecutable("npm", deps?.platform), - [ - "view", - PACKAGE_NAME, - "dist-tags.latest", - "--json", - ] + ["view", PACKAGE_NAME, "dist-tags.latest", "--json"] ); const raw = stdout.trim(); @@ -166,9 +161,7 @@ export async function runUpgradeInstall( resolve(); return; } - reject( - new Error(`${command} exited with code ${code ?? "unknown"}.`) - ); + reject(new Error(`${command} exited with code ${code ?? "unknown"}.`)); }); }); } diff --git a/packages/cli/src/detection/environment-detector.test.ts b/packages/cli/src/detection/environment-detector.test.ts index 062390c45..887f6debe 100644 --- a/packages/cli/src/detection/environment-detector.test.ts +++ b/packages/cli/src/detection/environment-detector.test.ts @@ -146,7 +146,10 @@ describe("detectEnvironment", () => { }); it("detects poetry-managed python repositories with pytest guidance", async () => { - await writeFile(join(tempDir, "pyproject.toml"), "[project]\nname = 'poetry-fixture'\n"); + await writeFile( + join(tempDir, "pyproject.toml"), + "[project]\nname = 'poetry-fixture'\n" + ); await writeFile(join(tempDir, "poetry.lock"), "package = []\n"); await writeFile(join(tempDir, "pytest.ini"), "[pytest]\n"); @@ -301,7 +304,10 @@ describe("detectEnvironment", () => { }); it("detects Cargo workspace monorepos", async () => { - await writeFile(join(tempDir, "Cargo.toml"), "[workspace]\nmembers = [\"crates/*\"]\n"); + await writeFile( + join(tempDir, "Cargo.toml"), + '[workspace]\nmembers = ["crates/*"]\n' + ); const result = await detectEnvironment(tempDir); diff --git a/packages/cli/src/detection/environment-detector.ts b/packages/cli/src/detection/environment-detector.ts index e83244d37..3a5496e91 100644 --- a/packages/cli/src/detection/environment-detector.ts +++ b/packages/cli/src/detection/environment-detector.ts @@ -1,13 +1,7 @@ import { access, readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; -type DetectedPackageManager = - | "pnpm" - | "npm" - | "yarn" - | "bun" - | "uv" - | "poetry"; +type DetectedPackageManager = "pnpm" | "npm" | "yarn" | "bun" | "uv" | "poetry"; export type DetectedEnvironment = { packageManager: DetectedPackageManager | null; @@ -239,8 +233,7 @@ async function detectPythonCommands(cwd: string): Promise { } const hasPytestConfig = - hasPytestIni || - /\[tool\.pytest(?:\.ini_options)?\]/.test(pyproject ?? ""); + hasPytestIni || /\[tool\.pytest(?:\.ini_options)?\]/.test(pyproject ?? ""); if (!hasPytestConfig) { return { testCommand: null, buildCommand: null, lintCommand: null }; } @@ -272,16 +265,24 @@ async function detectRustCommands(cwd: string): Promise { }; } -async function detectValidationCommands(cwd: string): Promise { - const [makeCommands, justCommands, nodeCommands, pythonCommands, goCommands, rustCommands] = - await Promise.all([ - detectMakeCommands(cwd), - detectJustCommands(cwd), - detectNodeScripts(cwd), - detectPythonCommands(cwd), - detectGoCommands(cwd), - detectRustCommands(cwd), - ]); +async function detectValidationCommands( + cwd: string +): Promise { + const [ + makeCommands, + justCommands, + nodeCommands, + pythonCommands, + goCommands, + rustCommands, + ] = await Promise.all([ + detectMakeCommands(cwd), + detectJustCommands(cwd), + detectNodeScripts(cwd), + detectPythonCommands(cwd), + detectGoCommands(cwd), + detectRustCommands(cwd), + ]); const candidates: Record = { testCommand: [], diff --git a/packages/cli/src/workflow/workflow-runtime.ts b/packages/cli/src/workflow/workflow-runtime.ts index 744c8f7c0..ab772d1e3 100644 --- a/packages/cli/src/workflow/workflow-runtime.ts +++ b/packages/cli/src/workflow/workflow-runtime.ts @@ -17,7 +17,9 @@ export const DEFAULT_CLAUDE_PRINT_ARGS = [ "bypassPermissions", ] as const; -export function normalizeInitRuntime(runtime: string): InitRuntimeKind | string { +export function normalizeInitRuntime( + runtime: string +): InitRuntimeKind | string { if (runtime === "codex") { return "codex-app-server"; } diff --git a/packages/control-plane/client/.storybook/preview.tsx b/packages/control-plane/client/.storybook/preview.tsx index e9e91decd..9476559e5 100644 --- a/packages/control-plane/client/.storybook/preview.tsx +++ b/packages/control-plane/client/.storybook/preview.tsx @@ -5,7 +5,12 @@ import "../src/index.css"; const preview: Preview = { decorators: [ (Story) => ( - +
diff --git a/packages/control-plane/client/index.html b/packages/control-plane/client/index.html index bb893ea8a..10ff6c518 100644 --- a/packages/control-plane/client/index.html +++ b/packages/control-plane/client/index.html @@ -9,4 +9,4 @@
- + diff --git a/packages/control-plane/client/src/components/Button.tsx b/packages/control-plane/client/src/components/Button.tsx index a92956299..76b449345 100644 --- a/packages/control-plane/client/src/components/Button.tsx +++ b/packages/control-plane/client/src/components/Button.tsx @@ -34,8 +34,7 @@ type ButtonAsChildProps = ButtonSharedProps & export type ButtonProps = ButtonAsButtonProps | ButtonAsChildProps; const VARIANT_STYLES: Record = { - primary: - "border-transparent bg-interactive text-white hover:brightness-110", + primary: "border-transparent bg-interactive text-white hover:brightness-110", ghost: "border-border-subtle bg-bg-muted text-text-secondary hover:border-text-secondary/40 hover:text-text-primary", destructive: @@ -95,7 +94,9 @@ export function Button(props: ButtonProps) { onKeyDown={(event) => { if ( disabled && - (event.key === "Enter" || event.key === " " || event.key === "Spacebar") + (event.key === "Enter" || + event.key === " " || + event.key === "Spacebar") ) { blockDisabledEvent(event); return; diff --git a/packages/control-plane/client/src/components/components.test.tsx b/packages/control-plane/client/src/components/components.test.tsx index f96f7738e..8bd2effb7 100644 --- a/packages/control-plane/client/src/components/components.test.tsx +++ b/packages/control-plane/client/src/components/components.test.tsx @@ -26,7 +26,7 @@ describe("Button", () => { it("renders a primary button by default", () => { const markup = renderToStaticMarkup(); - expect(markup).toContain("type=\"button\""); + expect(markup).toContain('type="button"'); expect(markup).toContain("bg-interactive"); expect(markup).toContain("px-4"); expect(markup).toContain("Refresh"); @@ -56,7 +56,7 @@ describe("Button", () => { ); expect(markup).toContain(" { ); - expect(markup).toContain("aria-disabled=\"true\""); - expect(markup).toContain("data-disabled=\"\""); + expect(markup).toContain('aria-disabled="true"'); + expect(markup).toContain('data-disabled=""'); expect(markup).not.toMatch(/\sdisabled=/); - expect(markup).toContain("tabindex=\"-1\""); + expect(markup).toContain('tabindex="-1"'); }); }); diff --git a/packages/control-plane/client/src/main.tsx b/packages/control-plane/client/src/main.tsx index 54b8301ff..74d44d14a 100644 --- a/packages/control-plane/client/src/main.tsx +++ b/packages/control-plane/client/src/main.tsx @@ -9,7 +9,12 @@ import { router } from "./router"; function App() { return ( - + diff --git a/packages/control-plane/client/src/pages/FoundationsPage.tsx b/packages/control-plane/client/src/pages/FoundationsPage.tsx index 1c08520c7..41ac075de 100644 --- a/packages/control-plane/client/src/pages/FoundationsPage.tsx +++ b/packages/control-plane/client/src/pages/FoundationsPage.tsx @@ -13,8 +13,8 @@ export function FoundationsPage() { GitHub Symphony Control Plane

- Dark-mode design tokens and shared controls for the upcoming - project overview and issue detail surfaces. + Dark-mode design tokens and shared controls for the upcoming project + overview and issue detail surfaces.

diff --git a/packages/control-plane/client/src/routes/-index.test.tsx b/packages/control-plane/client/src/routes/-index.test.tsx index 4cb57e2d8..eca439d01 100644 --- a/packages/control-plane/client/src/routes/-index.test.tsx +++ b/packages/control-plane/client/src/routes/-index.test.tsx @@ -179,8 +179,12 @@ describe("Project overview helpers", () => {
); - expect(markup).toContain('href="https://github.com/acme/platform/issues/42"'); - expect(markup).toContain('href="https://github.com/acme/platform/issues/43"'); + expect(markup).toContain( + 'href="https://github.com/acme/platform/issues/42"' + ); + expect(markup).toContain( + 'href="https://github.com/acme/platform/issues/43"' + ); expect(markup).toContain("acme/platform#42"); }); }); diff --git a/packages/control-plane/client/src/routes/issues/$identifier.tsx b/packages/control-plane/client/src/routes/issues/$identifier.tsx index 265c27f56..8a59e9a40 100644 --- a/packages/control-plane/client/src/routes/issues/$identifier.tsx +++ b/packages/control-plane/client/src/routes/issues/$identifier.tsx @@ -117,7 +117,10 @@ export function IssueDetailView({
- + { afterEach(async () => { await Promise.all( - tempDirs.splice(0).map((path) => - rm(path, { recursive: true, force: true }) - ) + tempDirs + .splice(0) + .map((path) => rm(path, { recursive: true, force: true })) ); }); diff --git a/packages/core/src/observability/fs-reader.ts b/packages/core/src/observability/fs-reader.ts index 4aa0294cd..81a238b1d 100644 --- a/packages/core/src/observability/fs-reader.ts +++ b/packages/core/src/observability/fs-reader.ts @@ -28,8 +28,8 @@ export async function safeReadDir(path: string): Promise { export function isFileMissing(error: unknown): boolean { return Boolean( error && - typeof error === "object" && - "code" in error && - (error.code === "ENOENT" || error.code === "ENOTDIR") + typeof error === "object" && + "code" in error && + (error.code === "ENOENT" || error.code === "ENOTDIR") ); } diff --git a/packages/core/src/orchestration/retry-policy.ts b/packages/core/src/orchestration/retry-policy.ts index 9663785f1..5e70c23fe 100644 --- a/packages/core/src/orchestration/retry-policy.ts +++ b/packages/core/src/orchestration/retry-policy.ts @@ -1,7 +1,7 @@ import { DEFAULT_BASE_DELAY_MS, DEFAULT_MAX_DELAY_MS, - type RetryPolicyOptions + type RetryPolicyOptions, } from "../workflow/config.js"; export function calculateRetryDelay( diff --git a/packages/core/src/runtime/mcp-compose.ts b/packages/core/src/runtime/mcp-compose.ts index 98793739e..9981b3902 100644 --- a/packages/core/src/runtime/mcp-compose.ts +++ b/packages/core/src/runtime/mcp-compose.ts @@ -3,17 +3,19 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { readEnvFile } from "../workspace/env-file.js"; -export type McpServerDefinition = { - command: string; - args?: string[]; - env?: Record; - [key: string]: unknown; -} | { - type: "sse" | "http"; - url: string; - headers?: Record; - [key: string]: unknown; -}; +export type McpServerDefinition = + | { + command: string; + args?: string[]; + env?: Record; + [key: string]: unknown; + } + | { + type: "sse" | "http"; + url: string; + headers?: Record; + [key: string]: unknown; + }; export type McpCompositionOptions = { repositoryDir: string; diff --git a/packages/core/src/workflow/render.test.ts b/packages/core/src/workflow/render.test.ts index b32e65979..5577d7f22 100644 --- a/packages/core/src/workflow/render.test.ts +++ b/packages/core/src/workflow/render.test.ts @@ -92,24 +92,24 @@ describe("buildPromptVariables", () => { createTrackedIssue({ contentType: "Issue", linkedPullRequests: [ - { - id: "pr-1", - number: 7, - identifier: "acme/platform#7", - url: "https://github.com/acme/platform/pull/7", - state: "OPEN", - projectState: "In review", - isDraft: false, - merged: false, - headRefName: "fix/issue-42", - baseRefName: "main", - repository: { - owner: "acme", - name: "platform", - url: "https://github.com/acme/platform", - cloneUrl: "https://github.com/acme/platform.git", - }, + { + id: "pr-1", + number: 7, + identifier: "acme/platform#7", + url: "https://github.com/acme/platform/pull/7", + state: "OPEN", + projectState: "In review", + isDraft: false, + merged: false, + headRefName: "fix/issue-42", + baseRefName: "main", + repository: { + owner: "acme", + name: "platform", + url: "https://github.com/acme/platform", + cloneUrl: "https://github.com/acme/platform.git", }, + }, ], }), { attempt: null } @@ -189,36 +189,36 @@ describe("buildPromptVariables", () => { createTrackedIssue({ contentType: "Issue", linkedPullRequests: [ - { - id: "pr-7", - number: 7, - identifier: "acme/platform#7", - url: "https://github.com/acme/platform/pull/7", - state: "OPEN", - headRefName: "fix/issue-42", - baseRefName: "main", - isDraft: false, - merged: false, - repository: { - owner: "acme", - name: "platform", - url: "https://github.com/acme/platform", - cloneUrl: "https://github.com/acme/platform.git", - }, + { + id: "pr-7", + number: 7, + identifier: "acme/platform#7", + url: "https://github.com/acme/platform/pull/7", + state: "OPEN", + headRefName: "fix/issue-42", + baseRefName: "main", + isDraft: false, + merged: false, + repository: { + owner: "acme", + name: "platform", + url: "https://github.com/acme/platform", + cloneUrl: "https://github.com/acme/platform.git", }, - { - id: "pr-8", - number: 8, - identifier: "acme/platform#8", - url: "https://github.com/acme/platform/pull/8", - state: null, - repository: { - owner: "acme", - name: "platform", - url: "https://github.com/acme/platform", - cloneUrl: "https://github.com/acme/platform.git", - }, + }, + { + id: "pr-8", + number: 8, + identifier: "acme/platform#8", + url: "https://github.com/acme/platform/pull/8", + state: null, + repository: { + owner: "acme", + name: "platform", + url: "https://github.com/acme/platform", + cloneUrl: "https://github.com/acme/platform.git", }, + }, ], }), { attempt: null } @@ -256,24 +256,24 @@ describe("buildPromptVariables", () => { url: "https://github.com/acme/platform/pull/9", contentType: "PullRequest", pullRequest: { - id: "pr-9", - number: 9, - identifier: "acme/platform#9", - url: "https://github.com/acme/platform/pull/9", - state: "OPEN", - projectState: "In review", - headRefName: "feature/pr-metadata", - baseRefName: "main", + id: "pr-9", + number: 9, + identifier: "acme/platform#9", + url: "https://github.com/acme/platform/pull/9", + state: "OPEN", + projectState: "In review", + headRefName: "feature/pr-metadata", + baseRefName: "main", }, linkedPullRequests: [ - { - id: "pr-8", - number: 8, - identifier: "acme/platform#8", - url: "https://github.com/acme/platform/pull/8", - state: "OPEN", - projectState: "Ready", - }, + { + id: "pr-8", + number: 8, + identifier: "acme/platform#8", + url: "https://github.com/acme/platform/pull/8", + state: "OPEN", + projectState: "Ready", + }, ], }), { attempt: null } diff --git a/packages/core/src/workspace/index.ts b/packages/core/src/workspace/index.ts index 2efbc8231..1cc59e027 100644 --- a/packages/core/src/workspace/index.ts +++ b/packages/core/src/workspace/index.ts @@ -1,6 +1,10 @@ export const CORE_WORKSPACE_BOUNDARY = { module: "workspace", - responsibilities: ["issue-scoped identity", "workspace lifecycle", "hook surfaces"] + responsibilities: [ + "issue-scoped identity", + "workspace lifecycle", + "hook surfaces", + ], } as const; export * from "./env-file.js"; diff --git a/packages/extension-github-workflow/src/approval-workflow.ts b/packages/extension-github-workflow/src/approval-workflow.ts index bebcd310a..d8bd35927 100644 --- a/packages/extension-github-workflow/src/approval-workflow.ts +++ b/packages/extension-github-workflow/src/approval-workflow.ts @@ -139,9 +139,7 @@ export async function executePlanningPhase( fieldName: input.lifecycle.stateFieldName, state: input.transitionTo, }); - operations.push( - `transitioned item to ${input.transitionTo}` - ); + operations.push(`transitioned item to ${input.transitionTo}`); } return { @@ -217,9 +215,7 @@ export async function executeImplementationPhase( fieldName: input.lifecycle.stateFieldName, state: input.transitionTo, }); - operations.push( - `transitioned item to ${input.transitionTo}` - ); + operations.push(`transitioned item to ${input.transitionTo}`); } return { @@ -300,10 +296,7 @@ export function buildImplementationBranchName( return `symphony/issue-${issue.number}-${slug || "change"}`; } -export function buildPhaseMarker( - label: string, - issueId: string -): string { +export function buildPhaseMarker(label: string, issueId: string): string { return ``; } diff --git a/packages/orchestrator/src/skills.test.ts b/packages/orchestrator/src/skills.test.ts index c8846d052..46df5534d 100644 --- a/packages/orchestrator/src/skills.test.ts +++ b/packages/orchestrator/src/skills.test.ts @@ -109,7 +109,10 @@ describe("layered runtime skills", () => { "/worktree/.claude/skills" ); expect( - resolveRuntimeSkillsDirectory("/worktree", "/usr/local/bin/codex app-server") + resolveRuntimeSkillsDirectory( + "/worktree", + "/usr/local/bin/codex app-server" + ) ).toBe("/worktree/.codex/skills"); }); @@ -119,15 +122,35 @@ describe("layered runtime skills", () => { const seed = join(root, "seed"); const worktree = join(root, "worktree"); await execFileAsync("git", ["init", "-q", "-b", "main", seed]); - await execFileAsync("git", ["-C", seed, "config", "user.email", "test@example.com"]); - await execFileAsync("git", ["-C", seed, "config", "user.name", "Test User"]); + await execFileAsync("git", [ + "-C", + seed, + "config", + "user.email", + "test@example.com", + ]); + await execFileAsync("git", [ + "-C", + seed, + "config", + "user.name", + "Test User", + ]); await writeFile(join(seed, "README.md"), "seed", "utf8"); await execFileAsync("git", ["-C", seed, "add", "README.md"]); await execFileAsync("git", ["-C", seed, "commit", "-qm", "seed"]); await execFileAsync("git", ["init", "-q", "--bare", bare]); await execFileAsync("git", ["-C", seed, "remote", "add", "origin", bare]); await execFileAsync("git", ["-C", seed, "push", "-q", "origin", "main"]); - await execFileAsync("git", ["--git-dir", bare, "worktree", "add", "-q", worktree, "main"]); + await execFileAsync("git", [ + "--git-dir", + bare, + "worktree", + "add", + "-q", + worktree, + "main", + ]); await excludeRuntimeSkillsFromGit(worktree, "codex app-server"); await writeSkill(join(worktree, ".codex", "skills"), "injected", "skill"); @@ -146,8 +169,20 @@ describe("layered runtime skills", () => { const project = join(root, "project"); const repository = join(root, "repository"); await execFileAsync("git", ["init", "-q", repository]); - await execFileAsync("git", ["-C", repository, "config", "user.email", "test@example.com"]); - await execFileAsync("git", ["-C", repository, "config", "user.name", "Test User"]); + await execFileAsync("git", [ + "-C", + repository, + "config", + "user.email", + "test@example.com", + ]); + await execFileAsync("git", [ + "-C", + repository, + "config", + "user.name", + "Test User", + ]); await writeSkill( join(repository, ".codex", "skills"), "committed", diff --git a/packages/orchestrator/src/skills.ts b/packages/orchestrator/src/skills.ts index 82d396622..d0072857a 100644 --- a/packages/orchestrator/src/skills.ts +++ b/packages/orchestrator/src/skills.ts @@ -153,10 +153,10 @@ export async function excludeRuntimeSkillsFromGit( if (!skillsDirectory) { return; } - const relativePath = `${relative(repositoryDirectory, skillsDirectory).replaceAll( - "\\", - "/" - )}/`; + const relativePath = `${relative( + repositoryDirectory, + skillsDirectory + ).replaceAll("\\", "/")}/`; const { stdout } = await execFileAsync( "git", ["-C", repositoryDirectory, "rev-parse", "--git-path", "info/exclude"], diff --git a/packages/runtime-claude/src/session-store.test.ts b/packages/runtime-claude/src/session-store.test.ts index 2a74ca39e..b6b1d410b 100644 --- a/packages/runtime-claude/src/session-store.test.ts +++ b/packages/runtime-claude/src/session-store.test.ts @@ -18,10 +18,14 @@ async function createTempDir(): Promise { describe("ClaudeSessionStore", () => { afterEach(async () => { - await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { - recursive: true, - force: true, - }))); + await Promise.all( + tempDirs.splice(0).map((dir) => + rm(dir, { + recursive: true, + force: true, + }) + ) + ); }); it("saves claude-print session files with protocol discriminator", async () => { diff --git a/packages/runtime-claude/src/session-store.ts b/packages/runtime-claude/src/session-store.ts index dc2235c2b..954eb81f3 100644 --- a/packages/runtime-claude/src/session-store.ts +++ b/packages/runtime-claude/src/session-store.ts @@ -72,7 +72,11 @@ export class ClaudeSessionStore { const path = this.sessionFilePath(options); await mkdir(dirname(path), { recursive: true }); - await writeFile(`${path}.tmp`, `${JSON.stringify(session, null, 2)}\n`, "utf8"); + await writeFile( + `${path}.tmp`, + `${JSON.stringify(session, null, 2)}\n`, + "utf8" + ); await rename(`${path}.tmp`, path); return session; } @@ -92,10 +96,14 @@ export function parseClaudeSessionFile(value: unknown): ClaudeSessionFile { ); } if (typeof value.sessionId !== "string" || value.sessionId.length === 0) { - throw new Error("Claude session file sessionId must be a non-empty string."); + throw new Error( + "Claude session file sessionId must be a non-empty string." + ); } if (typeof value.createdAt !== "string" || value.createdAt.length === 0) { - throw new Error("Claude session file createdAt must be a non-empty string."); + throw new Error( + "Claude session file createdAt must be a non-empty string." + ); } if ( "parentRunId" in value && @@ -116,7 +124,8 @@ export function parseClaudeSessionFile(value: unknown): ClaudeSessionFile { protocol: CLAUDE_SESSION_PROTOCOL, sessionId: value.sessionId, createdAt: value.createdAt, - parentRunId: typeof value.parentRunId === "string" ? value.parentRunId : undefined, + parentRunId: + typeof value.parentRunId === "string" ? value.parentRunId : undefined, protocolState: isRecord(value.protocolState) ? value.protocolState : {}, }; } diff --git a/packages/runtime-codex/src/convergence-detection.test.ts b/packages/runtime-codex/src/convergence-detection.test.ts index 9f514a7e1..f0e5198bd 100644 --- a/packages/runtime-codex/src/convergence-detection.test.ts +++ b/packages/runtime-codex/src/convergence-detection.test.ts @@ -37,7 +37,9 @@ describe("convergence detection helpers", () => { }); it("captures the git workspace fingerprint from file changes", async () => { - const repoRoot = await mkdtemp(join(tmpdir(), "runtime-codex-convergence-")); + const repoRoot = await mkdtemp( + join(tmpdir(), "runtime-codex-convergence-") + ); tempRoots.push(repoRoot); execSync("git init", { diff --git a/packages/runtime-codex/src/convergence-detection.ts b/packages/runtime-codex/src/convergence-detection.ts index f36b3d56d..025e56bc3 100644 --- a/packages/runtime-codex/src/convergence-detection.ts +++ b/packages/runtime-codex/src/convergence-detection.ts @@ -17,9 +17,7 @@ export type TurnProgressEvaluation = { reason: string | null; }; -export function resolveMaxNonProductiveTurns( - env: NodeJS.ProcessEnv -): number { +export function resolveMaxNonProductiveTurns(env: NodeJS.ProcessEnv): number { const rawValue = env.SYMPHONY_MAX_NONPRODUCTIVE_TURNS; const parsed = Number(rawValue); return Number.isInteger(parsed) && parsed > 0 diff --git a/packages/runtime-codex/src/thread-resume.test.ts b/packages/runtime-codex/src/thread-resume.test.ts index c60290d1b..882074cf4 100644 --- a/packages/runtime-codex/src/thread-resume.test.ts +++ b/packages/runtime-codex/src/thread-resume.test.ts @@ -22,9 +22,7 @@ describe("parseNonNegativeInteger", () => { describe("buildContinuationTurnInput", () => { it("falls back to the default continuation guidance", () => { - expect(buildContinuationTurnInput({})).toBe( - DEFAULT_CONTINUATION_GUIDANCE - ); + expect(buildContinuationTurnInput({})).toBe(DEFAULT_CONTINUATION_GUIDANCE); }); it("renders continuation template variables for resume-aware prompts", () => { diff --git a/packages/runtime-codex/src/turn-limits.test.ts b/packages/runtime-codex/src/turn-limits.test.ts index d5f1dd322..87215a80a 100644 --- a/packages/runtime-codex/src/turn-limits.test.ts +++ b/packages/runtime-codex/src/turn-limits.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - DEFAULT_SESSION_MAX_TURNS, - resolveMaxTurns, -} from "./turn-limits.js"; +import { DEFAULT_SESSION_MAX_TURNS, resolveMaxTurns } from "./turn-limits.js"; describe("resolveMaxTurns", () => { it("falls back to the default when max_turns is missing or invalid", () => { diff --git a/packages/worker/src/after-create-hook.test.ts b/packages/worker/src/after-create-hook.test.ts index 2e5ae600f..95a71c183 100644 --- a/packages/worker/src/after-create-hook.test.ts +++ b/packages/worker/src/after-create-hook.test.ts @@ -149,9 +149,7 @@ describe("prepareAfterCreateHook", () => { }); it("reclones when the target repository changes", async () => { - const root = mkdtempSync( - join(tmpdir(), "github-symphony-hook-reclone-") - ); + const root = mkdtempSync(join(tmpdir(), "github-symphony-hook-reclone-")); const firstRepository = join(root, "first"); const secondRepository = join(root, "second"); const hooksRoot = join(root, "hooks"); @@ -190,7 +188,13 @@ describe("prepareAfterCreateHook", () => { cwd: secondRepository, }); await execFileAsync("git", ["-C", secondRepository, "add", "SECOND.md"]); - await execFileAsync("git", ["-C", secondRepository, "commit", "-m", "init"]); + await execFileAsync("git", [ + "-C", + secondRepository, + "commit", + "-m", + "init", + ]); const firstHook = await prepareAfterCreateHook(hooksRoot, { workspaceId: "workspace-1", @@ -227,9 +231,7 @@ describe("prepareAfterCreateHook", () => { }); it("recognizes git worktree checkouts on rerun", async () => { - const root = mkdtempSync( - join(tmpdir(), "github-symphony-hook-worktree-") - ); + const root = mkdtempSync(join(tmpdir(), "github-symphony-hook-worktree-")); const sourceRepository = join(root, "source"); const hooksRoot = join(root, "hooks"); const workspaceRoot = join(root, "workspaces"); diff --git a/packages/worker/src/approval-workflow.integration.test.ts b/packages/worker/src/approval-workflow.integration.test.ts index c8ea8b51e..7a1c7d72c 100644 --- a/packages/worker/src/approval-workflow.integration.test.ts +++ b/packages/worker/src/approval-workflow.integration.test.ts @@ -5,7 +5,7 @@ import { type ApprovalWorkflowClient, type ApprovalWorkflowComment, type ApprovalWorkflowIssue, - type ApprovalWorkflowPullRequest + type ApprovalWorkflowPullRequest, } from "./approval-workflow.js"; import { DEFAULT_WORKFLOW_LIFECYCLE } from "./workflow-lifecycle.js"; @@ -13,7 +13,7 @@ describe("approval workflow integration", () => { it("hands planning off for human review without duplicating comments on retry", async () => { const client = createMemoryApprovalClient(); const issue = createIssue({ - state: "Todo" + state: "Todo", }); const firstResult = await executePlanningPhase( @@ -23,8 +23,11 @@ describe("approval workflow integration", () => { transitionTo: "Plan Review", report: { summary: "Investigate the issue and stage the implementation.", - steps: ["Inspect the worker workflow", "Document the implementation plan"] - } + steps: [ + "Inspect the worker workflow", + "Document the implementation plan", + ], + }, }, client ); @@ -35,8 +38,11 @@ describe("approval workflow integration", () => { transitionTo: "Plan Review", report: { summary: "Investigate the issue and stage the implementation.", - steps: ["Inspect the worker workflow", "Document the implementation plan"] - } + steps: [ + "Inspect the worker workflow", + "Document the implementation plan", + ], + }, }, client ); @@ -50,7 +56,7 @@ describe("approval workflow integration", () => { it("resumes after approval, upserts a pull request, and transitions to awaiting merge", async () => { const client = createMemoryApprovalClient(); const issue = createIssue({ - state: "In Progress" + state: "In Progress", }); const firstResult = await executeImplementationPhase( @@ -60,8 +66,8 @@ describe("approval workflow integration", () => { transitionTo: "In Review", report: { summary: "Implemented the approval-aware worker loop.", - validation: ["pnpm test --filter @gh-symphony/worker"] - } + validation: ["pnpm test --filter @gh-symphony/worker"], + }, }, client ); @@ -72,8 +78,8 @@ describe("approval workflow integration", () => { transitionTo: "In Review", report: { summary: "Implemented the approval-aware worker loop.", - validation: ["pnpm test --filter @gh-symphony/worker"] - } + validation: ["pnpm test --filter @gh-symphony/worker"], + }, }, client ); @@ -86,7 +92,9 @@ describe("approval workflow integration", () => { }); }); -function createIssue(overrides: Partial): ApprovalWorkflowIssue { +function createIssue( + overrides: Partial +): ApprovalWorkflowIssue { return { id: "issue-1", number: 42, @@ -99,9 +107,9 @@ function createIssue(overrides: Partial): ApprovalWorkflo repository: { owner: "acme", name: "platform", - defaultBranch: "main" + defaultBranch: "main", }, - ...overrides + ...overrides, }; } @@ -124,7 +132,7 @@ function createMemoryApprovalClient(): ApprovalWorkflowClient & { async createIssueComment(_issueId, body) { const comment = { id: `comment-${comments.length + 1}`, - body + body, }; comments.push(comment); return comment; @@ -133,7 +141,7 @@ function createMemoryApprovalClient(): ApprovalWorkflowClient & { const index = comments.findIndex((comment) => comment.id === commentId); comments[index] = { ...comments[index], - body + body, }; return comments[index]!; }, @@ -141,7 +149,11 @@ function createMemoryApprovalClient(): ApprovalWorkflowClient & { projectStateUpdates.push(input.state); }, async findPullRequestByBranch(input) { - return pullRequests.find((pullRequest) => pullRequest.headBranch === input.branchName) ?? null; + return ( + pullRequests.find( + (pullRequest) => pullRequest.headBranch === input.branchName + ) ?? null + ); }, async createPullRequest(input) { const pullRequest = { @@ -150,19 +162,21 @@ function createMemoryApprovalClient(): ApprovalWorkflowClient & { url: `https://github.com/${input.owner}/${input.repository}/pull/${pullRequests.length + 1}`, headBranch: input.headBranch, title: input.title, - body: input.body + body: input.body, }; pullRequests.push(pullRequest); return pullRequest; }, async updatePullRequest(input) { - const index = pullRequests.findIndex((pullRequest) => pullRequest.id === input.pullRequestId); + const index = pullRequests.findIndex( + (pullRequest) => pullRequest.id === input.pullRequestId + ); pullRequests[index] = { ...pullRequests[index], title: input.title, - body: input.body + body: input.body, }; return pullRequests[index]!; - } + }, }; } diff --git a/packages/worker/src/approval-workflow.test.ts b/packages/worker/src/approval-workflow.test.ts index 0c9f53e1f..0f905ca7d 100644 --- a/packages/worker/src/approval-workflow.test.ts +++ b/packages/worker/src/approval-workflow.test.ts @@ -5,7 +5,7 @@ import { buildPullRequestBody, executeStateGuard, hasMergedCompletionSignal, - isIssueStillActionable + isIssueStillActionable, } from "./approval-workflow.js"; import { DEFAULT_WORKFLOW_LIFECYCLE } from "./workflow-lifecycle.js"; @@ -14,7 +14,7 @@ describe("buildImplementationBranchName", () => { expect( buildImplementationBranchName({ number: 42, - title: "Ship the approval-gated PR workflow" + title: "Ship the approval-gated PR workflow", }) ).toBe("symphony/issue-42-ship-the-approval-gated-pr-workflow"); }); @@ -30,7 +30,9 @@ describe("buildPhaseMarker", () => { describe("buildPullRequestBody", () => { it("links the PR merge back to the issue completion path", () => { - expect(buildPullRequestBody(99, "Implement the worker lifecycle")).toContain("Fixes #99"); + expect( + buildPullRequestBody(99, "Implement the worker lifecycle") + ).toContain("Fixes #99"); }); }); @@ -41,13 +43,10 @@ describe("state safeguards", () => { ).toThrow("Issue is no longer actionable"); expect( - isIssueStillActionable( - "In Progress", - DEFAULT_WORKFLOW_LIFECYCLE - ) - ).toBe(true); - expect( - hasMergedCompletionSignal("Done", DEFAULT_WORKFLOW_LIFECYCLE) + isIssueStillActionable("In Progress", DEFAULT_WORKFLOW_LIFECYCLE) ).toBe(true); + expect(hasMergedCompletionSignal("Done", DEFAULT_WORKFLOW_LIFECYCLE)).toBe( + true + ); }); }); diff --git a/packages/worker/src/github-tracker.test.ts b/packages/worker/src/github-tracker.test.ts index 864252eb3..42553dc00 100644 --- a/packages/worker/src/github-tracker.test.ts +++ b/packages/worker/src/github-tracker.test.ts @@ -6,7 +6,7 @@ import { isActionableState, isTrackedIssueActionable, normalizeGithubProjectItem, - normalizeStateName + normalizeStateName, } from "./github-tracker.js"; import { DEFAULT_WORKFLOW_LIFECYCLE } from "./workflow-lifecycle.js"; @@ -33,14 +33,14 @@ describe("normalizeGithubProjectItem", () => { { __typename: "ProjectV2ItemFieldSingleSelectValue", name: "Todo", - field: { name: "Status" } + field: { name: "Status" }, }, { __typename: "ProjectV2ItemFieldTextValue", text: "repo context", - field: { name: "Repository Context" } - } - ] + field: { name: "Repository Context" }, + }, + ], }, content: { __typename: "Issue", @@ -52,17 +52,17 @@ describe("normalizeGithubProjectItem", () => { createdAt: "2026-03-07T09:00:00.000Z", updatedAt: "2026-03-07T10:00:00.000Z", labels: { - nodes: [{ name: "Agent" }, { name: "Infra" }] + nodes: [{ name: "Agent" }, { name: "Infra" }], }, assignees: { - nodes: [] + nodes: [], }, repository: { name: "platform", url: "https://github.com/acme/platform", owner: { - login: "acme" - } + login: "acme", + }, }, blockedBy: { nodes: [ @@ -73,13 +73,13 @@ describe("normalizeGithubProjectItem", () => { repository: { name: "shared", owner: { - login: "other" - } - } - } - ] - } - } + login: "other", + }, + }, + }, + ], + }, + }, }); expect(issue).toMatchObject({ @@ -99,8 +99,8 @@ describe("normalizeGithubProjectItem", () => { { id: "issue-9", identifier: "other/shared#9", - state: "CLOSED" - } + state: "CLOSED", + }, ], createdAt: "2026-03-07T09:00:00.000Z", updatedAt: "2026-03-07T10:00:00.000Z", @@ -108,12 +108,12 @@ describe("normalizeGithubProjectItem", () => { owner: "acme", name: "platform", url: "https://github.com/acme/platform", - cloneUrl: "https://github.com/acme/platform.git" + cloneUrl: "https://github.com/acme/platform.git", }, tracker: { adapter: "github-project", bindingId: "project-123", - itemId: "item-1" + itemId: "item-1", }, nativeRef: { itemId: "item-1", @@ -121,7 +121,7 @@ describe("normalizeGithubProjectItem", () => { }, isArchived: false, metadata: {}, - rateLimits: null + rateLimits: null, }); }); }); @@ -134,7 +134,7 @@ describe("isTrackedIssueActionable", () => { state: "Plan Review", } as never, { - lifecycle: DEFAULT_WORKFLOW_LIFECYCLE + lifecycle: DEFAULT_WORKFLOW_LIFECYCLE, } ) ).toBe(false); @@ -145,7 +145,7 @@ describe("isTrackedIssueActionable", () => { state: "In Progress", } as never, { - lifecycle: DEFAULT_WORKFLOW_LIFECYCLE + lifecycle: DEFAULT_WORKFLOW_LIFECYCLE, } ) ).toBe(true); @@ -172,9 +172,9 @@ describe("fetchActionableIssues", () => { { __typename: "ProjectV2ItemFieldSingleSelectValue", name: "Todo", - field: { name: "Status" } - } - ] + field: { name: "Status" }, + }, + ], }, content: { __typename: "Issue", @@ -190,21 +190,21 @@ describe("fetchActionableIssues", () => { repository: { name: "platform", url: "https://github.com/acme/platform", - owner: { login: "acme" } + owner: { login: "acme" }, }, - blockedBy: { nodes: [] } - } - } + blockedBy: { nodes: [] }, + }, + }, ], pageInfo: { endCursor: "cursor-1", - hasNextPage: true - } - } - } - } + hasNextPage: true, + }, + }, + }, + }, }), - text: async () => "" + text: async () => "", }) .mockResolvedValueOnce({ ok: true, @@ -222,9 +222,9 @@ describe("fetchActionableIssues", () => { { __typename: "ProjectV2ItemFieldSingleSelectValue", name: "Done", - field: { name: "Status" } - } - ] + field: { name: "Status" }, + }, + ], }, content: { __typename: "Issue", @@ -240,28 +240,28 @@ describe("fetchActionableIssues", () => { repository: { name: "platform", url: "https://github.com/acme/platform", - owner: { login: "acme" } + owner: { login: "acme" }, }, - blockedBy: { nodes: [] } - } - } + blockedBy: { nodes: [] }, + }, + }, ], pageInfo: { endCursor: null, - hasNextPage: false - } - } - } - } + hasNextPage: false, + }, + }, + }, + }, }), - text: async () => "" + text: async () => "", }); const issues = await fetchActionableIssues( { projectId: "project-123", token: "secret", - lifecycle: DEFAULT_WORKFLOW_LIFECYCLE + lifecycle: DEFAULT_WORKFLOW_LIFECYCLE, }, fetchImpl as typeof fetch ); @@ -275,7 +275,7 @@ describe("fetchActionableIssues", () => { const fetchImpl = vi.fn().mockResolvedValue({ ok: false, status: 502, - text: async () => "bad gateway" + text: async () => "bad gateway", }); await expect( @@ -283,7 +283,7 @@ describe("fetchActionableIssues", () => { { projectId: "project-123", token: "secret", - activeStates: ["todo"] + activeStates: ["todo"], }, fetchImpl as typeof fetch ) @@ -294,9 +294,9 @@ describe("fetchActionableIssues", () => { const fetchImpl = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ - errors: [{ message: "Something broke" }] + errors: [{ message: "Something broke" }], }), - text: async () => "" + text: async () => "", }); await expect( @@ -304,7 +304,7 @@ describe("fetchActionableIssues", () => { { projectId: "project-123", token: "secret", - activeStates: ["todo"] + activeStates: ["todo"], }, fetchImpl as typeof fetch ) diff --git a/packages/worker/src/github-tracker.ts b/packages/worker/src/github-tracker.ts index c8c0c51a4..280748aad 100644 --- a/packages/worker/src/github-tracker.ts +++ b/packages/worker/src/github-tracker.ts @@ -7,21 +7,26 @@ export { normalizeGithubProjectItem, type GitHubRepositoryRef, type GitHubTrackedIssue, - type GitHubTrackerConfig + type GitHubTrackerConfig, } from "@gh-symphony/tracker-github"; import { isStateActive, type WorkflowLifecycleConfig } from "@gh-symphony/core"; import type { GitHubTrackedIssue, - GitHubTrackerConfig + GitHubTrackerConfig, } from "@gh-symphony/tracker-github"; export function normalizeStateName(state: string): string { return state.trim().toLowerCase(); } -export function isActionableState(state: string, activeStates: string[]): boolean { - return activeStates.map(normalizeStateName).includes(normalizeStateName(state)); +export function isActionableState( + state: string, + activeStates: string[] +): boolean { + return activeStates + .map(normalizeStateName) + .includes(normalizeStateName(state)); } export function isTrackedIssueActionable( @@ -31,7 +36,10 @@ export function isTrackedIssueActionable( } ): boolean { if (config.lifecycle) { - return isStateActive(issue.state, config.lifecycle as WorkflowLifecycleConfig); + return isStateActive( + issue.state, + config.lifecycle as WorkflowLifecycleConfig + ); } return isActionableState(issue.state, config.activeStates ?? []); diff --git a/packages/worker/src/host-dynamic-tool-call.ts b/packages/worker/src/host-dynamic-tool-call.ts index 4fa859972..d0f75ab1f 100644 --- a/packages/worker/src/host-dynamic-tool-call.ts +++ b/packages/worker/src/host-dynamic-tool-call.ts @@ -17,10 +17,7 @@ export async function executeRateLimitedCodexDynamicToolCall(options: { } } -function failure( - code: string, - message: string -): CodexDynamicToolCallResponse { +function failure(code: string, message: string): CodexDynamicToolCallResponse { return { success: false, contentItems: [ diff --git a/packages/worker/src/retry-policy.test.ts b/packages/worker/src/retry-policy.test.ts index 4486dfb7a..8232e033c 100644 --- a/packages/worker/src/retry-policy.test.ts +++ b/packages/worker/src/retry-policy.test.ts @@ -3,13 +3,21 @@ import { calculateRetryDelay, scheduleRetryAt } from "./retry-policy.js"; describe("calculateRetryDelay", () => { it("uses exponential backoff", () => { - expect(calculateRetryDelay(1, { baseDelayMs: 1000, maxDelayMs: 30000 })).toBe(1000); - expect(calculateRetryDelay(2, { baseDelayMs: 1000, maxDelayMs: 30000 })).toBe(2000); - expect(calculateRetryDelay(3, { baseDelayMs: 1000, maxDelayMs: 30000 })).toBe(4000); + expect( + calculateRetryDelay(1, { baseDelayMs: 1000, maxDelayMs: 30000 }) + ).toBe(1000); + expect( + calculateRetryDelay(2, { baseDelayMs: 1000, maxDelayMs: 30000 }) + ).toBe(2000); + expect( + calculateRetryDelay(3, { baseDelayMs: 1000, maxDelayMs: 30000 }) + ).toBe(4000); }); it("caps the retry delay at the configured maximum", () => { - expect(calculateRetryDelay(8, { baseDelayMs: 1000, maxDelayMs: 5000 })).toBe(5000); + expect( + calculateRetryDelay(8, { baseDelayMs: 1000, maxDelayMs: 5000 }) + ).toBe(5000); }); }); @@ -17,8 +25,11 @@ describe("scheduleRetryAt", () => { it("computes the next retry timestamp from the current attempt", () => { const now = new Date("2026-03-07T09:00:00.000Z"); - expect(scheduleRetryAt(now, 3, { baseDelayMs: 1000, maxDelayMs: 30000 }).toISOString()).toBe( - "2026-03-07T09:00:04.000Z" - ); + expect( + scheduleRetryAt(now, 3, { + baseDelayMs: 1000, + maxDelayMs: 30000, + }).toISOString() + ).toBe("2026-03-07T09:00:04.000Z"); }); }); diff --git a/packages/worker/src/retry-policy.ts b/packages/worker/src/retry-policy.ts index 8c11368d3..d5fa3cc8b 100644 --- a/packages/worker/src/retry-policy.ts +++ b/packages/worker/src/retry-policy.ts @@ -1,5 +1,5 @@ export { calculateRetryDelay, scheduleRetryAt, - type RetryPolicyOptions + type RetryPolicyOptions, } from "@gh-symphony/core"; diff --git a/packages/worker/src/thread-resume.test.ts b/packages/worker/src/thread-resume.test.ts index c60290d1b..882074cf4 100644 --- a/packages/worker/src/thread-resume.test.ts +++ b/packages/worker/src/thread-resume.test.ts @@ -22,9 +22,7 @@ describe("parseNonNegativeInteger", () => { describe("buildContinuationTurnInput", () => { it("falls back to the default continuation guidance", () => { - expect(buildContinuationTurnInput({})).toBe( - DEFAULT_CONTINUATION_GUIDANCE - ); + expect(buildContinuationTurnInput({})).toBe(DEFAULT_CONTINUATION_GUIDANCE); }); it("renders continuation template variables for resume-aware prompts", () => { diff --git a/packages/worker/src/turn-lease.test.ts b/packages/worker/src/turn-lease.test.ts index 23cc8d9e0..4c624eed7 100644 --- a/packages/worker/src/turn-lease.test.ts +++ b/packages/worker/src/turn-lease.test.ts @@ -139,18 +139,14 @@ describe("tracker refresh fail-closed threshold", () => { }); it("uses the convergence action for a confirmed active tracker read", () => { - expect( - resolveTrackerRefreshGate("active", 1, 3, "convergence") - ).toEqual({ + expect(resolveTrackerRefreshGate("active", 1, 3, "convergence")).toEqual({ action: "converge", count: 0, }); }); it("defers convergence when a transient tracker read is below the threshold", () => { - expect( - resolveTrackerRefreshGate("unknown", 1, 3, "convergence") - ).toEqual({ + expect(resolveTrackerRefreshGate("unknown", 1, 3, "convergence")).toEqual({ action: "defer", count: 2, }); diff --git a/packages/worker/src/turn-limits.test.ts b/packages/worker/src/turn-limits.test.ts index d5f1dd322..87215a80a 100644 --- a/packages/worker/src/turn-limits.test.ts +++ b/packages/worker/src/turn-limits.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - DEFAULT_SESSION_MAX_TURNS, - resolveMaxTurns, -} from "./turn-limits.js"; +import { DEFAULT_SESSION_MAX_TURNS, resolveMaxTurns } from "./turn-limits.js"; describe("resolveMaxTurns", () => { it("falls back to the default when max_turns is missing or invalid", () => { diff --git a/packages/worker/src/workflow-lifecycle.ts b/packages/worker/src/workflow-lifecycle.ts index bb2340f0a..2987ac019 100644 --- a/packages/worker/src/workflow-lifecycle.ts +++ b/packages/worker/src/workflow-lifecycle.ts @@ -4,5 +4,5 @@ export { isStateTerminal, matchesWorkflowState, normalizeWorkflowState, - type WorkflowLifecycleConfig + type WorkflowLifecycleConfig, } from "@gh-symphony/core";