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
mcpPython SDK (server), pytest (tests). Packaged as an importabletaskshippackage (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.
- per-field hashes) is committed alongside
- Task linkage (verified against Jira Cloud 2026-07): Jira's
parentmust 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 thetaskship:<epic>/<story>/<task>watermark plus a visibletaskship:story:<epic>/<story>label so it stays filterable in Jira. Sub-task-under-Story remains a possible futuretask_linkage: subtaskmode.
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.yamland 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 aproduct/jira_projectabsent at the top level) is rejected with an error naming the offending node path; no partial write occurs.
- A
tech-spectask withsubtype: perfthat omitsmetrics(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
titlewhile keeping itsidleaves its identity unchanged: a subsequent sync updates the existing Jira issue rather than creating a new one.
- A node authored without an explicit
idreceives 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
labelsof its own resolves to its story's labels, which in turn fall back todefaults.labelswhen the story defines none.
- A task that declares its own
labelsuses 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-spectemplate renders an ADF description containing the problem statement, user story, acceptance criteria, out-of-scope, and open questions sections.
- The
tech-spectemplate withsubtype: perfrefuses to render when no measurable baseline+target metric is present, and renders the metric (e.g.p95 480ms → 200ms) when present.
- The
devopstemplate 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 asparent(Jira rejects task→story parenting; the task carries ataskship: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.jsondeleted but issues still bearingtaskship:<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-runprints, per node, one of create / update / skip and the reason, and makes zero POST/PUT calls to Jira.
- A dry-run leaves
.taskship/state.jsonbyte-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.yamland syncing applies thetaskship:orphanedlabel 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-Afterheader 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
statusfetches 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 initscaffoldsplan.yaml, the templates directory, and.taskship/.
taskship reviewrenders the epic → story → task tree in the terminal from the currentplan.yaml.
taskship sync(andtaskship sync --dry-run) invoke the same reconcile engine and produce the same create/update/skip decisions as the MCPsync_to_jiratool for the same plan.
taskship statusrenders 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 CLIsync --dry-runproduces for the same plan, and writes nothing.
add_taskmutates the in-memory plan such that a subsequentget_planreflects the new task and a serialize produces aplan.yamla human can review.
get_board_statusreturns live issue state equivalent to the CLIstatusview.
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.yamlis 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84290da2baa6993e93c272896f746af61daa1ffbd9e5a7a058cf49a7cad3b16d
|
|
| MD5 |
08a1e8ceac0eec7e4b2ab5527b41ae49
|
|
| BLAKE2b-256 |
59e9607b07435200d588e6aa85e2cb1e445a556d3db02bf3ba3296d38b297129
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
830b645f1bb2db57c40aa5f5cbbe29d9bb395468db9e5bd6399ffa218008c8cb
|
|
| MD5 |
e808c810386685dd18de403c2d488292
|
|
| BLAKE2b-256 |
bab850f0dca6bf22e2dc026ec08d02258143afc0ee57f6cdf1d65fc04d52a553
|