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.py → issue; 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
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:
- Removed the dead
workflow_github CLI entry point.
- Broke the
workflow_session_state → issue.github import cycle.
- Added foundation
issue/keys.py (normalize_issue_number + IssueKeyError); moved workflow_refs → issue/refs.py; pointed workflow_session_state at issue.keys.
- Moved the
workflow_github gh-call layer → issue/github/gh.py.
- Moved
workflow_jira_data_center_client → issue/jira/client.py.
- Moved
workflow_cache → issue/cache.py and workflow_providers → issue/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.
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 → calladd_<intent>_args/run_<intent>. The unifiedissue.pythen re-exported each module'smain()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 declarativeIntentSpectable inissue/dispatch.py, deletesissue/legacy/, and removes the now-redundant top-levelissue.pyshim in favor of a package entry pointissue/__main__.py. Per-provider behavior is unchanged and continues to live inissue/github/andissue/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_verbsflags) captures the only per-intent deviations — the Jira-onlyattachguard and the Jira state-verb discovery branch forfields. Onerun_intentruns the shared boilerplate; the verb routermain()translates verbs to specs.issue/__main__.py(new): carries the PEP 723 deps and bootstrapssys.path. Underuv run --scriptthe script's own directory lands onsys.path[0], so it prepends the parent (scripts/) for the absoluteissue.*imports to resolve, then delegates toissue.dispatch.main. Verified empirically that neitheruv run -m issue(ignores PEP 723 deps) noruv run --script issue/__main__.pywithout the bootstrap (breaks absolute imports) works alone.scripts/workflowlauncher: bare-name resolution now falls back to<command>/__main__.py, soworkflow issueruns the package entry without hardcoding deps in the launcher.progrenameissue.py→issue; 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.pyauthoring/providers/issue/{github,jira}/convention.md,skills/{handoff,setup}/SKILL.md,agents/task-size-auditor.mdUnit Test Strategy
Behavior-preserving refactor covered by the existing suite. Tests that imported
issue.legacy.issue_X.mainnow bindfunctools.partial(run_intent, SPEC)fromissue.dispatch, with call sites unchanged, so--helpbackend 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 newusage: issue <verb>header. The fullplugins/workflow/testssuite (581 tests) passes.Acceptance Criteria
issue/legacy/removed; provider-agnostic dispatch lives once inissue/dispatch.py(run_intent+IntentSpectable).issue.pyremoved;workflow issue <verb>runs viaissue/__main__.py.issue.pyreferences remain in code or docs.plugins/workflow/testspasses (581 tests).Resume
All six increments of the cross-reviewed (Plan agent)
issue/module-layering plan are complete onrefactor/issue-dispatch; full suite green (581 tests). The plan relocated the issue-domain modules that were flat underscripts/into theissue/package, in safe ordered increments:workflow_githubCLI entry point.workflow_session_state→issue.githubimport cycle.issue/keys.py(normalize_issue_number+IssueKeyError); movedworkflow_refs→issue/refs.py; pointedworkflow_session_stateatissue.keys.workflow_githubgh-call layer →issue/github/gh.py.workflow_jira_data_center_client→issue/jira/client.py.workflow_cache→issue/cache.pyandworkflow_providers→issue/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_numbermoved to foundationissue/keys.pyand now raisesIssueKeyError(ValueError)instead ofGitHubParseError. Safe:GitHubParseErroris never caught anywhere, andIssueKeyErroris still aValueError, so existingexcept ValueErrorsites are unaffected.normalize_jira_issue_keystays bound toJiraProviderErrorinissue/jira/refs.py. Adopted option (A):workflow_session_statekeeps its cycle-safe leaf import ofissue.jira.refs(that module imports only stdlib, so there is no cycle).workflow_session_statenow 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 flatscripts/*.pymodules are cross-cutting infrastructure (workflow_command,workflow_config,workflow_env,workflow_hook,workflow_main_context,workflow_session_state,workflow_setup), correctly kept flat.