← Plugin catalog
Productivity

Juscribe

Juscribe v1.4.50

Plan and track work in chat

Full description ↓

View on ChatGPT ↗

Save this plugin to Following and return to review observed changes. Follow up to 10 plugins free.

Description

From the marketplace listing

Read and change your Juscribe board, and work its tickets the way your team does. One install brings the Juscribe connector and three skills that carry the board's rules: ticket-workflow (the ticket lifecycle, estimation, labels, testing gates and the jus API reference), hard-rules (what never to do on a board) and retrospective (reviewing an iteration).

Language: English · Automatically detected from descriptions.

What changed

Juscribe

Oct 27, 2026 · 1 saved observations

Metadata · Oct 27, 2026 · 00:52 UTC · Compared with Oct 24, 2026 · 13:45 UTC

WHAT CHANGED

Package or technical metadata updated

Package or technical metadata updated

Observed in package metadata. These changes alone do not establish a new customer-facing feature.

Open full comparison.

Is v2 template

Before

Oct 24, 2026 · 13:45 UTC

Not present

After

Oct 27, 2026 · 00:52 UTC

false

Metadata: full comparison →

Competitor profile

From the developer’s listing and package; capabilities are not independently tested. Package observed Nov 3, 2026.

Declared capabilities

  • Read
  • Write

Declared limitations

Specific limitations have not been established. Missing information does not mean unrestricted capabilities.

Cost & access

Pricing and account requirements have not been established from the available package declarations. Unknown pricing does not mean free access.

Starter prompt

A starter prompt has not been provided in the catalog.

View package source →

Keywords

Files & skills

File archives

Plugin package40 files · 130 KBBrowse files →
Skill instructions
hard-rules17.7 KB

View saved version →

---
name: hard-rules
description: Non-negotiable behavioral guardrails for Juscribe work — every-ticket lifecycle, COMMIT-IMMEDIATELY rule, no lint/test suppression, stakeholder-verbatim ticket descriptions (agent additions kept current), no false deliveries, external blockers when waiting on user input, where a new ticket gets filed, splitting a ticket blocked on a third party, steps-as-subtasks, no `git push`, and the document-discoveries protocol. Auto-invoke before committing, before editing a ticket description, before transitioning to finished/delivered, before filing or placing a new ticket, when work turns out to depend on someone outside the team, and on hitting an unfamiliar error or workaround. In a Juscribe-wired project, generic ticket and board language means Juscribe by default, unless the wording names another system (a PR, a GitHub issue, another tracker's key).
allowed-tools: Bash(jus *), Bash(git *), Read, Grep, Glob, Edit, Write
license: MIT
---

# Hard Rules — Non-Negotiable Behavioral Guardrails

> Read this first, every session. These rules are always on. They have been flagged repeatedly because violating them wastes time, ships broken work, or destroys the stakeholder's intent. The companion `ticket-workflow` skill covers the _how_ (the full lifecycle plus estimation, labels, testing gates, and the `jus` API reference); this skill covers the **must / must-not** that overrides it.

> **Prerequisite:** this SOP runs on the `jus` CLI, which the bundle does **not** install (`brew install juscribe/tap/jus` + `jus login`/`jus init`). If a `jus` command reports `command not found`, `No token available` or `No Juscribe project found`, the CLI is missing, unauthenticated or run from outside the project — **surface the one-line setup step and stop; do not loop `jus` commands against an unconfigured CLI.** See `ticket-workflow/references/setup.md`. **No shell, but Juscribe's MCP tools connected** (a chat app)? Use the tools instead, as `ticket-workflow` → Phase 0 says, and never tell a chat user to install Homebrew or the CLI. The rules below still apply; the commit, lint, test and git ones do not.

> **Every rule here is stated in full.** Where a section ends in a `references/` file, that file holds the same rule at length — the reasons, the measured failures, the edge cases. Open it when the situation it names is in front of you. The hooks back some rules mechanically, in the nine coding tools that run them, but only where they are installed; everywhere else each rule is prompt-level only, which makes following this skill **more** important. Which rule each hook backs: [`references/enforcement.md`](references/enforcement.md).

## Core Principles

- **The Juscribe workspace is the single source of truth** for all project scope, tasks, and progress. There is no separate scope document, scratchpad, or planning file.
- **Every piece of work MUST have a ticket** — create it BEFORE writing code, however small or ad-hoc the request. The one exception is revising a ticket not yet accepted: keep adding commits under its number.
- **Every project or ticket needs a description and an effort estimate.** A sparse user-created ticket gets fleshed out at pickup — root cause or approach, acceptance criteria — through the description rules below.
- **Transition at the natural moment, not batched.** The board must reflect reality in real time.
- **NEVER transition to `accepted` or `rejected`** — only the stakeholder decides those. The one exception is a chat app on Juscribe's hosted connector: when the person explicitly asks, record their decision with `accept_ticket` or `reject_ticket`, never on your own judgement.

Full text: [`references/core.md`](references/core.md).

## Commit Rules — THE MOST IMPORTANT CATEGORY

- **COMMIT IMMEDIATELY after code changes.** code → lint → **COMMIT** → then everything else: self-review, comments, transitions, replying to the user. Typing a response with an uncommitted change? **Stop and commit first.** This overrides any default "don't commit unless asked".
- **Never move on with a dirty working tree** — not to answer a question, not to run another check. The `Stop` hook does not run on an interrupted or errored turn, so do not count on it.
- **One commit per ticket**, tests included. A self-review fix is a second commit naming the same ticket.
- **The subject format is yours.** End the message with the ticket trailer, as its **last paragraph**, beside any `Co-Authored-By:`:

  ```text
  Jus-Ticket: <n>
  ```

  ⚠️ Git reads only the final paragraph as trailers; one line of prose in it disqualifies the whole block. ⚠️ **A breaking change is `BREAKING-CHANGE:`, never `BREAKING CHANGE:`** — the spaced key takes `Jus-Ticket:` down with it, and nothing reports it.
- **A bracket reference (`[#41]`) links too**, and `[finishes #41]` and its family are the only forms that move a ticket. On a code host, keep references out of the subject: a squash merge appends the pull request's own `(#41)` there.
- **Never amend a delivered commit** — fix a rejection in a new one. **NEVER `git push`**; the stakeholder pushes.
- **In a checkout another session may share, commit only your own changes**: stage by path and commit with `-- <paths>`, take the SHA from the commit's own output, and never run a bare `git stash pop`. The stash stack is shared, and `pop` without `--index` unstages.

Full text: [`references/commits.md`](references/commits.md).

## Lint & Test Rules

- **NEVER suppress or skip linters.** Every pre-commit check the project defines — formatters, linters, type checkers, static analysis — must pass cleanly. No inline `disable`, `ignore` or `expect-error` directive; fix the underlying problem. The only acceptable annotations are structural ones already established in the codebase. A genuine false positive goes to the stakeholder, never into a suppression comment.
- **Fix ALL lint warnings in modified files**, pre-existing ones included. Don't check `git blame`; fix it.
- **A change ships with tests, and a bug fix ships with one that failed first.** Whether you write them before the code is **the project's testing policy** to set; this skill's default is that you do.
- **Lint and test the changed files BEFORE every commit** — every applicable linter, formatter and type checker, plus the tests covering the change, preferring the runner's dependency-aware selection. The commands live in the project's own instructions, not here. **Widen the scope when the change is cross-cutting** (a base class, shared fixture, migration, config default): a changed-file gate cannot see a test with no import-level link to it. Skipped gates compound silently across tickets.
- **Diff coverage is a gate, and the bar is the project's** — 100% of new and changed lines where the policy sets no other number. A failure means more tests, not "good enough".

Full text: [`references/lint-and-tests.md`](references/lint-and-tests.md).

## Shell Safety — Prose and Commands

- **NEVER build a shell command by inlining prose you wrote.** Comments, descriptions and commit bodies go through a **file written with a quoted heredoc** (`<<'EOF'`), then `"$(cat file)"`, a pipe or `@file`. One apostrophe ends a single-quoted argument, and the backticks in the rest of the sentence then **run as commands** — an arbitrary-command-execution risk, not a formatting preference.
- **NEVER hand a person a command long enough to wrap.** Put it in a script taking one short argument: a shell continues a line that _ends_ with `&&` and rejects one that _begins_ with it. **Never ask for a token to be pasted into a chat** — hand over a script that reads it without echoing — and **never print a token file** to check it is set; `jus whoami` answers that.
- **zsh does not word-split `$files`**, so a scoped lint can check nothing and exit 0: use an array. **A handed-over command** gets `GH_PAGER=cat` or `git --no-pager`, says what it prints, and never reads the clipboard.

Full text: [`references/shell-safety.md`](references/shell-safety.md).

## Delivery Rules — Did You Actually Do What the Ticket Asks?

- **NEVER deliver work that defers, skips, or deviates from what the ticket prescribes.** Re-read the ticket before finishing. Anything deferred, skipped or changed: leave it `started`, comment, and add the External blocker below. When in doubt, ask — don't deliver.
- **"Comprehensive" / "100%" / "thorough" mean exactly that.** Where a project ships more than one client, passing tests on one do not cover the other: `ticket-workflow/references/second-client.md`.
- **TICK THE BOXES BEFORE YOU DELIVER.** Every `- [ ]` you met, every subtask done — an unticked one tells the stakeholder the work is not done. Tick a subtask the turn its step completes, not in a sweep at delivery.
- ⚠️ **And never tick one you have not met.** It is the worse lie, because it looks like success. **The check is one question: what evidence would I cite?** A command, an output, a file, a commit. "It will be true once this ships" is a forecast.
- **Every delivery comment carries a "To verify" section and the git information** (commit SHA and `git show`, or an explicit "no code changes") — in batch work too.

Full text: [`references/delivery.md`](references/delivery.md).

## Ticket Description Rules

The rule protects exactly one thing — **stakeholder-authored text, preserved verbatim, always** — and imposes one duty on everything else: **agent-authored text stays current.**

- **NEVER overwrite the stakeholder's words.** Before any PATCH that replaces `description`, fetch the ticket first; an append (`description_append`) needs no fetch, because it never sends their words back. Their text comes first, then a `---` line, then yours — in every context: triage, notes, cancellation reasons, acceptance criteria.
- **Below that boundary, EDIT — don't layer.** When facts change, fix your own text in place so it reads as one current spec; never stack dated "Update (…):" sections. A dated note is only for a stakeholder claim you must not touch.
- A stakeholder-requested rewrite may restructure everything — the one line never crossed is discarding their original words.

**Fleshing out needs no read at all.** Write your additions to a file, starting with the `---` line (drop it when the description is empty), and send them as `description_append`. The server joins them to whatever the row holds at write time:

```sh
test -s .jus/tmp/append.md \
  && jq -Rs '{ticket:{description_append:.}}' < .jus/tmp/append.md > .jus/tmp/append.json \
  && jus api PATCH /workspaces/{ws}/tickets/{id} @.jus/tmp/append.json
```

**MCP:** `append_to_description`.

**Editing inside the text** needs the round trip; use the guarded read and write in `ticket-workflow/references/delivering.md` → _Then tick the boxes you met_. ⚠️ An unguarded round trip wipes descriptions — an empty description is a valid PATCH and answers `200`. The routes and the recovery: `ticket-workflow/references/api.md` → _Writing a description without wiping it_.

**MCP:** `get_ticket` to read, then `update_ticket` with the whole description.

Full text: [`references/descriptions.md`](references/descriptions.md).

## External Blocker Rule — Always Track What Needs User Input

- **Waiting on the stakeholder** (a value, a credential, a decision)? Leave the ticket `started`, comment what you need, and add an **External dependency** — the dependency is what makes the block visible; a comment alone is not.
- **`title` is the half the board draws**: a few words, not a paragraph. Omitted, it is derived from the description and cut at 80 characters.

```sh
jus api POST /workspaces/{ws}/tickets/{id}/dependencies '{"dependency":{"blocker_type":"External","blocked_type":"Ticket","blocked_id":{id},"title":"User input: <the ask in a few words>","description":"<the full ask>"}}'
```

**MCP:** `add_blocker` with `blocker_type` External.

- **A blocker whose condition is a time carries `due_on` and `due_kind`** (`wait_until`, `review_on` or `expected_by`); each without the other is a `422`. Not knowing the date is what `review_on` is for.

Full text: [`references/blockers.md`](references/blockers.md).

## Steps Are Subtasks, Never Description Checkboxes

- **A SEQUENCE of steps someone performs is subtasks**, not `- [ ]` lines: a checkbox is prose, a subtask is data the board renders, counts, orders, assigns and lets the stakeholder tick from a phone. One thing to do needs no subtask; it is the ticket.
- Each carries a `title` of `N. ` plus a short label (**not the command** — titles truncate), a `description` holding the bare command in a fence then the traps, an `assignee_id` (**the actor is the `assignee_id`, not a `**[Actor]**` tag**) and a `position`. **One command per subtask**; a `cd` is its own and is never repeated.
- **Record completion by setting `completed`; never `/toggle`**, which unticks a step someone else already ticked.
- **Mixed-actor tickets:** assign both, one chronological list, and when the next step is the stakeholder's, a blocker that is a `title` naming the subtask and nothing else.
- **Acceptance criteria stay description checkboxes** — a step has an actor, a criterion does not.

Full text: [`references/subtasks.md`](references/subtasks.md).

## Ticket Placement — Filing Is Not Prioritising

- **A new ticket goes to the BOTTOM of the backlog by default** (an unprioritized one to the top of the icebox), set on the create. The middle is a sequencing decision the stakeholder already made.
- **Three overrides:** the stakeholder named a position; the work is genuinely urgent (an outage, an exposed credential), which belongs at the top; or it plainly belongs before things already queued — then **read the backlog**, place it between two neighbours, and say which two in the delivery message.
- **`project_id` names only a project still `unprioritized`, `prioritized` or `started`.** Check its state first; nothing refuses the create, and a closed project stops tracking its contents. File standalone, under a live project, or in a successor, and write the closed one as `pN` in the description.

Full text: [`references/placement.md`](references/placement.md).

## The Ticket Is a Claim, Not a Contract

**Before you start, check the ticket's own statements, and say what the prescribed approach assumes** — both in the start comment. Re-run cheap measurements, and ask what has to be true for the approach to work. A prescription questioned before implementation costs one message; after, it costs the rebuild. Full text: [`references/claim-check.md`](references/claim-check.md).

## Blocked on a Third Party — Split at the Boundary, Deliver Your Half

A ticket whose remaining criteria wait on someone outside the team **never sits in `started`**. Re-scope the original to what you own and **deliver** it (do not cancel it); move the rest to a **successor in the icebox** with an External blocker naming the awaited event, a cross-reference both ways, and a closing condition. **The test:** _could anyone here complete this ticket today, given unlimited effort?_ Waiting on your own deploy plus elapsed time fails it too. Full text: [`references/third-party-split.md`](references/third-party-split.md).

## Convention & Reuse Rules

- **Follow existing standards and conventions** — study how similar things are done (naming, structure, patterns, component design, CSS approach, API shape, test style) and match them.
- **Reuse existing styles, components, and patterns** before building new ones.
- **Flag, don't silently fork.** Raise an outdated convention with the stakeholder rather than adding a second pattern beside it.

Full text: [`references/core.md`](references/core.md).

## Document Discoveries

**Hit an error, found a workaround, learned something non-obvious? Document it NOW** — in the ticket thread as it happens, in `.jus/docs/` (with an `INDEX.md` line) for anything that will recur, in code comments for how a system works, and in your agent's context file for cross-session patterns. **Read `.jus/docs/INDEX.md` before re-deriving**, and route shared knowledge to the shared docs, never only to private memory. Full text: [`references/discoveries.md`](references/discoveries.md).

## Quick "Stop and Check" Reflexes

If any of these are true at the moment you're about to act, stop and reset:

| Reflex | Stop and… |
| --- | --- |
| Working tree is dirty and you're typing a response | **Commit first.** |
| About to replace `description` without fetching first | **Fetch first** — or append with `description_append`. |
| About to silence a lint warning with a suppression comment | **Fix the smell or escalate to the stakeholder.** |
| About to mark `finished`/`delivered` with a deferred or skipped item | **Leave in `started`** + comment + External blocker. |
| About to ask the user a blocking question without an External dependency | **Create the dependency** before asking. |
| About to `git push` | **Don't.** The stakeholder pushes. |
| About to write code without a ticket | **Create the ticket first.** |
| About to edit a source file on a `started` ticket, no start comment yet | **Post the start comment first** (root cause + plan + test intent). |
| About to transition to `accepted` or `rejected` | **Don't.** Only the stakeholder owns those transitions. |
| Hit an error or learned something non-obvious | **Document it now** (ticket comment + docs/code as applicable). |
| About to write "Stakeholder does: / Agent does:" sections in a description | **Rewrite as ONE numbered subtask list in execution order**, each with its own `assignee_id`, and assign both parties on the ticket. |

## Related Skills

- `ticket-workflow` — the single load-bearing SOP skill: full lifecycle phases, transitions, comments, delivery format, dependency handling, plus estimation, ticket types, labels, metadata, the testing gates, and the `jus` CLI / API reference. (Where a plugin install shows skill names prefixed with `jus:`, invoke the prefixed form.)

Referenced files: 13 — file references are metadata, not package contents.

retrospective40.7 KB

View saved version →

---
name: retrospective
description: How to run a retrospective on an iteration and hand it over as a self-contained HTML report with charts, then record it back on the board. Covers what to gather (the first-hand accounts first — the ticket comments that hold most of them — then the tickets, their activity and the board's timezone), the sections a retrospective owes and the ones it must stop writing, when in the day work actually happened, production health and standing technical debt, drawing conclusions rather than counting metrics, hand-authored inline-SVG charts that need no library and no network, verifying derived numbers before you publish, sending the report's own HTML to the board so the iteration's details pane can show it, and writing the record back as an iteration comment. Auto-invoke on "retrospective", "retro", "sprint review", "review the iteration", or any request to review or summarize what an iteration delivered.
allowed-tools: Bash(jus *), Bash(git *), Read, Grep, Glob, Write
license: MIT
---

# Retrospective — Review an Iteration and Publish the Report

> Read this when asked to retrospect on an iteration, sprint or cycle. It covers gathering, analysis, charts, verification, delivery and the write-back. The companion `ticket-workflow` skill covers the ticket lifecycle and the `jus` API; this skill assumes it and does not repeat it.

## What you produce: a report and a record

**Two artefacts, and they are not the same document.**

|  |  |
| --- | --- |
| **The report** | One self-contained HTML file. Charts, tables, the full analysis. This is what a person reads, and you send it to the board, which stores and serves it. |
| **The record** | A comment on the iteration carrying `context: "retrospective"` — a short lede, where the report is, and the follow-ups you are **suggesting**. This is what the board keeps, and what someone replies to. |

**The record is short on purpose.** Its job is that the iteration carries a durable trace and something to reply to. If you find yourself restating the analysis in it, stop — you are writing the report twice, and the second copy is the one that goes stale.

⚠️ **NEVER deliver only the record.** A retrospective that fits in a comment is one you did not actually do. The board renders comments as plain markdown with no embedded HTML, so every chart you drew is lost the moment you paste it there.

## Step 1 — Read the first-hand accounts FIRST, and look on the TICKETS

```
jus api GET '/workspaces/{ws}/iterations/{iteration}/comments'
```

**MCP:** no MCP tool reads an iteration's comments. Read the thread in the app, or ask the person to paste it.

That is the iteration's own thread. On most boards it is **empty, or holds only the records of previous retrospectives** — measured on one board, one comment across 196 iterations, and it was a retrospective record. Read it, then keep going. The testimony is real; it is written one level down.

```
jus api GET '/workspaces/{ws}/tickets?iteration_id={iteration}&per_page=200&include_markers=true&include_comments=true&include_label_objects=false'
```

**MCP:** `list_tickets` with `iteration_id` and `include_markers`, then `get_ticket` on each ticket for its comments: the list pages 50 at a time and carries none.

⚠️ **`include_comments=true` IS WHAT TURNS THE TICKET LIST INTO THE LOG.** Without it the same request returns comment *counts*, which is a measure of back-and-forth and not a word of what anyone said. Where a workflow asks for a comment when work starts and another when it is delivered, every ticket carries a first-hand account of itself — the plan, what it assumed, what turned out to be wrong. Measured on a 112-ticket iteration: **252 comments on 102 of the 112 tickets**, median 2,940 characters, **78 tickets carrying language about a retry, a wrong assumption or a reversal.**

⚠️ **DO NOT READ THE WHOLE CORPUS.** That same fetch was **793,576 characters — roughly 198,000 tokens**, arriving as one 1.03 MB payload in 0.75 s. One request is cheap; reading all of it is not, and a retrospective that tries will run out of room before it reaches the charts. Fetch once, then slice.

**Two slices pay for themselves:**

| Slice | What it gives you | Cost, measured |
| --- | --- | --- |
| Comments **not written by a bot** — `select(.user.bot \| not)` | On an agent-run board this is the whole human record: bug reports from a real device, scope corrections, "we don't need that here". ⚠️ A way to **find** the testimony, never a statistic to publish — see **What NOT to report** | **7 comments, 1,814 characters** |
| The tickets the report will actually discuss — every `rejections_count > 0`, the longest cycle times, the re-estimates | Where "it was the third attempt" is actually written. Section 7 needs these | a handful |

```
jq '[.tickets[] | {id, title, comments: [.comments[] | select(.user.bot | not) | {created_at, body}]}] | map(select(.comments | length > 0))'
```

**Weigh testimony above anything you infer.** Where the two disagree, say so explicitly and give both. "The board shows this accepted on the first pass; the comments record two days lost to a wrong assumption before the ticket was opened" is the most valuable sentence a retrospective can contain, and no metric produces it.

⚠️ **THE ITERATION BOUNDARY DOES NOT BOUND THE CONVERSATION.** A ticket belongs to the iteration it was **accepted** in, not the one it was worked in, so its comments spill either side of the dates. Measured on a one-day iteration: 4 comments written before it, 178 on the day, and **70 the day after**. Do not date-filter the comments to the iteration's own dates — you would discard 28% of the record, and the discarded part is the end of every story.

⚠️ **AN EMPTY ITERATION THREAD IS THE NORMAL CASE, AND THE REPORT SAYS NOTHING ABOUT IT.** Take the testimony from the ticket comments and write it up without provenance — where you found it is your problem, not the reader's. Report testimony as *missing* only when the tickets are silent too, which is the case where the absence really is the finding. **Do not file a ticket saying nobody writes iteration notes** — that was filed, measured and decided against: with one-day iterations and up to ten tickets in flight inside half an hour across concurrent sessions, nobody has the vantage point to write "how the iteration went", so per-ticket is the granularity and the iteration thread is for the record.

## Step 2 — Gather the iteration

```
jus api GET '/workspaces/{ws}/summary' | jq -r '.workspace.timezone'
jus api GET '/workspaces/{ws}/iterations/{iteration}'
jus api GET '/workspaces/{ws}/tickets?iteration_id={iteration}&per_page=200&include_markers=true'
```

**MCP:** `get_workspace_summary` for the zone, `list_iterations` for the iteration, and `list_tickets` with `iteration_id` and `include_markers`.

**Fetch the zone first and hold it**, because every date you bucket and every time you render depends on it — see **EVERY TIMESTAMP** in [references/reporting-traps.md](references/reporting-traps.md) for why the summary endpoint is the one to ask and the workspace row is not.

The iteration row carries the committed and accepted point totals. The ticket list carries everything else — state, type, points, labels, project, requester, stakeholder, assignees, `rejections_count`, and the timestamps the board exposes.

⚠️ **`include_markers=true` IS LOAD-BEARING.** The ticket index drops release, milestone and deadline markers by default, and `pagination.count` reports the reduced number — it describes the rows the filter let through, so it is internally consistent with the thinned page you were handed and every check you can run on the list passes. Measured on a real iteration: **102 without the flag, 112 with it.**

**The ten it dropped were the release markers**, which is precisely the population section 10 exists to examine — so the query is blind in the one place the analysis has to look. A retrospective run without the flag is not slightly incomplete; it is confidently wrong about what shipped.

⚠️ **`.meta.excluded_markers` answers this directly, WHERE THE SERVER HAS IT.** Newer Juscribe returns it on every ticket index — always present, `0` when nothing was withheld, so a zero is an answer rather than a silence. Read it and you know at once:

```
jus api GET '/workspaces/{ws}/tickets?iteration_id={iteration}&per_page=200' | jq '.meta.excluded_markers'
```

**MCP:** `list_tickets` with `iteration_id`. Its reply says in `notes` when markers were left out.

**An older server has no `meta` key at all**, and `jq` answers `null` — which reads exactly like "nothing was withheld" and is not. So do not treat its absence as reassurance; that is what check 5 in step 5 is for, and that check works against every version.

**Page it.** An iteration can hold a hundred tickets or more; read `pagination.pages` and keep going. A retrospective computed over page 1 is wrong in a way that looks entirely normal.

### Timing comes from the activity feed, not the ticket

**A ticket payload carries very little about when anything happened** — typically its creation and, if the board has one, a single done timestamp. Check what you actually got back before planning around it; boards differ, and a column existing is not the same as a client being able to read it.

**Everything else is in the per-ticket activity feed, and it is richer than it looks:**

```
jus api GET '/workspaces/{ws}/tickets/{ticket}/activities?per_page=100'
```

**MCP:** no MCP tool reads a ticket's activity. Use the timestamps `get_ticket` returns, and say the history is incomplete.

| Entry | Carries | Gives you |
| --- | --- | --- |
| `state_change` | `{from, to}` + `occurred_at` | transitions in order — cycle time, lead time, dwell in each state, when a rejection happened. A backward move before 2026-08-28 is an `update` instead: see **A BACKWARD STATE MOVE** in [references/reporting-traps.md](references/reporting-traps.md) |
| `create` | the opening `title`, `ticket_type`, `points` | the **first estimate**, which is what estimate drift is measured against |
| `update` | the changed field as `{from, to}` | re-estimates (`points`), retypes, project moves, ownership changes |
| `comment`, `dependency_*`, `attachment_*` | ids and names | how much back-and-forth a ticket took |

⚠️ **`create` plus `update` is how you get estimate drift**, and it is easy to conclude wrongly that it is unavailable. Nothing is named "original points" anywhere in the feed — the opening estimate is a key inside the `create` entry, and every change to it is a separate `update`. Measured on a real iteration: 75 of 102 tickets carried an opening `points` value and 31 re-estimates followed.

⚠️ **A ticket created without an estimate has no opening value to drift from.** That is a real state, not missing data — count those tickets and say how many rather than treating the first later estimate as the original.

⚠️ **It is one request per ticket, so decide before you start.** On a hundred-ticket iteration that is a hundred round trips — measured at about 40 seconds, which is usually worth paying. Either spend them deliberately, or say in the report that timing was computed over a named subset and which one. **Do not silently sample.**

⚠️ **BUCKET DATES IN THE BOARD'S TIMEZONE, NOT THE TIMESTAMP'S.** Timestamps come back in UTC and an
iteration's start and end are local dates, so a ticket filed in the evening lands on the following day if
you compare the raw strings. Measured on the first real run of this skill: 19 of 102 tickets moved to the
next day, turning "90% of this iteration was filed the day it was worked" into a much weaker claim. Nothing
about the wrong answer looks wrong.

⚠️ **A null timestamp means unknown, never zero.** Columns added partway through a board's life are null on every row that predates them. Averaging over them silently pulls the number toward zero and produces a figure that is wrong and plausible — the worst kind. Count the nulls, exclude them, and report how many you excluded.

## Step 3 — What the retrospective owes

**A headline and ten sections. Scale the depth to the iteration, not to this list** — a one-day iteration with four tickets gets a short report, and saying "there is not enough here to retrospect on" is a legitimate finding. A hundred-ticket iteration gets all ten.

### The headline is three to five numbers, and no more

Points closed, tickets closed, the rank against the whole series, and at most two others. **Every other number in the report appears inside a sentence that says what it means.** A figure standing on its own is one the reader has to interpret on your behalf, and most figures do not survive the attempt — which is how a report ends up long, dense and unread.

⚠️ **The test for a number is not "is it true", it is "would the reader act differently".** Type distribution, comment counts, per-actor event tallies and events-per-hour are all true and none of them changes a decision. Cut them unless one of them moved.

1. **What happened** — grouped by **theme**, never by ticket. This is the section a reader most wants and the one most often replaced by a list. A ticket id belongs in it as **evidence for a claim** — "the third attempt at this (#81) is where the parser was finally replaced" — never as the subject of its own row. ⚠️ **If a table's rows are tickets, it is the wrong table**: rewrite it so the rows are the findings and the ticket ids sit in a cell.

2. **What people said** — the testimony gathered in step 1, quoted, and weighted above anything you inferred. Where testimony and timestamps disagree, say so and give both; that is the most valuable sentence a retrospective can contain and no metric produces it.

3. **Where the effort went** — by kind of work: user-facing features, bug fixing, performance, internal tooling and plumbing, research, documentation. **In points, not ticket counts**, so it reads as effort rather than volume.

4. **How good the estimates turned out** — whether the points the board committed to matched the work the iteration actually took. This is the section that turns an estimate into a measurement instead of a ritual.

   **Three questions, and only the first is arithmetic:**

   | Ask | From |
   | --- | --- |
   | Did the estimates hold in aggregate — committed against accepted | the iteration's own totals |
   | Which tickets were sized wrong, and in which direction | each ticket's points against how long it actually took (the activity feeds from step 2) |
   | **Why** they were wrong | the ticket's own comments. Nothing else answers it |

   ⚠️ **POINTS ARE NOT HOURS, so "the 3-pointer took four hours" is not a finding.** What makes one is a comparison **between tickets of the same size**: if the 3-pointers ran from twenty minutes to a full day, the scale is not measuring what it claims to. Group by point value, and report where the spread is widest.

   **Name the direction, and keep the three causes apart — they have different remedies:**
   - **Mis-estimated.** The work was what everyone thought it was and the size was called wrong. A calibration problem, and the only one that corrects by moving the scale.
   - **Unforeseen complexity.** The work turned out to be a different shape once opened. The ticket's comments almost always contain the sentence where somebody found out — quote it rather than paraphrasing.
   - **An unknown unknown.** Nobody could have estimated it, because nobody knew it was there. **Not a calibration failure**, and counting it as one makes the estimates look worse than they are. The useful figure is how many there were and what they had in common.

   ⚠️ **A ticket created without an estimate has no opening value to drift from** — a real state, not missing data. Count those separately and give the number.

   ⚠️ **Do not report a single accuracy percentage.** Committed against accepted is dominated by scope entering and leaving, not by estimation, so one number attributes to the estimator what the week actually did. Give the per-ticket picture instead — and if the estimates simply held, say so in a line. That is a legitimate finding and usually the true one.

5. **When the work happened** — the hours of the day work was actually being done, per day of the iteration, in the board's zone. **Distinct active hours, the span from first to last, and where the gaps fall.** This is the section that answers "what did the week actually look like", and it is invisible in every other cut.

   The cheapest complete source is the commit log, which needs no API and covers every day at once:

   ```sh
   TZ=<board zone> git log --since='<start> 00:00' --until='<end+1> 00:00' \
     --date=iso-local --format='%ad' | awk '{print substr($2,1,2)}' | sort | uniq -c
   ```

   ⚠️ **`TZ=` plus `--date=iso-local` is what makes this correct, and `%aI` is the trap.** `%aI` prints each commit's own recorded offset, so slicing it by character position buckets a commit by whatever zone its author's machine was in. Measured on one repository: **35 of 5,232 commits are recorded at `+00:00`** because they were made from a container, against 5,197 at the local offset — enough to move a bar and never enough to look wrong. `--date=iso-local` re-renders every commit in one zone, and `TZ=` chooses which.

   **Cross it with the board.** Commits miss the tickets that produced no code — research, review, anything the stakeholder did — and the per-ticket activity feeds from step 2 carry those. Where the two disagree about when the day started, that difference is itself worth a sentence.

   **What to actually report:** how many of the 24 hours carried work, the span, the longest gap inside it, and whether any of that changed from the previous iteration. Not a count of events per hour, which is throughput wearing a clock.

6. **How production behaved** — the iteration's effect on the running system, from whatever the project already has: an APM, an error tracker, a log store, an uptime check.

   | Ask | Because |
   | --- | --- |
   | **Error rate** over the window, against the preceding one | the one number that says whether the iteration made things worse |
   | **Latency** — p50, p95 and p99, not a mean | a mean hides the tail, and the tail is what people feel |
   | **Throughput** | the denominator. A halved error rate on a tenth of the traffic is not an improvement |
   | **The error classes that actually fired**, faceted and counted | a rate says something changed; the classes say what |

   ⚠️ **A deploy inside the window means the figures straddle two versions.** Say where the deploys fell before comparing anything across them, or bound the query to one side.

   ⚠️ **Bound the window from the board's local dates, converted.** A query language's time literal is machine input and is usually UTC — that is the one place UTC is correct, and the report says so where it shows the query.

   ⚠️ **If the project has no such tooling, or this run cannot reach it, that is one clause and NO SECTION.** Not a heading, not a table of what could not be read, not the query you would have run. See _A source you cannot reach_ below for where the clause goes. Do not substitute something that looks like a production signal either — CI duration, test counts, a log line count.

7. **What debt stands** — the standing cost the iteration inherited, and whether it moved.

   | Ask | Typical source |
   | --- | --- |
   | How far behind are the direct dependencies, and for how long | whatever the project's package manager answers this with |
   | What is known-vulnerable right now | the project's audit tool, plus the platform's own advisories |
   | What upgrades are pending and blocked, and on what | the board, plus whatever the project already records |

   ⚠️ **"Is what we have vulnerable?" and "how far behind are we?" are different questions, and a clean scan is not evidence of currency.** A library can sit six releases behind for four months and emit no vulnerability signal at all, because none of those releases fixed a CVE. Report both, and never let one stand in for the other.

   **The interesting figure is the direction, not the level.** Debt that is flat is a fact about the project; debt that grew or shrank this iteration is a fact about the iteration, which is what a retrospective is for.

8. **What went wrong** — difficulties, reversals, work that shipped and was then found wrong, and the rejections.

   ⚠️ **CLASSIFY THE REJECTIONS BEFORE ANY RATE IS REPORTED.** A rejection count is not a rejection rate: the counter tallies transitions, and several of them are not the thing the number is read as meaning. **Read each one** — the ticket's comments and its activity feed — and split them:

   | Looks like a rejection | Is it? |
   | --- | --- |
   | Work sent back because it was wrong or incomplete | **Yes.** This is the population a rate is about |
   | A reject-and-redeliver minutes apart, to exercise the fix under test | No. That is a verification step wearing a state change |
   | An author re-opening their own work before anyone reviewed it | No. Nobody rejected anything |

   Measured on a real iteration: **5 events across 4 tickets, of which 3 were real and sat on 2 tickets.** Reported raw, the rate is nearly double. Give the classified figure, say how many events you excluded and why, and name the tickets — the reader can then disagree with your calls, which they cannot do with a bare number.

   **Scope churn lives here, and only when it moved** — what entered and left mid-flight, and at whose instigation. ⚠️ **Estimate drift does NOT: it belongs in section 4**, which is about the estimates themselves. Two sections reporting on the same points is how a reader ends up holding two different accuracy figures and no way to reconcile them.

9. **What the data says that nobody asked for** — the section the rest of the report exists to make possible, and the one most likely to come out as filler.

   **A finding here is a claim someone could disagree with.** "Velocity was 82 points" is not one. "Every ticket that took more than a day had its estimate raised, and none that took less did" is.

   **Four rules keep it honest:**
   - **Name the two things you compared, and the window.** A trend with no baseline is an observation about one number.
   - **Try once to explain it away, and say what you tried.** A finding that survives a stated attempt is worth reading; one that was never challenged is a coincidence with a sentence around it.
   - **Distinguish a trend from a step change.** A gradual drift and a clean jump at one instant have different causes, and a jump that lands exactly on a boundary — a deploy, a month, a schema change — is a measurement artefact until proven otherwise. ⚠️ **Check the population before believing the number**: an instrument working perfectly can be counting something other than what you think it is counting.
   - **"Nothing unusual this iteration" is a legitimate finding.** Say it in a line. It is far better than four manufactured ones, and a reader who has seen you say it will believe the next section that is not empty.

10. **What shipped, and what has not** — work that is merged but not yet released is not delivered value. If the board tracks releases, say what is sitting behind an unrun one.

   ⚠️ **Split that figure by state before you report it.** A release's list of pending work mixes accepted tickets with ones merely delivered or since cancelled, so a single point total silently overstates what the iteration actually accepted and has not shipped. Give the accepted number as the headline and the rest beside it. Measured on a real run: 37 points on the marker, of which **25** were accepted — the draft led with 37 and called them accepted.

**Seven traps in the prose of those sections — a rank claim computed over the wrong set, a velocity that is a setting or a two-week window, a backward move the old feed logged as an edit, the section that gets faked, the board's timezone, naming people, and an unreachable source: [references/reporting-traps.md](references/reporting-traps.md).**

## ⚠️ What NOT to report

**This list exists because every item on it is true, easy to compute, and worth nothing to the reader.** That combination is what makes them come back: each one looks like diligence, each one passes every check in step 5, and a skill that only says what to *include* re-acquires the whole set one run at a time.

They were named by the person the reports are written for, after reading one that contained all of them.

| Do not report | Why it is not a finding |
| --- | --- |
| **Who is a bot and who is not** — that most tickets were created, worked or accepted by an agent | It is the board's operating model, not something that happened this iteration. State the model in one clause the first time and never again. **Report it only when it changed** — a shift in who does what IS a finding |
| **That one person does all of the accepting** | Same. It is the review structure, and it was the same last iteration |
| **The human-versus-bot comment ratio** | A constant of how the board is run. "Only 2 of 140 comments are human-authored" measures the workflow, not the work |
| **That most chat happens outside the board** | True on nearly every board, unactionable, and it reads as a complaint about the reader. Quote what was said; do not audit where it was said |
| **That the iteration thread had no comments** | The normal case. Take the testimony from the ticket comments and say nothing about it. It becomes a finding only when the tickets are silent too |
| **Which source the testimony came from** | Provenance the reader did not ask for. Quote the person and cite the ticket |
| **Ticket-by-ticket detail** — a table whose rows are tickets, a paragraph per ticket, a walk through one ticket's lifecycle | The reader has the board for that. A ticket id is evidence inside a claim, not a subject |
| **Metrics that did not move** | Type distribution, per-actor event counts, comment counts, events per hour. If the number is the same as last time, the sentence is "unchanged" or it is nothing |

⚠️ **The failure mode is subtler than including them: it is letting one become a section heading.** A heading is a promise that what follows matters, so a section named after a constant makes the whole report read as padding — and the sections that do matter get skimmed at the same rate.

**What replaces them is not silence.** Sections 4, 5, 6 and 8 are all things nobody was reporting, and every one of them costs about what the list above cost. The budget moves; it does not shrink.

### Anonymising is on this list too

See **NAME PEOPLE** in [references/reporting-traps.md](references/reporting-traps.md). It appears here as well because it is the same instinct — writing around the specific because the general feels safer — and because it was the first thing noticed in the report that produced this list.

## Step 4 — Draw the charts

**Hand-authored inline SVG. No chart library, no CDN, no network.**

This is not a stylistic preference. The report has to work as a hosted page **and** as a file opened from disk — a hosted page restricts which hosts it may load from, and a file on a laptop may have no network at all. A library that works in one and fails silently in the other gives you a report with blank rectangles where the analysis was, and nothing warns you.

**If a chart needs a library to be worth drawing, make it a table instead.** A retrospective's charts are bars, columns and a distribution; all three are a dozen lines of SVG.

**Which cuts earn a chart, the bar-row markup, the `viewBox` rule and a palette that survives both themes: [references/charts.md](references/charts.md).**

## Step 5 — Verify the numbers before you hand it over

⚠️ **A RETROSPECTIVE COMPUTES, SO LOOKING AT IT IS NOT CHECKING IT.** A wrong figure in a report is worse than no report: it is quotable, it gets carried into the next planning conversation, and nothing downstream will ever re-derive it.

**Check these nine, every time. They are the ones that go wrong:**

1. **The parts sum to the whole.** Effort by kind of work must total the iteration's points. If it does not, some tickets fell out of your classification — find them and add an "unclassified" row rather than letting the shortfall hide.
2. **Percentages total 100**, allowing for rounding, and say which way you rounded.
3. **Every bar's width is proportional to its value, on the SAME scale as its own axis.** Recompute one by hand from the rendered `width` and compare — then check that the axis labels use that scale too. The failure that actually happens is a chart whose bars were sized from the largest value and whose axis was labelled from a round number: every bar is proportional to every other bar, so it looks right, and every one is in the wrong place against the gridline.
4. **Counts agree with the source.** Your ticket total must equal what the API reported in `pagination.count`. A mismatch means you missed a page.
5. **The list agrees with the ITERATION ROW, which is a second source.** ⚠️ Check 4 alone cannot catch a filtered list — `pagination.count` describes the rows the filter let through, so it agrees with itself no matter what was withheld. The iteration's own `done_tickets_count` counts every ticket the Done panel lists — accepted, cancelled, converted and archived — **including markers**, and is computed by a different query, so compare the two: **your list must not be smaller than it.** A list of 102 under a `done_tickets_count` of 106 is the tell. Where the server returns `.meta.excluded_markers` (step 2) that says it outright, but this check needs no particular server version, so run it either way. If your list is smaller, re-fetch with `include_markers=true` before going any further.
6. **Excluded rows are declared.** Nulls, cancelled tickets, tickets that moved out mid-iteration — every exclusion appears in the report with its count.
7. **Every rendered timestamp carries a zone, and it is the board's.** Grep the finished HTML for `UTC` and for a bare `\d\d:\d\d` with no zone beside it. Each hit is either a machine-input literal you are deliberately showing — an APM `SINCE`, a quoted log line — or a defect. This is the cheapest check in the list and it caught nothing for as long as it did not exist.
8. **Every figure you could not measure is named as unmeasured — as a line, not as a section.** Walk the sections that depend on an outside source (production health, standing debt) and confirm each either carries a number you actually ran, or appears in the closing "What this could not measure" list. A source absent from both fails this check — and so does one that got a heading and a table in order to say that nothing was read.
9. **Nothing is drawn outside its own `viewBox`.** For every chart, take the minimum and maximum `x` and `y` you actually emitted — axis labels and value labels included, not just the bars — and confirm each falls inside `minX … minX + width` and `minY … minY + height`, with room under a text baseline for its descenders. This is a clip, so it produces no error and the chart still renders; the only way to catch it is to check the numbers. See step 4.

**Do it by re-deriving, not by re-reading.** Recompute a figure from the raw list in a scratch calculation and compare it to what the page says. Two independent paths to the same number is the check; reading the number twice is not.

## Step 6 — Deliver the report to the board

**Send the file's contents. The board stores the document and serves it back**, which is what lets the iteration's details pane show the report instead of merely linking to it.

```
jq -Rs '{retrospective:{html:.}}' <report.html> | jus api PATCH '/workspaces/{ws}/iterations/{iteration}/retrospective'
```

**MCP:** no MCP tool sends the report to the board, and `attach_file` takes only .md, .txt and .csv. The report stays in the chat.

⚠️ **Do not try to inline the document into the command.** It is an HTML file full of quotes, backticks and apostrophes — the two characters that break shell quoting — and tens of kilobytes long. `jq -Rs` reads the file as one JSON string and `jus api` takes the object on stdin, so there is nothing to escape by hand.

**The body is bounded and the endpoint says so.** Over the limit is a `422` naming the size; nothing is truncated and nothing is stored. If a report is genuinely that big, the charts are carrying embedded images they should not be.

### Publishing a hosted copy is optional, and it is not the record

**Where your harness can publish a hosted page**, publishing gives a reader a nicer viewer, and you record its address alongside the body:

```
jq -Rs --arg url '<address>' '{retrospective:{html:.,url:$url}}' <report.html> | jus api PATCH '/workspaces/{ws}/iterations/{iteration}/retrospective'
```

**MCP:** no MCP tool, as above.

**Where it cannot** — Codex, Cursor, Zed, Windsurf and Antigravity today — send the body alone. Nothing is missing: the board holds the report either way.

⚠️ **A hosted page is not a substitute for the body, and this is the trap the step exists for.** Hosted documents are commonly private to whoever they were shared with, and commonly refuse to be embedded with `frame-ancestors 'self'`, so a link to one opens nothing for a colleague and frames nowhere at all. Sending only a `url` leaves the board with an address it cannot show and most readers cannot open.

⚠️ **Never improvise an address you did not publish.** If you cannot publish, say so in one clause and send the body.

⚠️ **Publishing sends the iteration's contents to a hosted page.** Ticket titles, participant names, and anything you quoted from the testimony — which since step 1 includes ticket comments, and the human-authored ones are the likeliest to name a person, a device or an opinion. **Say so before you publish, not after** — and never publish a board you were not asked to retrospect.

### The report is served under a strict policy — write it accordingly

The board serves your document from its own origin, in a sandbox that permits its own `<style>`, one Google Fonts stylesheet and `data:` images, and nothing else. **Script never runs.** So:

- Inline SVG charts, as step 4 already says. Nothing to load, nothing to execute.
- No `<script>`, no interactivity, no analytics.
- No remote images or stylesheets beyond the font link. Embed anything you need as a `data:` URI.

A report that ignores this is not rejected — it renders with the offending parts silently inert, which is the worse outcome. Write it self-contained and the question does not arise.

## Step 7 — Write the record back

```
jus api POST '/workspaces/{ws}/iterations/{iteration}/comments' '{"comment":{"body":"…","context":"retrospective"}}'
```

**MCP:** no MCP tool writes an iteration comment. Give the person the text to post on the iteration in the app.

`context` is what marks this comment as the retrospective rather than a note, and the board uses it to render the entry differently. **Set it. Nothing else will.**

**The body is short, and it is exactly four things:**

1. One paragraph: what the iteration delivered and the single thing most worth knowing.
2. Where the report is — the board serves it from the iteration's details pane, so say that, and give the hosted address too if you published one.
3. The follow-ups you are suggesting — numbered, and nothing filed. See step 8.
4. Anything you could not compute, and why.

### Re-running replaces; it does not append

**A second retrospective comment on the same iteration is wrong.** Find the existing one and update it:

```
jus api PATCH '/workspaces/{ws}/iterations/{iteration}/comments/{comment}' '{"comment":{"body":"…"}}'
```

**MCP:** no MCP tool, as above.

The body is versioned, so the previous text survives. **Re-sending the report replaces it too** — the board keeps one per iteration — so the record stays correct without being touched.

## Step 8 — Propose the follow-ups; do not file them

A retrospective that surfaces nothing to do is a summary. But **the filing decision is not the analysis's to make.** These would be the least-reviewed tickets on the board — written by an agent, at the end of a long run, about work nobody has re-read — and they arrive already prioritised, above tickets someone placed deliberately. Measured on the first real run of this skill: three landed in an empty backlog and became its first three entries, unasked for.

**So propose them, numbered, in the record.** ⚠️ **Do not file them.** Not "except the obvious one", not "just the urgent one". A suggestion costs a line; a ticket costs a place in someone's queue.

### The format

Under a **Suggested follow-ups** heading in the record:

| # | Suggested title | Type | Pts | Why, and what it rests on |
| --- | --- | --- | --- | --- |
| 1 | … | chore | 2 | … |

**Number them.** The approval names a subset by number, and an unnumbered list can only be answered "all" or "none". Say in the same section, plainly, that nothing has been filed.

Keep saying what you considered and rejected, and why — that judgement is the same one, and it is worth more than the list.

⚠️ **Do not suggest one per observation.** Group them. Five that all say "this doc was out of date" is noise; one that says "the reference and the code have no guard holding them together" is the finding.

### Once approved

The approval arrives as a **reply on the record comment**, which dispatches an agent with that thread as its context.

File **exactly what the reply approves** — no more, and nothing you have thought of since — under the normal rules in `ticket-workflow`: a description and an estimate on each, at the **bottom of the backlog** unless one is genuinely urgent. Then reply in the same thread naming what you created, by id, and update the record's table so each approved row carries its ticket id.

⚠️ **An ambiguous approval is a question, not a licence.** "yes, file those" against a list of six is clear. "yes, the doc ones" is not — ask in the thread rather than deciding which two you meant.

⚠️ **A suggestion with no id is one nobody can connect back to why it exists**, which is what naming them in the record is for. The same is true in reverse: a ticket the report does not explain is one nobody knows the reason for.

## Reflexes

| If | Then |
| --- | --- |
| The iteration thread is empty | Normal. Take the testimony from the ticket comments and say nothing about where it came from |
| The ticket comments are empty too | *Now* it is a finding. Say the report is reconstruction only |
| The comment corpus is enormous | Expected — slice it. Human-authored first, then the tickets the report discusses |
| Your ticket list is smaller than `done_tickets_count` | You are missing the markers. Re-fetch with `include_markers=true` |
| `.meta.excluded_markers` came back `null` | The server predates it. That is not a zero — run check 5 |
| You are about to write "best", "second-highest", "worst" | Rank over every iteration, name the metric, name the ones above |
| You have a `rejections_count` | Read each event and classify it before reporting a rate |
| A timestamp is missing from the payload | Fall back to the activity feed, and say what it cost |
| You are about to render a time | Board's zone, zone abbreviation beside it. UTC only for a machine-input literal |
| `.workspace.timezone` came back `null` | You asked the workspace row. Ask `/summary`, which serves the **effective** zone |
| You are about to write "the stakeholder" or "the agent" | Use their name. You fetched it |
| You notice most tickets are bot-authored, or that one person accepts everything | Not a finding. One clause the first time, then never again unless it changed |
| You cannot reach the APM, the error tracker or the debt tooling | Name the source you could not reach. Never omit the section, never substitute a lookalike, never guess the number |
| A section would have nothing in it | Say so in a line. An empty section honestly labelled beats a manufactured one |
| A metric needs a null-heavy column | Count the nulls, exclude them, report the count |
| The classification is ambiguous | Show the judgement and name the tickets |
| A chart needs a library | Make it a table |
| You cannot publish a hosted page | Send the body anyway — the board serves it. Say you did not publish |
| You already ran this iteration | Update the existing comment, never post a second |
| The iteration is tiny | Write a short report and say it is short |
| You found something worth doing | Suggest it, numbered, in the record. Do **not** file it |
| A reply approves some of the suggestions | File exactly those, bottom of the backlog, then say in the thread what you created |
| A reply approves them ambiguously | Ask which, in the thread. Do not pick |

## Related skills

- **`ticket-workflow`** — the lifecycle, estimation, labels and the `jus` CLI / API reference. Filing the step 8 suggestions, once a reply approves them, follows it.
- **`hard-rules`** — the non-negotiable guardrails, including the rule that every piece of work has a ticket.

Referenced files: 2 — file references are metadata, not package contents.

ticket-workflow8.92 KB

View saved version →

---
name: ticket-workflow
description: The single load-bearing Juscribe SOP skill — the full ticket lifecycle from pickup to delivery, plus estimation, ticket types, labels, metadata, testing gates, and the complete `jus` CLI / API reference in bundled files this skill points at. Use when working any ticket — picking one up, transitioning state, investigating, sizing, labeling, writing tests, running pre-commit gates, calling `jus api`, committing, self-reviewing, finishing, delivering, handling rejections, processing batches, or resolving dependency blockers. Auto-invoke whenever a ticket ID (`#N`) or "work on this ticket" / "pick up backlog" / "deliver" / "rejected" appears. In a project wired to Juscribe (a `.jus/` directory or the `jus` CLI), bare ticket language — `#42`, the board, the backlog, deliver — means Juscribe tickets by default, unless the wording names another system (a PR, a GitHub issue, another tracker's key), so this skill is the one to invoke for them.
allowed-tools: Bash(jus *), Bash(git *), Bash(make *), Bash(cd *), Read, Grep, Glob, Edit, Write
license: MIT
---

# Ticket Workflow — Juscribe Lifecycle SOP

> **This skill and `.jus/SOP.md` overlap deliberately.** `jus init` appends that file to your AI context so a project with no plugin still has an SOP. This skill covers the same lifecycle in more depth and is kept more current: where the two differ, this one wins.
>
> Read it when working any ticket. It is the **single load-bearing skill** for Juscribe work. This file is the order of the lifecycle and, at each step, the reference file to open before it. What each step requires sits in that file, and so do the `jus` API, estimation, types, dependencies, subtasks and conversion: none of it costs anything until you open it. The companion [`hard-rules`](#related-skills) skill carries the must/must-not. Harnesses that run the jus hooks enforce the worst of those deterministically; everywhere else they are prompt-level only.

The lifecycle, in one line:

```
(1) create or pick up → (2) start → (3) investigate → (3b) apply labels → (4) code → (5) commit → (6) self-review → (7) finish
```

**Finishing is your last transition.** Deliver only when a person asks for it; Phase 6 says why.

Every change goes through every phase, however small or ad-hoc. Transition at the natural moment, never in a batch, so the board shows reality. **NEVER transition to `accepted` or `rejected`** — only the stakeholder decides. The one exception is a chat app on Juscribe's hosted connector: when the person explicitly asks you to accept or reject a ticket, record their decision with `accept_ticket` or `reject_ticket`, and never on your own judgement.

## Phase 0: Prerequisites — the `jus` CLI must be installed and authenticated

This SOP drives the board through the **`jus` CLI**, which the bundle does not install: the user installs it from juscribe.ai, then `jus login` or `jus init` (which also sets the `{ws}` used throughout this skill). **When a `jus` command fails before it reaches the board, open [references/setup.md](references/setup.md).** A missing CLI, a missing token, a retired token and an unreachable server each have a different fix, and only one of them is a rotation. `jus doctor` checks that these skills loaded, which `jus whoami` cannot.

**Do not loop `jus` commands against an unconfigured CLI, or against a 401.** Surface the one setup step the error points to, and stop.

**No shell, but Juscribe's MCP tools are connected?** That is a chat app, or any AI tool with the Juscribe connector and no terminal. Use the tools: each recipe in these files is followed by an **MCP:** line naming the tool that does the same job, or saying none does and what to do instead. Every board rule here still applies. The commit, lint, test, branch and git steps do not, since there is no repository. **Never tell a chat user to install Homebrew or the CLI.**

## Each step, and the file to open before it

⚠️ **This file holds the order, not the steps.** What a step requires is in the file named at it, and nothing here repeats it. Open that file at the moment it names, not earlier and not from memory of another ticket.

### Phase 1–2: Pick up and start

**Before your first `jus` call on the ticket, open [references/pickup.md](references/pickup.md).** It carries the fetch, the checks before `started`, and the lock itself, which comes before you read any source file.

### Phase 3: Investigation & Labels

**Right after the `started` transition, before you read the codebase, open [references/investigating.md](references/investigating.md)** — how far to investigate, the labels that go on before any code, the start comment that must come before your first edit, the tests that ship with the change, and how every comment and description is written on the board.

### Phase 4: Coding

**Before each `git commit` on the ticket, open [references/committing.md](references/committing.md)** — when to commit, and the form that links the commit to the ticket.

### Phase 5: Self-Review

**Right after each commit, open [references/self-review.md](references/self-review.md)**, before you comment, transition or reply — the checks that run after a commit, the coverage of the diff, and a second client.

### Phase 6: Finish

**Before the `finished` transition, open [references/delivering.md](references/delivering.md)** — before you tick a box or write the delivery comment, not after. It carries the pre-delivery gate, the boxes, the delivery comment and its form, the one transition call, and what closing a project involves.

⚠️ **Delivering is the phase most often done from memory, and memory asks the wrong question.** The gate is not "is the code good" but "does this differ from what the ticket says", and only the file asks the second.

**Tick the boxes you met, before the delivery comment and the `finished` call.** Fetch the description, change `- [ ]` to `- [x]` on each acceptance criterion you met, and PATCH it back with the stakeholder's words otherwise verbatim. Tick only what you can answer one question for: **what evidence would I cite?** An unmet box means do not finish — leave it unticked and the ticket in `started`. The commands are in [Then tick the boxes you met](references/delivering.md#then-tick-the-boxes-you-met).

## At any step

| Read | Before |
| --- | --- |
| [references/api.md](references/api.md) | Constructing **any** `jus api` call. A wrong request shape hangs, no-ops or returns a bare 500 rather than an error, and the response envelope is not the JSON root. |
| [references/api-writes.md](references/api-writes.md) | Creating a ticket anywhere but the bottom of the icebox, or changing many at once. Placement belongs in the create, and a bulk call reports failure per item. |
| [references/api-queries.md](references/api-queries.md) | Listing or filtering tickets. The index refuses a filter name it does not know, and leaves markers out unless asked. |
| [references/estimation-and-types.md](references/estimation-and-types.md) | Sizing, typing or labelling a ticket, or filling in a sparse one. Also the pre-start metadata gate and the placement rules for a new ticket. |
| [references/dependencies.md](references/dependencies.md) | Recording that a ticket is blocked. One blocker per independently-clearing condition, and editing one means setting **both** `title` and `description`. |
| [references/subtasks.md](references/subtasks.md) | Writing steps someone performs. Steps are subtasks, never description checkboxes, and `completed` is not `/toggle`. |
| [references/converting.md](references/converting.md) | Converting a ticket to a project, or wondering whether a retype is what you actually want. Only a `research` ticket can be converted. |
| [references/formatting.md](references/formatting.md) | Putting a fence inside a list, or reading the reactions on a comment. The fence trap looks correct in the source. |
| [references/second-client.md](references/second-client.md) | Delivering a ticket that touches a second client — a mobile app, a CLI, a public API. Passing tests do not cover it. |
| [references/setup.md](references/setup.md) | A `jus` command failing before it reaches the board, or skills that seem not to have loaded. Each failure has a different fix. |
| [references/states.md](references/states.md) | Any transition other than `started` or `finished`, or one that was refused. A transition steps one state at a time, and cancelling needs a resolution. |

⚠️ **A pointer is not the content.** About to write a `jus api` call, a dependency, a subtask or a delivery comment? Open the file. Answering from this table is how a wrong request shape ships.

## Related Skills

- `hard-rules` — the non-negotiable must/must-not that overrides everything here (commit immediately, no lint suppression, stakeholder-verbatim descriptions, never deliver incomplete work, no `git push`, document discoveries), and which of them the enforcement hooks back deterministically. Where a plugin install prefixes skill names with `jus:`, invoke the prefixed form.

Referenced files: 16 — file references are metadata, not package contents.

Package details

Publisher declarations from the archived package. These are separate from our research and the live service's terms.

Package license
MIT
Package author
Juscribe
Keywords
See keywords

Declared capabilities

  • Read
  • Write

Package observed Nov 3, 2026.

Technical details
First seen
Oct 24, 2026 · 12:40 UTC
Last seen
Nov 3, 2026 · 18:00 UTC
Collection status
Collected

plugin_asdk_app_6ac438d753188191b22c34d46f4d8039

Download plugin data (JSON)