gadak · git:20260909.2b0056a · 2026-09-09 · sha256 b8c16f5b3a3c4ce4

gadak git:20260909.2b0056aB

Immutable. This exact content is served forever at /api/v1/blob/b8c16f5b3a3c4ce4.

---
name: gadak
description: >
  Local issue tracker, and a SQLite mirror of Jira/Confluence when a site
  exists. Answer questions from that mirror with SQL instead of the Atlassian
  API — it can join, aggregate and read history JQL cannot express. Use when
  the user asks what changed, who is working on what, why a ticket was
  reopened, what the wiki says, what is stuck or untriaged, how a release is
  shaped, about a backlog, or anything that would otherwise mean paging
  through Jira search — including when they have no Atlassian account and want
  a tracker on this machine (the built-in tracker). Use it before any write —
  creating an issue, attaching a file, editing a summary, label or priority,
  commenting, transitioning, assigning, writing a wiki page all go through
  gadak, not the API. When the user wants to see issues, do not paste a
  markdown table: open them with `gadak views open`. Filter on status_category
  and ids, never on display names. Changing gadak itself (theme, sync
  interval, projects) is `gadak config`.
---

# Asking the mirror

gadak keeps a local SQLite mirror of Jira (and optionally Confluence) at
`~/.gadak/gadak.db`. The origin is a Jira site, or — with no Atlassian account —
an in-process tracker (`gadak init --local`), or another machine's
`gadak serve` bound with `gadak init --pairing-code-stdin`. Reads never touch
the network, so queries are free and fast: prefer a query over asking the user
to look something up.

If `gadak doctor` reports `skill: stale`, run `gadak skill install` so this
file matches the binary you are running. An upgrade of a previous gadak
install is in-place; a file gadak did not write needs `--force`.

**Show, don't paste.** SQL answers; `gadak views open` presents. Render a
markdown table or other document artifact only when there is no UI to focus,
or when the user explicitly asked for a document. If they asked to *see* the
issues, put them on the app:

```bash
gadak views open --jql 'project = NMA AND statusCategory = "In Progress"'
gadak views open --keys 'NMA-1,NMA-2'
gadak views open NMB-140
gadak sql --no-header "select key from issues_full where status_category != 'done'" | gadak views open --keys -
```

`--keys` accepts comma or whitespace; `--keys -` reads stdin. First-seen order
is kept, so the SQL `ORDER BY` is what the list shows. `gadak sql` prints a
header row first — skip it with `--no-header` (or `tail -n +2`) or that header
becomes a key.
`--keys` cannot be combined with `--jql` or a view name.

`gadak open <KEY>` is the origin escape hatch (system browser to the issue's page on Jira or Linear).
`gadak views open` is the "open in gadak" verb (focus the running app or serve
tab). The names collide; the verbs do not.

First, check it is there and current:

```bash
gadak sync --if-stale 15m   # no-op when fresh; one incremental pass if older than 15m or last sync failed
gadak status --json     # counts, watermark, last_error, schema_version, custom_fields.mapped
```

`gadak sync --if-stale 15m` is the session preamble on a CLI-only host: it
returns immediately when every source is fresh, and runs one incremental
pass when a source is older than 15m or its last sync failed. A running
`gadak serve` keeps the mirror fresh on its own.

While a first full sync is still filling the mirror, read commands print one
stderr line (`first sync in progress: … — results are partial`) and
`gadak status --json` carries a `first_sync` object (`phase`, `fetched`,
`total`, `wiki_pending`). That is normal, not an error: work with what has
landed so far, say in your answer that results are partial, and do not start
a second sync — the running one owns the mirror until it finishes.

`last_error` means the last sync failed. An old `watermark` on a quiet project
is normal — treat it as "possibly behind", not broken. `watermark` /
`sync_count` / `last_error` are the issue-source row (Jira when it has run,
Linear when Linear is the only issue source that has; `sources.jira` /
`sources.linear` for per-source rows). If `gadak` is missing,
say so rather than guessing at answers. If `gadak` is on PATH but there is no
workspace yet and they asked for a backlog (or have no Jira), create a
workspace on the built-in tracker — do not invent a `TODO.md`
or a GitHub Issue.

After a context compaction, or when resuming someone else's session on an
existing workspace, the first command is `gadak recents`: the keys this
workspace read most recently, newest first. Reads record themselves —
`gadak issue` and `gadak search` append the visit/search rows to `local.db`
as they run, so the list is already there when you need it.

```bash
gadak recents                    # kind, key, viewed_at — TSV with a header
gadak recents --json --limit 50
```

When the question is how the work itself went — sessions, how long before a
session produced its first write, how old the in-progress tail is, what closed,
whether comments claim work is done on issues that are not — run `gadak retro`
before writing any status prose: it reads the mirror and `local.db`, prints one
ISO-week column per week with a change column, and prints the definition of
every row under the numbers, so a claim can be checked against the exact rule
that produced it. The table also carries wip age max (the oldest in-progress
issue, beside the p85) and cycle p50/p85 (how long the week's closures took,
never-reopened issues only). `gadak retro --json` emits the same numbers for
scripting, `--session-gap 45m` moves the read-gap that splits sessions (5m to
24h; the default gap is the `retro.sessionGap` config key, 30m unset), and
`gadak retro --open closed --week 1` opens the issues behind a cell
in the running app (with `--json`, prints the keys only; `--open cycle`
follows a week's cycle samples).

`gadak retro --by-sprint` cuts the same report by sprint window instead of
by ISO week — one column per sprint, named, with the running one marked — on
any origin that has sprints (Jira Software, Linear cycles, the built-in
tracker). It replaces `--since` rather than joining it, and every definition
under the table names the sprint instead of the week, so the footer still
describes the table on screen. With more than one board carrying sprints it
refuses and names them; pass `--board <id>`.

## Which origin you are talking to

Two questions, not one. Same CLI verbs either way.

**Which tracker is the origin** (`origin_type`):

- `jira` — an Atlassian Cloud site. Needs a stored credential.
- `linear` — a Linear workspace, when a `linear.apiKey` is configured.
- `gadak` — the tracker that ships with gadak (`issuetap`). No Atlassian
  account. The durable file is `origin/issuetap.db` under the workspace
  directory, not `gadak.db` — that one is still a cache you can delete.

**Where that origin runs** (`transport`):

- `local` — in this process. Only a `gadak` origin can be.
- `remote` — across a serve API. Every `jira` and `linear` origin is, and so
  is a `gadak` origin on another machine you paired with.

Writes always go to the origin first, then the mirror re-reads.

A paired workspace is the case worth remembering: `gadak, remote`. Its
origin is the built-in tracker one machine away, so writes work with no
Atlassian credential at all.

Detect both before you write:

```bash
gadak doctor --json     # workspace.origin_type and workspace.transport
gadak status --json     # the same pair, plus freshness and a pairing object when paired
gadak workspaces --json # name, active, configured, site_host, issues, …
```

`kind` is still there — `standalone` or `connected` — and still means what
it used to. It is the older, coarser field: it calls a paired workspace
`connected` even though its origin is the built-in tracker. Prefer
`origin_type` and `transport`; read `kind` only from a gadak too old to
send them.

A `gadak` origin row is `configured: true` with an **empty** `site_host`.
That is not a broken workspace. Do not report "no site" and stop, and do
not ask for an Atlassian token.

## Which mirror you are reading

Someone may have several: one per Jira site, kept apart as **workspaces**. Ask,
rather than assume there is one:

```bash
gadak workspaces --json   # name, active, configured, site_host, issues, documents, last_sync_at
```

`active` is the workspace the command you just ran used. Nothing else sets it —
there is no stored "current workspace" to switch. It comes from the command line
or the environment, both of which are visible in what you ran:

```bash
gadak --workspace work sql "…"    # this call only
```

`--profile` is an alias of `--workspace`; existing scripts and MCP installs
that pass `--profile` keep working. `gadak profiles` is the same command as
`gadak workspaces`. To remove a workspace entirely:
`gadak workspaces rm <name> --yes` — one on the built-in tracker additionally
needs `--destroy-origin` (its persist is the only copy of that tracker; the
refusal names the file to copy out first).

Three rules follow, and they are the point of the design:

- **Name the workspace in every command that matters.** Never rely on the
  ambient default when a question is about a specific site — a shell that was
  configured before you arrived can point anywhere, and your transcript will
  not record which mirror answered. The one exception is gadak's own terminal:
  it sets `GADAK_WORKSPACE` to the workspace its window shows, so a bare
  `gadak` there is on that workspace by construction — but the transcript
  still records nothing, so name `--workspace` on writes.
- **Say which mirror you read.** `gadak status --json` carries a `profile`
  field (the workspace name; empty for the root); quote it when the answer
  could differ per site.
- **Never write to one origin while reading another.** `gadak comment`,
  `transition`, `assign` and `api --write` all take the same `--workspace`. If
  the question came from data in one mirror, the write goes to that same one.

If a workspace shows `configured: false` it has no credential: a **Jira or Linear**
mirror can still be read if it was ever synced, but it will not be refreshed
and writes will fail. A **built-in-tracker** workspace is `configured: true` with
an empty `site_host` and no token — writes still work; they go to the local
origin, not Atlassian. If `site_host` is empty, read `workspace.kind` from
`gadak doctor --json` (or `kind` from `gadak status --json` when that field
is present) before assuming the workspace is broken. A row that says
`unreadable` is still a broken config — report it. If `site_host` is empty
**and** `configured` is false, there is no workspace yet — go to *A tracker of your own*
below.

## The one mistake that silently returns nothing

Filter on ids and categories, never on display names. Jira translates
`status.name` and `issuetype.name` per account, so a name that works on your
site returns zero rows on someone else's.

```sql
WHERE status = 'In Progress'          -- WRONG: empty on a localized account
WHERE status_category = 'inprogress'  -- RIGHT: stable everywhere
```

`status_category` is `new`, `inprogress`, or `done`. `status_id` and
`issue_type_id` are stable too.

## The schema in one paragraph

`items` is the source-neutral spine (title, `body_text`, timestamps).
**`issues` is the view to reach for** — the Jira projection joined onto the
spine, so it carries `summary`, `description_text` (`items.body_text`,
flattened), and every projection column in one relation; the intuitive
`SELECT key, summary, status FROM issues` just works (since 0.19.1;
`issues_full` is the same view under its older name, and on an older
mirror it is the one that has `summary`). Sprint is three columns there
(`sprint_id`, `sprint_name`, `sprint_state` — filter on id or state,
never the name) **and** a `sprints` table of its own (id, board_id, name,
goal, state, start_at, end_at, complete_at), with `boards` beside it —
join on `sprint_id = sprints.id`. A sprint holding no issues exists only
in that table, and so do its goal and its dates. `versions` is the project catalog; join it on
`fix_version_ids` (same-order ids next to the name array `fix_versions`).
`pages` is
the Confluence projection. `comments`, `attachments`, `changelog`, `links`,
and `dev_links` (development-panel PRs) hang off `items.id`. `items_fts` is
one FTS5 index over titles, bodies, and
comment text — issues and wiki pages together. `labels`, `components`, and
`fix_versions` are JSON arrays; reach them with `json_each`. Mapped custom
fields live in `issues.custom` (and `issues_full.custom`) keyed by alias —
only after `gadak fields --apply`. If `gadak status --json` shows
`custom_fields.mapped` of 0, an empty `json_extract(custom, '$.alias')` may
mean mapping has not run, not that the field is blank:

```sql
SELECT key, json_extract(custom, '$.story_points') AS sp
FROM issues_full WHERE json_extract(custom, '$.story_points') IS NOT NULL;
```

Personal state lives in `local.db` beside the mirror (ATTACHed as `local`;
you do not type ATTACH): `local.saved_views` (`gadak views save`),
`local.recipes` (`gadak recipes save`), `local.visits`, `local.searches`.
It survives deleting `gadak.db`.

Some columns exist only here, derived from the changelog while syncing:
`carryover_count` (how many times an issue was carried into another sprint
after the first — NULL, not 0, on an origin with no changelog),
`first_sprint_id` / `first_sprint_at`,
`reopen_count`, `reopened_at`, `reopen_reason`, and `epic_key` (the nearest
level-1 ancestor). Jira cannot answer questions about these at all.

Output modes: `gadak sql "…"` is tab-separated *with a header row*,
`--no-header` omits that row (TSV and `--csv`), `--json` gives one object per
row, `--csv` is header plus CSV.

## Queries that cover most questions

```sql
-- Someone's open work, most urgent first
-- assignee_email can be empty when the site hides emails; prefer assignee_id after looking it up by name.
-- gadak assign accepts that accountId (or the display name) when email is hidden; ambiguous names are refused with the candidates.
SELECT key, status, priority, summary FROM issues_full
WHERE assignee_email = 'dana@example.com' AND status_category != 'done'
ORDER BY priority_rank, updated_at DESC;

-- What regressed (reopens are the highest-signal quality metric here)
SELECT key, summary, reopen_count, reopen_reason FROM issues_full
WHERE reopen_count > 0 ORDER BY reopen_count DESC, reopened_at DESC LIMIT 20;

-- What keeps getting pushed to the next sprint (carryover_count is NULL, not
-- 0, on an origin that supplies no changelog — Linear)
SELECT key, summary, carryover_count, sprint_name FROM issues_full
WHERE carryover_count > 0 ORDER BY carryover_count DESC LIMIT 20;

-- What is stuck, and for how long
-- Same GDK-369 clock caveat on the built-in tracker for julianday('now').
SELECT key, status, ROUND(julianday('now') - julianday(status_changed_at), 1) AS days
FROM issues WHERE status_category = 'inprogress' ORDER BY days DESC LIMIT 20;

-- Has anyone hit this before? (descriptions, comments AND wiki pages, one index)
-- ref is the issue key, or for kind='page' the origin page id (items.key —
-- the id `gadak page get` prints and `gadak page edit` takes; NOT
-- pages.item_id, an internal join id).
SELECT it.kind, COALESCE(i.key, it.key) AS ref, it.title, p.space_key
FROM items_fts f JOIN items it ON it.rowid = f.rowid
LEFT JOIN issues i ON i.item_id = it.id
LEFT JOIN pages  p ON p.item_id = it.id
WHERE items_fts MATCH 'webhook AND retry' LIMIT 20;

-- What moved this week, and who moved it
-- On the built-in tracker, the origin clock may not match the wall clock (GDK-369);
-- this window can be empty. Do not rewrite the predicate.
SELECT c.at, c.author, c.field, c.from_value, c.to_value, i.key
FROM changelog c JOIN issues i ON i.item_id = c.item_id
WHERE c.at > datetime('now', '-7 days') ORDER BY c.at DESC LIMIT 50;

-- Untriaged: nobody on it, no priority set
SELECT key, created_at, summary FROM issues_full
WHERE status_category = 'new' AND assignee_id IS NULL AND priority_rank = 0
ORDER BY created_at LIMIT 30;

-- Everything one person wrote, across issues and pages
SELECT it.kind, it.title, substr(c.body_text, 1, 80), c.created_at
FROM comments c JOIN items it ON it.id = c.item_id
WHERE c.author = 'Dana Whitfield' ORDER BY c.created_at DESC LIMIT 20;

-- Work in the active sprint (sprint_id / sprint_state are the stable keys;
-- sprint_name is a localized display name — never filter on it)
SELECT key, summary, sprint_id, sprint_name
FROM issues_full
WHERE sprint_state = 'active' AND status_category != 'done'
ORDER BY priority_rank, updated_at DESC;

-- What blocks one issue (links are stored from both ends: the blocked issue
-- carries the inward row, the blocker the outward one)
SELECT l.target_key AS blocked_by, t.status_category
FROM links l
JOIN issues_full i ON i.item_id = l.item_id
JOIN issues_full t ON t.key = l.target_key
WHERE i.key = 'NMA-24' AND l.type = 'Blocks' AND l.direction = 'inward';

-- Everything under one epic (epic_key is derived: nearest level-1 ancestor)
SELECT key, status_category, priority_rank, summary FROM issues_full
WHERE epic_key = 'NMB-194' ORDER BY status_category, priority_rank, key;
```

Blockers both ways, duplicate networks, open work per component or per epic,
overdue-not-done, comments keyed on `author_id`, and @-mentions read out of
the ADF body are worked queries in `docs/RECIPES.md` ("Blockers and
duplicates", "Epics, components, and due dates").

## Listing work: list, ready, next

`gadak list` is the default open-issues read — run it before writing any
query. Every issue with `status_category != 'done'`, highest
`priority_rank` first, `updated_at` newest first as the tiebreak, 30 rows.
Both `priority` and `priority_rank` are in the output so the ordering can
be checked without a second query. `--limit N`, `--json`, `--csv`,
`--no-header` behave like `gadak sql`; `--all` includes done issues.

`gadak ready` (or `gadak list --ready`) narrows that list to issues no
*recorded* open blocker holds back: an inward blocking link whose target
issue is not done disqualifies. That is the `open_blockers` column (a
mirror read, no origin call) — which link types block resolves through the
mirror's `link_types` catalog, the same vocabulary `gadak link --type`
resolves, never a hardcoded name. Block links are typed far less
consistently than epic links (Relates is the default), so "ready" means
nothing recorded blocks it, not that nothing does. On a mirror whose schema
predates the column, a
stderr notice says so and the plain open list is shown; an empty "nothing
ready" would be a stronger and wronger claim.

```bash
gadak list                          # open issues, priority rank first
gadak list --limit 5 --json
gadak list --all                    # include done
gadak ready                         # open issues nothing open blocks
```

A recipe is a name for a mirror SQL query, stored in `local.recipes`. It is
not a ranking engine — order comes from `priority_rank` / `status_category`.
`gadak next` (alias `gadak pick`) runs the recipe named next when one is
saved; with none saved it runs the `gadak list` default and prints the save
command on stderr. This is a report, not occupancy — claiming still goes
through the origin (`gadak claim`).

```bash
gadak recipes save next "select key, priority_rank, status, round(julianday('now') - julianday(status_changed_at), 1) as age_days, summary from issues_full where status_category != 'done' order by priority_rank, updated_at desc limit 10"
gadak next                          # saved recipe, or the built-in default
gadak pick                          # same command, the changelog's name
gadak recipes run next --json       # the runner: still an error when unsaved
gadak recipes show next             # SQL text; pipeable into save
gadak recipes show next | gadak recipes save next -m -
gadak recipes                       # name, updated_at, sql preview
gadak recipes rm next
```

### When asked what to do next

Run `gadak ready` (`gadak next` when a recipe is saved); the rows carry
`age_days`, days in the current status. Answer with facts about the issues,
in the order the list gives: key, summary, `age_days` as "in progress for N
days" — "waiting N days" when `status_category` is `new`. For the top pick,
say whether anything blocks it (`gadak ready` already excludes blocked
issues; when it degraded — the stderr notice — say so) and who is waiting
on it: the "What does this issue block?" query in docs/RECIPES.md, keyed on
link direction. Do not prescribe — the subject of every sentence is an
issue, never a person; no ranking of people, no praise. The answer carries
age, blockers, and who waits, and nothing else (docs/project/THEORY.md,
G1/G10).

`gadak search "…"` is the shortcut when a query is overkill; `--json` includes a
`pages` array, and every hit says which field matched. If the user pastes Jira
JQL or a navigator URL, use `gadak search --jql '…'` (or pass the URL as the
query). Clauses the subset cannot express are printed on stderr and must be
repeated to the user — do not pretend the list is what Jira would have shown.

CJK queries of two or more runes match inside a compound (`결제` hits
`간편결제`). English middles still miss (`ency` does not hit a title that is
only `idempotency`). JQL `project NOT IN (KEY, …)` applies; `status NOT IN`
does not (`cannot apply JQL — status not in … (only = and IN)`). Prefer
`status_category` / `status_id` in SQL — never `status = 'In Progress'`.

To put the human on a view, do not describe the filters — set them:

```bash
gadak views                         # Jira filters (after sync) + saved views
gadak views open "the name"         # focuses the running desktop app or serve tab
gadak views save "Night triage" --jql '…'   # keep a named view in local.db (survives deleting the mirror)
```

`views open` writes a one-shot hash the UI applies (`ks=` for `--keys`,
`issue=` for a positional key); it also opens a serve tab when one is
listening, and focuses Gadak.app on macOS (the `--workspace` is passed through
so the window and the file match). `--no-open` writes the hash only. `--json`
prints the hash and where it was sent. Confirm you named `--workspace` if the
user has more than one mirror. `gadak open` is the origin (Jira/Linear) escape
hatch; `gadak views open` is open-in-gadak.

## A tracker of your own: a backlog you may create and own

When work needs tracking and no Jira exists — a personal project, this
machine, or your own plan as an agent — create a workspace gadak keeps.
One command, no token, no site:

```bash
gadak init --local --json
gadak sync                          # first fill; in-process, no network
gadak create "first ticket title" -m "why this exists"
gadak views open <KEY>
```

`init --local` is non-interactive. Do not combine it with `--site`,
`--email`, or a token. It seeds project `STD` and wiki space `LOC`, and
records the default issue type, so a summary-only `gadak create` is enough.
The record is `origin/issuetap.db`, not the mirror — `gadak backup --to <dir>`
copies it to one file while serve runs, and prints the path.

Use a dedicated `--workspace` and name it in every command, so the backlog
never mixes with a Jira mirror:

```bash
gadak --workspace plan init --local --json
gadak --workspace plan create "Ship the uploader" -m "found while refactoring"
gadak --workspace plan views open <KEY>
```

If the active workspace is already a Jira Cloud site and the user asked
for a backlog of this repo (not of that site), do not file there. That write
is visible to their whole team. Init the built-in tracker under a new workspace instead.

Do not invent a `TODO.md`, a markdown checklist, or a GitHub Issue when
`gadak` is on PATH. The window is how the human sees the same backlog.

The durable record is `<workspace-dir>/origin/issuetap.yaml` — that file is
the backup target, not `gadak.db`. `gadak doctor --json` reports it as
`workspace.persist` (and top-level `origin`). Running `init` with a site on
the built-in tracker deletes issues that originated there; warn before
converting.

**Write freely here.** Confirm-before-writing applies to Jira or Linear
workspaces because the team can see the issue. A built-in-tracker write is a file
on this machine: create, edit, comment, and transition without asking, then
report what you filed (keys and one-line summaries).

Everything else in this file works identically: `status_category`,
`issues_full`, `reopen_count`, `views open`.

### Leaving a tracker: migrate a mirror into a workspace gadak keeps

`gadak --workspace <new name> migrate --from <workspace>` exports the
source workspace's mirror — issues, comments, history, links, attachment
bytes, wiki pages — into a brand-new workspace on the built-in tracker and ends with a
source-vs-migrated count table (derived columns like `reopen_count`
included). The source is read-only throughout and keeps working. The
target must not exist yet; migrating never rebinds an existing workspace.
`--projects A,B` / `--spaces X,Y` narrow the set, `--skip-attachments`
skips the byte downloads. Bodies migrate as the origin's ADF, formatting
included; the report counts the code blocks / tables / inline media that
arrived (an inline image resolves by filename against its attachment).

The second destination is Linear: `gadak --workspace <linear workspace>
migrate --from <workspace> --to linear --team <KEY>` writes the issues into
that team through the Linear credential of the workspace the command runs
in (no new workspace; `gadak sync` fills its mirror afterwards).
`status_category` picks the workflow state by type, `priority_rank` becomes
0-4, issue types and labels become team labels, parents and links become
sub-issues and relations, comments carry `author · time` in the body.
Re-running is safe: every created issue ends with a `gadak-migrate: <KEY>`
footer and a match is skipped. Not migrated, and said so in the report:
history (Linear has no changelog write API), wiki pages, attachment bytes
(linked by URL), real authorship. `--dry-run` prints the mapping and counts
with no network call; `--limit N` takes the first N issues.

### Pointing at another workspace's issue

On the built-in tracker, local or paired, `gadak ref <KEY> <workspace>/<TARGET>`
records a pointer at an issue in a different workspace — a personal note
that names a team ticket, without writing anything to that team's tracker.
`gadak ref <KEY> --list` prints each pointer with the target's **current**
status and assignee, read from that workspace's own mirror on this machine
(no network). A target this machine does not mirror still lists — it just
says so. `--rm <id>` takes one back. The same references appear on the
issue in the app. A Jira Cloud workspace refuses the verb: the pointer
would be visible to that whole site.

## Pairing: the origin is another machine's serve

Home (the built-in tracker, with `gadak serve` running) mints one offer per device.
The remote binds a *fresh* workspace. After that, every verb on that workspace
uses the home serve as origin. `--endpoint` is the URL the remote reaches the
home serve at (the tailnet URL); without it mint takes the serve's own address
and refuses when that is loopback (exit 64) — show that error, it may already
carry the completed command.

```bash
gadak pairing mint --label laptop --endpoint https://<machine>.<tailnet>.ts.net   # home: stdout is one offer line
gadak --workspace laptop init --pairing-code-stdin  # remote: paste the offer
gadak --workspace laptop status                     # kind is connected; prints paired with "laptop"
gadak pairing list                                # home: token table; remote: one status line
gadak pairing revoke laptop                       # home only
```

Do not combine `--pairing-code-stdin` with `--local` or a site token.
`_home` is this machine's routing token, not a device — `revoke` refuses it;
`gadak pairing mint --label _home` rotates it. If a command fails with a
`pairing:` prefix, show that error to the user. Do not invent a retry.

## Writing as yourself: the actor

On the built-in tracker, local or paired, every write records who made it. Set
your identity before the first write so comments and transitions attribute
to you, not the workspace's default user:

```bash
export GADAK_ACTOR="claude:354bff2b|Claude (build 1)"   # slug | display name
gadak status          # the actor row confirms recognition (--json: actor.slug, actor.source)
```

Claude Code is detected automatically — no export needed; each session
writes as `claude:<session prefix>`. A slug is a stable identity: pick one
per agent and keep it across sessions. The machine's fallback lives in
`gadak config set actor '{"slug":"grok:aa11","name":"Grok"}'` (the env
value wins over it). On the built-in tracker the actor is the recorded
author of the write. On a Jira or Linear origin — where the write goes out
under a person's credential — agent-authored comments and new issues
instead carry one trailing line, `— via gadak · <actor>`, on by default and
off with `gadak config set actor.trailer false` (`gadak status` shows the
switch on the actor row). Without an actor,
writes attribute to the workspace's default user, exactly as before.

The attribution is queryable — "what did the previous session leave" is
one query, not archaeology across four tables. Every write surface carries
`author_id` (`items` for created issues and pages, `comments`, `changelog`,
`page_versions`), so filter each on the actor slug and union the results;
the ready-made timeline query is in `docs/RECIPES.md` under "What a
session left". The current session's slug is `actor.slug` in
`gadak status --json`.

When several agents work one backlog, claiming is a write, not a comment
convention: `gadak claim <KEY>` takes an issue as yours — assignee plus the
in-progress transition in one step — and refuses (exit 75, the holder's
name in the error) while another actor holds it. Use it instead of a
"[claim]" comment; `--take-over` replaces the holder only when the human
says to. On a workflow where two transitions land in progress the bare
claim refuses with both named — pass `--transition <id|name>` to choose
(it must land in progress; anything else is `gadak transition`). `gadak issue KEY` answers "how long has this sat?" with its
`durations` line (wait = created → first in-progress, progress = in-progress
→ done or now), computed from the changelog — never stored.

## One issue, and writes

On the **built-in tracker**, `init` seeds project `STD` and records a default issue type,
so a summary-only create is enough. On **Jira**, use a key and project
that exist on that site (the `NMB-140` lines below are an example, not a
universal project). A paired workspace is `connected` with no default project
or type — `create` will ask for `--project` / `--type` until you set them.

```bash
# built-in tracker (seed project STD)
gadak create "first ticket title" -m "why this exists"
gadak issue STD-1 --json
gadak comment STD-1 -m "Reproduced on staging."
gadak assign STD-1 you@example.com            # the one seeded user; example.com emails from the Jira examples do not exist here
gadak claim STD-1                             # take it as yours: assignee + in-progress transition; refuses while another actor holds it (exit 75)
gadak dev link STD-1 --pr https://github.com/org/app/pull/7   # opened a PR? record it right here
gadak dev scan                                                # or sweep the repo: keys in PR titles/branches → links

# Jira: a key that exists on that site
gadak issue NMB-140 --json                    # fields, description, comments, history
gadak issue NMB-140 --editmeta                # which configured fields this issue can edit (origin GET; not stored)
gadak issue NMB-140 NMB-141 --json            # JSON array of the same documents; omit --json for text with --- KEY --- between them
gadak sql --no-header "select key from issues_full where parent_key='NMB-140'" | gadak issue --keys -
gadak issue NMB-140 --derive                  # why reopen_count / resolved_at / epic_key are what they are
gadak comment NMB-140 Reproduced on staging.  # positional body
gadak comment NMB-140 -m "Reproduced on staging."
gadak comment NMB-140 -m "thanks @Dana"       # @Name resolves to a site user; ambiguous names are refused
gadak comment NMB-140 -m -                    # body from stdin, for anything multi-line
gadak comment NMB-140 -m "done" --visibility role=Administrators
gadak comment NMB-140 -m "done" --internal    # JSM internal
gadak transition NMB-140                      # list tokens this credential can fire
gadak transition NMB-140 "In Review"
gadak transition NMB-140 done                 # status category: new | inprogress | done
# Prefer the name or the category token over a number: on the built-in tracker
# transition ids (1..n) and status ids (3, 10000, …) overlap, so a bare number
# is refused as ambiguous — the refusal names both readings.
# A category token folds destinations you could not have told apart: two
# statuses with the same display name in the same category count as one, and
# the one the project actually uses wins. Two destinations with *different*
# names still refuse and name every reading, including the folded ones —
# `gadak transition NMB-140` lists them before you try to write.
gadak close NMB-140                           # same as transition KEY done; already done is a no-op
gadak transition NMB-140 done --resolution "Won't Do" -m "fixed in 1.2"
gadak assign NMB-140 dana@example.com         # email, display name, or accountId; `-` unassigns. Ambiguous names are refused with the candidates.
gadak claim NMB-140                            # Cloud has no atomic claim: assignee + transition run as two calls (warned on stderr); held issues refuse with exit 75
gadak claim NMB-140 --take-over                # replace the current holder
gadak create Batch worker drops the last page --project NMB --type Bug -m "repro on staging" --parent NMB-1
gadak create Severity required --project NMB --type Task --field severity=High
gadak attach NMB-140 screenshot.png trace.log
gadak attach get NMB-140 trace.log             # the read half: bytes to ./trace.log. `gadak issue KEY` lists the names; --out takes a directory, a path, or - for stdout. Works on every origin — do not parse issues.raw for an id and hand-build a REST path for `gadak api`.
gadak edit NMB-140 --summary "…" --label +regression --label -needs-triage --priority High --parent none
gadak edit NMB-140 --type Task                # name, localized name, or id — same resolver as create --type
gadak edit NMB-140 -m "## Repro\n\n- step one"  # markdown replaces the body; a text with no placeholders over a body that has panels/media/mentions refuses without --force-plain
gadak issue NMB-140                            # a formatted body prints with placeholders: <!-- adf:1:… panel info --> … <!-- /adf:1 -->, <!-- adf:3:… mention @Dana -->
gadak edit NMB-140 -m -  < edited.md           # keep the markers: each node goes back where its marker stands; delete a marker to remove that node (stderr says which); a stale marker is refused
gadak issue NMB-140 --json | jq .description_adf > body.json && gadak edit NMB-140 --adf-file body.json   # the raw path: the ADF back as it is, no guard (comment --adf-file too)
gadak edit NMB-140 --component +SDK --component -Docs
gadak edit NMB-140 --fix-version +v2.5 --fix-version -10012
gadak edit NMB-140 --field severity=High
gadak link NMB-140 NMB-141 --type blocks          # A blocks B; "is blocked by" means A is blocked by B
gadak unlink NMB-140 NMB-141 --type blocks        # removes that link (live id lookup; the mirror keeps no link ids)

gadak create --batch -                        # one JSON object per line on stdin (stops at the first failure)
gadak comment --batch -                       # JSON lines {"key","body"}; tries every line; one envelope row per key
gadak transition --batch -                    # JSON lines {"key","target"}; --dry-run writes nothing (--json adds transition_id)
gadak assign --batch -                        # JSON lines {"key","assignee"}; "-" unassigns
gadak edit --batch -                          # JSON lines {"key"} plus summary, labels (+x/-x), type, priority, due, parent, fields
gadak fields --apply                          # map in-use custom fields, then edit --field alias=value
gadak project create IDEA --name Ideas        # grow a built-in-tracker workspace by a project
gadak transition NMB-140 done --field environment=staging
gadak search NMB-140 --explain                # why each hit ranked: key-exact, key-prefix, or fts
```

`--batch -` on comment, transition, assign, and edit reads one JSON object per stdin line (at most 50). A line that fails does not stop the rest; stdout is one envelope row per line (`key`, `ok`, `changed`, `error`; `--json` is JSON lines). `create --batch -` still stops at the first failure.

Custom-field writes follow this order: `gadak fields --apply` (save aliases) →
`gadak issue KEY --editmeta` (which of those aliases this issue can edit) →
`gadak edit KEY --field alias=value` or `gadak create … --field alias=value`.

`gadak comment` resolves `@Name` to a site user (account id) before sending.
Ambiguous names are refused with the candidates and no comment is posted. A
name that matches nobody stays plain text and is named on stderr; stdout stays
pipeable.

Wiki pages — read from the local mirror (no network), write through the
origin (Confluence, or the built-in tracker's wiki). The seeded space
is `LOC`; Jira: a space key that exists on that site. `gadak wiki` runs
every one of these (`gadak wiki get <ID>` = `gadak page get <ID>`):

```bash
gadak page list                                  # id, space, title, updated_at — newest first; where <ID> comes from
gadak page get <ID>                              # title, body, comments from the mirror
gadak page get <ID> --json
gadak page create --space LOC --title "Retention notes" -m "first draft"
gadak page edit <ID> --title "Renamed"
gadak page comment <ID> -m "a question"
```

Writes go to the origin, then the issue (or page) is re-read into the mirror.
On a **Jira** Cloud workspace the origin is Jira — a create, comment,
or transition is visible to their whole team; confirm first.
On the **local built-in tracker** the origin is this machine. File without
asking, then report the keys and one-line summaries.
On a **paired** workspace (`status --json` has `pairing`), writes go to the
home serve, not Atlassian. Its mirror carries the home origin's wiki too
(every team space, personal excluded); if `status --json` says the wiki pass is skipped as
not configured, the workspace was paired before that was the default —
`gadak config set wiki.enabled true` turns it on (`wiki.*` is the alias the
CLI takes for the stored `confluence.*` paths; stderr names the stored one).

Never write to the SQLite file. A row written directly is destroyed by the
next sync, on either kind.

Discover flags from the binary, not from memory: `gadak <verb> --help` lists
what that verb accepts, and `gadak issue KEY --editmeta` lists which configured
custom fields this issue can edit. If a field still is not there, say so rather
than reaching for the REST API — `gadak api` exists for that, but it is an
escape hatch, not the path of least surprise.

## Leaving and finding memory

A page is where a session leaves what the next one should not have to
rederive. Pages and issues share one index — `items_fts` covers both kinds —
so retrieval is the search this file already teaches: the unified query under
*Queries that cover most questions* above, or `gadak search '<keyword>'`,
which returns issues and pages together.

The loop, three commands:

```bash
gadak page create --space LOC --title "retry backoff — findings" -m - <<'EOF'
Base 250ms, factor 2, cap 8s, jitter mandatory. Measured on the upload
path; the flat 1s retry lost large uploads.
EOF
# → 20001  retry backoff — findings   (stdout is the page id, then the title)

gadak comment STD-1 -m "Findings: /wiki/spaces/LOC/pages/20001 — backoff measured"
gadak search "backoff"        # next session: the issue and the page both return
```

The comment is what ties page to issue: `item_refs` is rebuilt from comment
text on every write, and a comment links a page **only when it carries the
page's URL** — `/wiki/spaces/<KEY>/pages/<ID>` or `pageId=<ID>`, the two
shapes `item_refs` recognizes (the linked page then shows under `ref_pages`
in `gadak issue KEY --json`). A bare page id in a comment does not link —
unlike a bare issue key, which does.

**Agent memory has its own verb.** When the intent is "leave it so the next
session finds it", `gadak memory add '<note>'` is the correct call: it
writes a page into the memory space through the same origin path as
`page create`, derives the title from the note's first line, and reports
`id → title → space`. `gadak memory search '<text>'` scopes the search to
that space alone. The space is the `memory.space` setting: the **built-in tracker**
defaults to the seeded `LOC`; **Jira refuses until it is set**
(`gadak config set memory.space KEY`) rather than guess a team-visible
space — ask the user which space before the first add. To extend an
existing note instead of starting a new page, `gadak page edit <ID>
--append -m '<text>'` grafts paragraphs onto the current body and keeps
whatever formatting is already there.

**Leave a page when the finding outruns the issue**: an investigation whose
result a future session would otherwise redo, a decision together with its
why, a map of something the mirror cannot derive from its own rows. What the
issue itself already records belongs in a comment — and so does a one-line
fact; a page that would hold one sentence is a comment wearing a title.

Confirm-first reaches pages exactly as it reaches issue writes: on a
**Jira** workspace a page is visible to the whole team — confirm before
creating or editing one, and use a space key the user names, never a guess.
On the **built-in tracker** the seed space is `LOC` and writes are free (see
*A tracker of your own* above).

## Workspace settings

An agent configures the workspace through the CLI. Do not hand-edit
`~/.gadak/config.json` and do not drive the Settings dialog.

```bash
gadak config list                         # every editable path, current value, one-line description
gadak config list --json
gadak config get appearance.theme
gadak config set appearance.theme dark
gadak config set syncIntervalSec 30
gadak config set features.feed true
gadak config set projects '["NMB","NMA"]'
gadak config set devStatus true
```

`--json` on `list` and `get` (and `set`, which prints the stored value).
Unknown paths exit 64 and print the valid list. `set` accepts JSON or a
bare scalar (`dark`, `true`, `30`); arrays and objects need JSON.

Credentials (site, email, token) stay on `gadak init` — they are not
`config set` paths. `gadak config list` says so.

`appearance.theme` is `system` (the default, not persisted), `light`,
`dark`, or a lowercase palette id (`[a-z0-9-]{1,32}`). Palette names
belong to the web; the CLI only checks the shape.

## UI colors (`ui.*`)

The user's color overrides are three `config set` paths. Discover the valid
token names first — the catalog ships with the binary:

```bash
gadak config get ui.tokens.catalog --json   # name, cssVar, tier, rules, per-palette values
gadak config set ui.tokens '{"colors":{"accent":"#7a4bd0"}}'
gadak config set ui.tokensByTheme '{"dark":{"colors":{"accent":"#9a6be0"}}}'
gadak config set ui.dataColors '{"label":{"urgent":"#c03030"},"type":{"10007":"#d07020"},"status":{"inprogress":"#7e5904"}}'
```

- **Warnings mean applied: only parsing refuses.** A value that
  cannot parse (`"red"`, `"90"` for a length) or a wrong shape refuses;
  everything else — locked tiers, contrast/ΔEok/deuteranopia floors,
  dimension ranges and relations — **warns on stderr and saves** (exit 0,
  value echoed). Do not retry or work around a warning: the user's look is
  theirs. Do surface the warning text — it carries the measured number,
  the floor, and the fix (contrast warnings name the failing palettes and
  the `ui.tokensByTheme.<palette>` scoping fix; type-step warnings list the
  four-rung ladder that moves together).
- **Tiers:** `locked` tokens warn and save (palette authoring — the build
  may re-derive them in an upgrade); `validated` tokens are judged in every
  palette they render in; `free` tokens need hex only. Values are `#rgb` or
  `#rrggbb`, nothing else.
- **`dataColors` keys are ids, never display names**: `label.*` is the label
  text itself, `type.*` is the Jira issue type id (digits — names localize per
  account), `status.*` is the status category `new` / `inprogress` / `done`.
  A display name is refused with the correct key kind in the message.
- **Unknown token names warn and are carried**, not refused — a newer gadak's
  config still loads. Do not "clean" them out on sight.
- Changes reach an already-open web tab within ~1s (the ui-focus poll carries
  `configVersion`); no reload, no server restart.

## Dashboards (agent-authored walls)

You can author a live dashboard yourself: one HTML document plus named
queries, saved like a view and rendered full-tab in the running web UI.
Write the HTML to a temp file, register it with its datasources, open it —
an already-open tab follows within ~1s, and every later `save` under the
same name live-replaces the frame:

```bash
gadak dashboards save triage --html /tmp/triage.html \
  --datasource "by_status=sql:select status_category, count(*) as n from issues_full group by 1 order by 1" \
  --datasource "mine=jql:assignee = currentUser() AND resolution is EMPTY"
gadak dashboards open triage        # focuses the running web tab
gadak dashboards list / show / rm   # lifecycle; same name on save = update
```

- **Your HTML never fetches.** It runs sandboxed (opaque origin, CSP closes
  the network); the host executes each registered datasource and pushes rows
  in as `postMessage` events `{type:'data', name, columns, rows}`. Listen
  for those and paint. **`rows` are positional arrays, not objects** —
  `columns` is `["label","n"]` and `rows` is `[["api",12], …]`, so read
  `row[columns.indexOf('n')]`. Reaching for `row.n` is `undefined` in every
  cell and throws nothing; a wall of `undefined`/`NaN` over correct SQL is
  always this. You may send back exactly two verbs, both throttled:
  `{type:'refresh'}` (re-run datasources) and
  `{type:'open', hash:'#/?issue=GDK-1'}` — navigate the app itself to one
  of its own hashes (issue detail `#/?issue=KEY`, filtered list
  `#/?sc=inprogress`, search `#/?q=…`; recipe table in `docs/DASHBOARDS.md`).
  To link off-app, use `<a target="_blank" rel="noopener">` — it opens a
  new tab and leaves the wall in place; never a plain external `href`,
  which navigates the frame away.
- **SQL datasources are arbitrary read-only SELECTs** over `issues_full` —
  CTEs and window functions included. Key on computed columns, never display
  names: `status_category` / `priority_rank` / `issue_type_id`.
- **Charts: use the vendored libraries, not a CDN** (CSP refuses external
  hosts). `<script src="/api/v1/dashboards/vendor/uPlot.iife.min.js">` (+ its
  CSS) — leading slash required. The frame inherits no app styling: set your
  own explicit palette.
**A complete one. Copy this shape — there is no example file to go find.**
Everything the contract requires is here: the listener, the positional read,
and a paint. Nothing is elided.

```html
<!doctype html><meta charset="utf-8">
<style>
  :root { color-scheme: light dark }
  body { margin:0; padding:24px; font:14px/1.5 ui-sans-serif,system-ui,sans-serif;
         background:#12141a; color:#e8eaf0 }
  h1 { font-size:15px; font-weight:600; margin:0 0 16px; letter-spacing:.01em }
  .row { display:flex; align-items:center; gap:12px; margin:6px 0 }
  .name { width:180px; color:#a8aec0; overflow:hidden; text-overflow:ellipsis;
          white-space:nowrap }
  .bar { height:18px; background:#5b8cff; border-radius:3px; min-width:2px }
  .n { color:#a8aec0; font-variant-numeric:tabular-nums }
</style>
<h1 id="t">…</h1>
<div id="out"></div>
<script>
  addEventListener('message', (e) => {
    const m = e.data
    if (!m || m.type !== 'data' || m.name !== 'by_label') return
    // rows are POSITIONAL arrays keyed by columns — never row.n
    const iL = m.columns.indexOf('label'), iN = m.columns.indexOf('n')
    const rows = m.rows.map((r) => [String(r[iL]), Number(r[iN])])
    const max = Math.max(1, ...rows.map((r) => r[1]))
    document.getElementById('t').textContent =
      `${rows.length} labels · ${rows.reduce((a, r) => a + r[1], 0)} issues`
    document.getElementById('out').replaceChildren(...rows.map(([label, n]) => {
      const row = document.createElement('div'); row.className = 'row'
      const nm = document.createElement('div'); nm.className = 'name'; nm.textContent = label
      const bar = document.createElement('div'); bar.className = 'bar'
      bar.style.width = `${(n / max) * 320}px`
      const num = document.createElement('div'); num.className = 'n'; num.textContent = n
      row.append(nm, bar, num); return row
    }))
  })
</script>
```

```bash
gadak dashboards save label_ratio --html /tmp/label_ratio.html \
  --datasource "by_label=sql:select json_each.value as label, count(*) as n \
from issues_full, json_each(issues_full.labels) group by 1 order by n desc limit 20"
gadak dashboards open label_ratio
```

- For a uPlot line chart and the full `open` recipe table, `docs/DASHBOARDS.md`
  lives in gadak's source tree — it is *not* installed beside this file, so
  fetch `https://github.com/midagedev/gadak/blob/main/docs/DASHBOARDS.md` if
  you want it. Do not search the filesystem for it: there is nothing to find,
  and the block above is already a working wall.

## Rules that come with the file

- **Never write to the database.** Writes go through the origin (the Jira site on
  a Jira workspace, the built-in tracker when local, the home serve when paired);
  a row written directly is destroyed by the next sync. No exception for
  "just a label". Saved views and visits live in `local.db`, not the mirror.
- **Do not depend on `issues.raw`.** It is shaped by Jira's API, not by gadak's
  contract. Use the projected columns.
- **Do not poll in a loop.** `sync_state.version` moves only when something
  changed — compare it instead.
- **Read the freshness warning.** `issue`, `search`, `comment`, `transition`,
  `assign`, and `fields` print one line to stderr when the last sync failed or
  is over an hour old. stdout stays clean and pipeable.
- **Do not quote a restricted or JSM-internal comment in a public channel.**
  Filter `visibility_type != '' OR jsd_public = 0` first (`jsd_public` NULL
  means the marker was absent, not internal).
- **Before quoting an issue in a public place** (commit message, public
  summary, chat), check `security_level_id`. NULL means unrestricted, or a
  row the next sync has not rewritten. Key on the id, never on
  `security_level` (names localize).

## Development-panel links (`dev_links`)

`dev_links` is the projected development panel (pull-request URL, title,
status). `gadak issue KEY --json` includes it; SQL joins `dev_links` on
`item_id`.

- **built-in tracker:** `gadak dev link KEY --pr <url>` records a PR through the
  built-in origin; `gadak dev scan` execs `gh pr list` and links matches
  (`cmd/gadak/dev.go`). Both refuse on a Jira workspace.
- **Jira Cloud:** do not run `dev link` / `dev scan`. `gadak config set
  devStatus true` to *mirror* Jira's development panel into `dev_links` (read).
- **paired** is kind `connected`: same refusal as Cloud. Write on the
  gadak home, then sync.

## When the mirror does not model it

Watchers, worklogs, user search, and anything else sync does not
project are reachable through the origin with `gadak api`. Sprints *are*
projected (`issues.sprint_id`, `sprint_name`, `sprint_state`); filter on
`sprint_id` or `sprint_state='active'`, never on `sprint_name`. The
`sprints` and `boards` tables carry the rest, and `gadak sprint` writes:

```bash
gadak sprint list                      # active first, with issue counts and the sprint goal
gadak sprint add 12 NMB-140 NMB-141    # into sprint 12
gadak sprint remove NMB-140            # back to the backlog
gadak sprint create 3 "Sprint 14" --goal "ship the uploader"
gadak sprint start 14 --days 7
gadak sprint close 14
```

They work on Jira (Cloud and Server, where Jira Software is installed), on
the built-in tracker, which serves the same Agile surface, and on Linear,
where a cycle is a sprint and a team is a board — there `list`, `add` and
`remove` work, and `create`, `start` and `close` refuse by name: a cycle
begins and ends by its dates, and Linear makes them from the team's cadence
rather than on request.

```bash
gadak api GET /rest/api/3/issue/NMB-140/watchers
```

Read-only unless `--write` is passed. Prefer the mirror when it can answer.
On a Jira or Linear workspace this is a network round trip against the site's
rate budget. On the local built-in tracker it talks in-process (no network);
unimplemented paths return 501.

## When gadak itself is the problem

You cannot see the window, so a path is your whole diagnostic surface. In
order:

- `gadak doctor` — a redacted summary of this install. Safe to paste into a
  report; it never contains a credential.
- `~/.gadak/logs/gadak.log` — the log file. It is local, never uploaded, and
  token values are stripped before anything is written; read the tail of it.
  `gadak doctor` prints the real path, which is what to trust if the home was
  moved.
- The mirror is a disposable cache, so `gadak sync --full` is a safe first
  move and losing the mirror costs nothing. Do that before anything drastic.
- `skill: stale` means this file and the binary disagree — `gadak skill
  install`.

## Report the friction; do not route around it

If gadak made you work harder than the task deserved — a column you had to
parse by hand, an identifier a read gave you that a write refused, an error
that named a problem but not a next move — that is worth reporting, and
reporting it is the expected move rather than a favour. The workaround you
were about to write destroys the evidence: once you have parsed it yourself,
nobody can see that the column was missing.

Report it at `https://github.com/midagedev/gadak/issues`. What makes a report
actionable here:

- the exact command you ran and the exact output you got
- `gadak --version`, and its origin type and transport, or
  Linear
- what you expected instead — one sentence

A patch is welcome too, and is often smaller than the report suggests: clone
`https://github.com/midagedev/gadak`, and `AGENTS.md` at the root is the
development contract, including the gate commands to run before you open a
pull request.

## More

`gadak sql "select ..."` against `specs/000-product/data-model.md` covers every
column; `docs/RECIPES.md` in the gadak repository has more worked questions.