Skip to main content

Plan-as-code planning layer over Jira

Project description


id: TASKSHIP-DOC title: TaskShip — plan-as-code planning layer over Jira owner: "@selvakumar" priority: high version: 1

TaskShip

TaskShip is the system of intent that sits above Jira, the system of record. A user describes a product; TaskShip decomposes it into a structured Epic → Story → Task tree, emits it as a version-controlled plan.yaml that can be reviewed in a pull request, then reconciles it into Jira Cloud idempotently — re-running never duplicates, it diffs and patches only what changed. The same core engine is driven two ways: a CLI for humans and an MCP server for agents.

The two load-bearing properties are plan-as-code (planning is reviewable, diffable, regenerable) and idempotent sync (safe to run repeatedly, so an agent can plan → push → replan without corrupting the board).

Scope of this document is the v0 MVP contract. v0 decisions of record:

  • Implementation language & core libraries: Python 3.11+. pydantic v2 (plan schema + validation), ruamel.yaml (comment-preserving YAML round-trip), httpx (Jira REST client), click (CLI), the mcp Python SDK (server), pytest (tests). Packaged as an importable taskship package (taskship/model.py, taskship/plan_io.py, taskship/reconcile.py, taskship/jira.py, taskship/cli.py, taskship/mcp_server.py; templates/ for typed templates).
  • Jira project type: company-managed.
  • Task typing in Jira: encoded as labels (taskship:type:<type>, taskship:subtype:<subtype>) — no custom-field or issue-type admin setup required.
  • External-id watermark: a taskship:<local-id> label on every issue.
  • Auth: Jira Cloud API token + account email (HTTP Basic) for a single tool account. OAuth 3LO is out of scope for v0.
  • Conflict policy: on divergence between plan and board, TaskShip surfaces the conflict for a human — it MUST NOT silently overwrite hand edits.
  • Deletions: never automatic — a removed node is flagged, not deleted.
  • Sync state is version-controlled: .taskship/state.json (id→key mapping
    • per-field hashes) is committed alongside plan.yaml, not gitignored. The conflict policy (REQ-TS-011) depends on the prior hashes: a fresh checkout without them would silently re-assert the plan over hand edits instead of surfacing conflicts. Watermark recovery (REQ-TS-006) remains the backstop for genuinely lost state; storing hashes as Jira issue entity properties is noted as future work.
  • Task linkage (verified against Jira Cloud 2026-07): Jira's parent must point exactly one hierarchy level up, and Story/Task are both level 0 — so a Task cannot parent to a Story. Tasks therefore parent to their Epic (keeping them first-class: sprintable, pointable, board cards), and the story⇄task containment is encoded in the taskship:<epic>/<story>/<task> watermark plus a visible taskship:story:<epic>/<story> label so it stays filterable in Jira. Sub-task-under-Story remains a possible future task_linkage: subtask mode.

Domain model (mirrors Jira's issue-type hierarchy 1:1): a product contains Epics (Jira level 1); each Epic contains Stories (level 0), including DevOps stories flagged kind: devops; each Story contains Tasks (level 0) typed biz-spec / tech-spec / devops / qa / docs, tech-spec optionally subtyped perf / optimization / security / scalability. Parent linkage uses Jira's standard parent field, never the deprecated Epic Link field.

The plan MUST round-trip through a validated YAML file

The plan is a single version-controlled plan.yaml describing product → epics → stories → tasks. TaskShip MUST parse it into a validated in-memory model and serialize it back without losing authored content, so the file is safe to review and diff in version control. An invalid plan MUST be rejected with a structured error, never silently repaired.

implementations:

Acceptance

  • Loading a well-formed plan.yaml and re-serializing it produces a file whose authored fields and comments are preserved (round-trip is lossless for author-visible content).
  • A plan whose node is missing a required field (e.g. an epic with no title, or a product/jira_project absent at the top level) is rejected with an error naming the offending node path; no partial write occurs.
  • A tech-spec task with subtype: perf that omits metrics (baseline + target) is rejected at validation time with an error naming the task.

Every node MUST have a stable local identity independent of its Jira key

Each epic, story, and task MUST carry a local id — supplied by the author or deterministically slugged from its title — that never changes across regenerations. This local id, not the Jira key, is the identity TaskShip tracks. Renaming a node's title MUST NOT change its identity or cause a duplicate on the next sync.

implementations:

Acceptance

  • Two nodes with the same title under different parents resolve to distinct, fully-qualified local ids (e.g. guest-flow/biz-spec-1), so titles need not be globally unique.
  • Changing a node's title while keeping its id leaves its identity unchanged: a subsequent sync updates the existing Jira issue rather than creating a new one.
  • A node authored without an explicit id receives a deterministic slug that is identical on every load of the same file (stable, not random).

Fields MUST cascade from defaults to epic to story to task

Fields defined at a broader scope MUST be inherited by narrower scopes, and a narrower scope MUST be able to override them. The cascade order is defaults → epic → story → task. This keeps the plan concise while letting any node opt out.

implementations:

Acceptance

  • A task with no labels of its own resolves to its story's labels, which in turn fall back to defaults.labels when the story defines none.
  • A task that declares its own labels uses exactly those and does not merge in the inherited set (override, not union), unless the author opts into merge explicitly.
  • The resolved (post-cascade) field set for any node is inspectable, so a reviewer can see what a task will actually carry into Jira.

Task-type templates MUST render structured ADF and MUST refuse incomplete specs

Each task type has a versioned template that renders its required fields into the Jira issue description as Atlassian Document Format (ADF) and sets its labels. Templates MUST enforce completeness: a template whose required fields are unmet MUST refuse to render rather than emit a blank or partial ticket. Templates MUST be versioned files a team can fork without editing TaskShip's core.

implementations:

Acceptance

  • The biz-spec template renders an ADF description containing the problem statement, user story, acceptance criteria, out-of-scope, and open questions sections.
  • The tech-spec template with subtype: perf refuses to render when no measurable baseline+target metric is present, and renders the metric (e.g. p95 480ms → 200ms) when present.
  • The devops template renders infra changes, pipeline stages, rollback plan, and a runbook link section.
  • Rendered output is valid ADF accepted by the Jira create/update description field (round-trips through the REST API without a 400).
  • A team can point TaskShip at a forked template directory and its templates render in place of the built-ins, with no change to TaskShip's source.

Sync MUST be idempotent — create, update, or skip per node

For each node, in an order where every parent is processed before its children, TaskShip MUST decide exactly one of: create (no existing Jira issue), update (issue exists but the node's content hash changed — patching only changed fields), or skip (hash unchanged — no API call). Re-running an unchanged plan MUST produce zero create/update calls. Parent linkage MUST use Jira's parent field.

implementations:

Acceptance

  • Syncing a plan for the first time creates one Jira issue per node and records each node's Jira key and content hash in .taskship/state.json.
  • Immediately re-syncing the same unchanged plan issues zero create and zero update calls (every node resolves to skip).
  • Editing exactly one task's title and re-syncing issues exactly one update call, targeting only the changed fields of that one issue; all other nodes skip.
  • A story is created with its epic set as parent, and a task with its epic as parent (Jira rejects task→story parenting; the task carries a taskship:story:<id> label for containment); a parent is always created before its children.

Sync MUST recover the id→key mapping when local state is missing

When .taskship/state.json is absent or does not contain a node (fresh checkout, lost state), TaskShip MUST attempt to recover the mapping from Jira before deciding to create, by searching for the node's taskship:<local-id> watermark label. Only if no watermark match exists may it create a new issue. This prevents duplicate issues after state loss.

implementations:

Acceptance

  • With state.json deleted but issues still bearing taskship:<id> labels, a sync recovers each node's Jira key via label search and issues zero creates.
  • A node whose watermark label matches no Jira issue is created (fresh), and its new key + hash are written back to state.json.
  • Every issue TaskShip creates carries the taskship:<local-id> watermark label so future recovery is possible.

Sync MUST support a dry-run that performs no writes

A --dry-run sync MUST compute and report the full create/update/skip plan without issuing any mutating Jira call. This lets a human or agent preview the effect of a sync before committing to it.

implementations:

Acceptance

  • sync --dry-run prints, per node, one of create / update / skip and the reason, and makes zero POST/PUT calls to Jira.
  • A dry-run leaves .taskship/state.json byte-for-byte unchanged.

Removing a node MUST NOT delete its Jira issue automatically

When a node present in state.json no longer appears in plan.yaml, TaskShip MUST NOT delete or close the Jira issue. It MUST instead flag the issue with a taskship:orphaned label and report it, leaving resolution to a human.

implementations:

Acceptance

  • Removing a node from plan.yaml and syncing applies the taskship:orphaned label to its Jira issue and issues no delete/transition-to-done call.
  • The orphaned node is reported in the sync summary so the human can act on it.

Jira REST calls MUST be rate-limit-aware and retried with backoff

TaskShip MUST tolerate Jira Cloud rate limiting: on a throttled or transient failure response it MUST retry with backoff up to a bounded number of attempts rather than aborting the whole sync, and MUST surface a clear error if the ceiling is exceeded.

implementations:

Acceptance

  • A 429 response with a Retry-After header causes the client to wait and retry, and the sync ultimately succeeds without operator intervention.
  • Retries are bounded; after the attempt ceiling is hit the call fails with an error identifying the node and the underlying Jira status, and the sync stops cleanly.

Reverse sync MUST pull live board state without overwriting intent

TaskShip MUST be able to read current issue state back from Jira — status, assignee, story points — and present a plan-vs-reality view. Reverse sync MUST be read-only with respect to plan.yaml: it annotates, it MUST NOT rewrite authored intent.

implementations:

Acceptance

  • status fetches each mapped issue's current status, assignee, and story points and displays them alongside the planned node.
  • Running reverse sync leaves plan.yaml's authored fields unchanged on disk.

Divergence between plan and board MUST be surfaced, not silently overwritten

When a field TaskShip manages has been changed by hand in Jira such that the board value no longer matches the plan-derived value, sync MUST detect and report the divergence for the human rather than blindly overwriting it. (v0 conflict policy: surface for human.)

implementations:

Acceptance

  • A hand edit in Jira to a TaskShip-managed field, followed by a sync where the plan also changed that node, is reported as a conflict listing the field, the plan value, and the board value.
  • The conflicting field is not overwritten by that sync; the human decides the resolution.

A CLI MUST expose init, review, sync, and status over the core engine

Humans MUST be able to drive the full v0 loop from a CLI: scaffold a project, render the plan tree, sync idempotently, and view board status. The CLI MUST be a thin wrapper over the core library so its behavior is identical to the MCP path.

implementations:

Acceptance

  • taskship init scaffolds plan.yaml, the templates directory, and .taskship/.
  • taskship review renders the epic → story → task tree in the terminal from the current plan.yaml.
  • taskship sync (and taskship sync --dry-run) invoke the same reconcile engine and produce the same create/update/skip decisions as the MCP sync_to_jira tool for the same plan.
  • taskship status renders the plan-vs-reality view from reverse sync.

An MCP server MUST expose the same operations as agent tools

Agents MUST be able to drive the identical engine through MCP tools: decompose_brief, get_plan / update_plan, add_epic / add_story / add_task, sync_to_jira, and get_board_status. The MCP layer MUST read and mutate the same plan-as-code a human edits, so an agent's changes are reviewable in the same file.

implementations:

Acceptance

  • sync_to_jira(dry_run=true) returns the same create/update/skip diff the CLI sync --dry-run produces for the same plan, and writes nothing.
  • add_task mutates the in-memory plan such that a subsequent get_plan reflects the new task and a serialize produces a plan.yaml a human can review.
  • get_board_status returns live issue state equivalent to the CLI status view.

Decomposition MUST emit only schema-valid plans

taskship plan "<brief>" and the decompose_brief tool MUST produce plan output that validates against the plan schema before it is written; an invalid generated plan MUST be rejected, never silently patched into shape. Decomposition MUST NOT write to Jira — it returns the tree only.

implementations:

Acceptance

  • A generated plan that fails schema validation is rejected with a validation error; no plan.yaml is written from invalid output.
  • decompose_brief(text) returns the structured tree and makes zero Jira calls.
  • A successfully decomposed brief yields a plan that, when synced, passes the same validation and reconcile path as a hand-authored plan (decomposition is a bolt-on, not a special case).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

taskship-0.1.0.tar.gz (22.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

taskship-0.1.0-py3-none-any.whl (44.3 kB view details)

Uploaded Python 3

File details

Details for the file taskship-0.1.0.tar.gz.

File metadata

  • Download URL: taskship-0.1.0.tar.gz
  • Upload date:
  • Size: 22.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for taskship-0.1.0.tar.gz
Algorithm Hash digest
SHA256 84290da2baa6993e93c272896f746af61daa1ffbd9e5a7a058cf49a7cad3b16d
MD5 08a1e8ceac0eec7e4b2ab5527b41ae49
BLAKE2b-256 59e9607b07435200d588e6aa85e2cb1e445a556d3db02bf3ba3296d38b297129

See more details on using hashes here.

File details

Details for the file taskship-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: taskship-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 44.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for taskship-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 830b645f1bb2db57c40aa5f5cbbe29d9bb395468db9e5bd6399ffa218008c8cb
MD5 e808c810386685dd18de403c2d488292
BLAKE2b-256 bab850f0dca6bf22e2dc026ec08d02258143afc0ee57f6cdf1d65fc04d52a553

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page