Skip to content

workflow: consolidate issue/legacy dispatchers into issue.dispatch and drop the issue.py shim #155

Description

@studykit

Description

The issue/legacy/ package held seven near-identical provider-agnostic dispatcher modules (issue_new, issue_update, issue_fetch, issue_comments, issue_link, issue_fields, issue_attach), each ~85 lines of duplicated boilerplate: load .workflow/config.yml → select the backend → build a shared parser → call add_<intent>_args / run_<intent>. The unified issue.py then re-exported each module's main() per verb. The result was two indirection layers with heavy duplication, and the "legacy" name no longer matched reality — those modules were the active implementation, not a deprecated path.

This collapses all provider-agnostic dispatch into a single run_intent(spec, argv) helper plus a declarative IntentSpec table in issue/dispatch.py, deletes issue/legacy/, and removes the now-redundant top-level issue.py shim in favor of a package entry point issue/__main__.py. Per-provider behavior is unchanged and continues to live in issue/github/ and issue/jira/. The removals are mechanical de-duplication: no option surface, JSON payload shape, or backend selection logic changes.

Approach

  • issue/dispatch.py (new): IntentSpec (error label + add_<intent>_args / run_<intent> method names + require_jira / state_verbs flags) captures the only per-intent deviations — the Jira-only attach guard and the Jira state-verb discovery branch for fields. One run_intent runs the shared boilerplate; the verb router main() translates verbs to specs.
  • issue/__main__.py (new): carries the PEP 723 deps and bootstraps sys.path. Under uv run --script the script's own directory lands on sys.path[0], so it prepends the parent (scripts/) for the absolute issue.* imports to resolve, then delegates to issue.dispatch.main. Verified empirically that neither uv run -m issue (ignores PEP 723 deps) nor uv run --script issue/__main__.py without the bootstrap (breaks absolute imports) works alone.
  • scripts/workflow launcher: bare-name resolution now falls back to <command>/__main__.py, so workflow issue runs the package entry without hardcoding deps in the launcher.
  • User-facing strings and the argparse prog rename issue.pyissue; authoring docs, agents, and skills updated to match.

Affected Paths

  • plugins/workflow/scripts/issue/dispatch.py (new)
  • plugins/workflow/scripts/issue/__main__.py (new)
  • plugins/workflow/scripts/issue.py (deleted)
  • plugins/workflow/scripts/issue/legacy/ (deleted)
  • plugins/workflow/scripts/workflow (launcher bare-name fallback)
  • plugins/workflow/scripts/issue/backend.py, issue/github/backend.py, issue/jira/backend.py (docstring references)
  • plugins/workflow/tests/test_issue_dispatcher.py, test_workflow_env.py, test_workflow_cache_*.py, test_workflow_issue_fields_{github,jira}.py, test_workflow_writeback_json_shape.py
  • docs — authoring/providers/issue/{github,jira}/convention.md, skills/{handoff,setup}/SKILL.md, agents/task-size-auditor.md

Unit Test Strategy

Behavior-preserving refactor covered by the existing suite. Tests that imported issue.legacy.issue_X.main now bind functools.partial(run_intent, SPEC) from issue.dispatch, with call sites unchanged, so --help backend gating, cross-backend flag rejection, the attach Jira-only guard, and the writeback JSON-shape contracts keep asserting the same behavior. The launcher subprocess test (test_workflow_env.py) asserts the new usage: issue <verb> header. The full plugins/workflow/tests suite (581 tests) passes.

Acceptance Criteria

  • issue/legacy/ removed; provider-agnostic dispatch lives once in issue/dispatch.py (run_intent + IntentSpec table).
  • Top-level issue.py removed; workflow issue <verb> runs via issue/__main__.py.
  • GitHub and Jira backend behavior (option surface, JSON payloads, attach Jira-only guard, Jira state-verb discovery) unchanged.
  • No residual issue.py references remain in code or docs.
  • plugins/workflow/tests passes (581 tests).

Resume

All six increments of the cross-reviewed (Plan agent) issue/ module-layering plan are complete on refactor/issue-dispatch; full suite green (581 tests). The plan relocated the issue-domain modules that were flat under scripts/ into the issue/ package, in safe ordered increments:

  1. Removed the dead workflow_github CLI entry point.
  2. Broke the workflow_session_stateissue.github import cycle.
  3. Added foundation issue/keys.py (normalize_issue_number + IssueKeyError); moved workflow_refsissue/refs.py; pointed workflow_session_state at issue.keys.
  4. Moved the workflow_github gh-call layer → issue/github/gh.py.
  5. Moved workflow_jira_data_center_clientissue/jira/client.py.
  6. Moved workflow_cacheissue/cache.py and workflow_providersissue/providers.py.

Relocated-module tests were renamed to match: test_issue_refs.py, test_issue_github_gh.py, test_issue_cache.py, test_issue_providers.py.

Open questions (resolved)

  • normalize_issue_number moved to foundation issue/keys.py and now raises IssueKeyError(ValueError) instead of GitHubParseError. Safe: GitHubParseError is never caught anywhere, and IssueKeyError is still a ValueError, so existing except ValueError sites are unaffected.
  • normalize_jira_issue_key stays bound to JiraProviderError in issue/jira/refs.py. Adopted option (A): workflow_session_state keeps its cycle-safe leaf import of issue.jira.refs (that module imports only stdlib, so there is no cycle). workflow_session_state now depends solely on foundation leaves — issue.cache, issue.keys, issue.jira.refs.

Outcome

All issue-domain modules now live under the issue/ package. The remaining flat scripts/*.py modules are cross-cutting infrastructure (workflow_command, workflow_config, workflow_env, workflow_hook, workflow_main_context, workflow_session_state, workflow_setup), correctly kept flat.

Metadata

Metadata

Assignees

Labels

taskWorkflow implementation task

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions