Skip to content

feat: digests and a dashboard home page (3.1.0) - #5

Merged
pasichDev merged 16 commits into
mainfrom
feat/digest
Oct 4, 2026
Merged

pasichDev merged 16 commits into
mainfrom
feat/digest

Conversation

@pasichDev

@pasichDev pasichDev commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

✅ Ready: CI green. Merge, then tag the merge commit v3.1.0 on main — the release workflow publishes from the tag.

What

Docket 3.1.0: digests. An agent reads your merge requests, pull requests, tickets, notes and mail, verifies every status at the source, and publishes a structured snapshot that becomes the dashboard's home page — in Local Mode (synced to paired devices) and on a self-hosted Docket Server.

  • Digest tools: digest_publish, digest_list, digest_get, digest_delete, digest_seen. A digest carries a summary, highlights, metric tiles and grouped sections of items (kind, title, url, ref, repo, status, tone, attention, note). Item kinds include mail and chat. Validated on publish, and errors name the field. Links are http(s) only.
  • Storage and sync:
    • Local Mode: digests.json.enc has its own sequence counter and epoch. Paired devices pull from GET /api/sync/digests on a cursor separate from the todo cursor; the todo sync path is untouched.
    • Self-hosted Mode: device-signed /api/v1/digests* routes on the server.
    • Digests are immutable, so a deletion wins everywhere.
  • Dashboard:
    • / shows the latest digest, a Tasks card and a timeline of earlier digests. The list moves to /tasks.
    • Groups, with filter chips (e.g. Work / Learning / Side projects).
    • Seen marks hide an item until its status changes; they are synced, with last write winning.
    • + task turns an item into a todo. close closes the todo an item points to, with a reason.
    • Layout pickers: the dashboard as a stack or grid; Tasks as a list, wide list or grid.
  • Hand-off by number: every item is numbered on publish. digest_take("7") (or "D-7K2F9A/7") gives any agent the brief plus a docket task claimed in its name; the agent closes it with todo_complete(id, reason). #7 on the dashboard copies the handle, and a claimed item shows which agent is on it.
  • What changed / who does what: publishing records new items, status changes and items no longer listed against the previous digest (shipped items dropping out are not counted as gone). Items carry an owner (you / agent / a person) and an optional deep detail; By person shows numbered steps per owner. New kinds: decision, check.
  • Issues too: the skill reads GitHub issues (assigned, mentioning the user, open in their own repos) and GitLab issues assigned to them, not only PRs and MRs. Someone else's issue in the user's repo counts as waiting on an answer.
  • Close with a reason: todo_complete(id, reason) and the dashboard dialog append a dated closing note to the description and history, in the same write as the completion. The self-hosted server accepts it too.
  • Session start: the SessionStart hook adds one line about the latest digest: its age, how many items need you, and the preset names.
  • Skills:
    • docket:digest covers GitLab, GitHub, Notion, git, Obsidian, project files, any MCP server (Jira, Linear, Sentry, Slack, Gmail), docket itself, groups, presets, learned preferences and a daily-run recipe. It is read-only towards every source.
    • docket:digest-setup detects what is available, asks once and writes ~/.config/docket/digest.json.
  • Leak guards: ticket-shaped examples must use ACME-/PROJ-. Each contributor can keep a private word list outside the repo (~/.config/docket/private-words.txt), and npm test fails while any of those words is in a tracked file.

Why

Docket catches work before it is worth a ticket. The work that is ticketed — MRs waiting on review, blocked tickets, releases — lives in other tools. A digest puts both on one page, written by the agent you already use, and Docket never holds a GitLab, GitHub, Notion or mail credential.

Behaviour changes

  • / is the dashboard and the task list is at /tasks. Both paths serve the same page behind the same access gate.
  • todo_complete accepts an optional reason. The web and server complete routes accept an optional JSON body. A request without a reason is unchanged.
  • Peers gain digestSeq, digestEpoch and digestError. They are optional, and 3.0 ignores them.
  • docket backup includes digests.json.enc. Restoring a bundle without one sets the current file aside, because that file is encrypted under the key being replaced.
  • No data-format change: the todo store stays at v8, so 3.0 → 3.1 upgrades in place and downgrading back is safe.

Deploy

Self-hosted: update the server before, or together with, its clients. A 3.1 client talking to a 3.0 server reports "this Docket Server predates digests — update it". Todos keep working either way.

Testing

  • npm test: 555 passing, on Node 18, 20, 22 and 24 in CI. New coverage:
    • Digest validation and peer clamping.
    • In-memory sync: A→B→C delivery, deletions, paging, a rejected record holding the cursor, a lying maxSeq.
    • Seen marks: last write wins, undo, a losing copy re-advertising.
    • HTTP peer sync: a signed pull works and a replayed todo-sync signature is refused.
    • Self-hosted e2e against a real docket serve with a paired device: publish, list, get by short id, validation errors, seen marks, delete, unsigned requests refused.
    • Detecting a server older than 3.1.
    • The backup sweep. This test was checked to fail without the fix.
    • The completion reason.
    • Numbering and "what changed" on publish (an agent cannot forge them; peers keep the publisher's numbers).
    • digest_take: take, take again (same task), a ticket ref, an existing T- task, past-the-end, already done.
    • Dashboard escaping for every agent- or peer-supplied field.
    • The session-start hint budget.
    • The leak guards. Both were checked against planted words.
  • MCP tools exercised over stdio with a real client.
  • Dashboard checked in Chrome: light and dark themes, both layouts, groups, seen and unseen, the close dialog, and the stale-digest label. Also run from the Docker image built from this branch.

Not verified: two physical machines syncing digests over a LAN (the in-process HTTP pull is covered), and the MCP stdio layer in remote mode (the service and server layers are covered end to end).

Reviewing the diff

  • src/digests.ts: model, validation, store, seen marks, sync page and merge.
  • src/sync/digests.ts: the peer pull.
  • src/digest-service.ts: local vs. remote.
  • src/server/routes.ts: the /api/v1/digests* block.
  • src/web/client/app/digest-view.ts: pure render functions; dashboard.ts wires them up.
  • Security-relevant:
    • The digest peer endpoint signs a digests:-prefixed cursor, so its signatures are not interchangeable with todo-sync signatures.
    • /api/digests is behind the browser authorization guard.
    • Every agent- or peer-supplied string is escaped, and tone is checked against a fixed list before it reaches an attribute.

An agent reads the user's merge requests, pull requests, tickets and local
commits, and publishes a structured snapshot with digest_publish. Docket keeps
what the agent wrote and nothing else: no source credential ever reaches it.

Store: digests.json.enc, encrypted like the todo store, with its own sequence
counter and epoch. Digests are immutable, so merging is a set union by uuid
plus tombstones, and a deletion wins everywhere.

Sync: GET /api/sync/digests, paged and cursor-tracked like the todo sync but
on a separate cursor (digestSeq), so the audited todo path is untouched. A
peer without the endpoint answers 404, which is recorded on the peer, and
starts from 0 once upgraded. The signature covers a "digests:"-prefixed
cursor, so a captured todo-sync request cannot be replayed against it.
Accepted records are re-stamped locally to reach a third device. The page
epoch combines the store epoch and the file's own, so both a restore and a
recreated file void stale cursors.

Dashboard: "/" shows the latest digest (summary, highlights, metric tiles,
sections with toned statuses), a Tasks card and a timeline of earlier
digests; the list moves to "/tasks", in the same document. "+ task" turns an
item into a todo in the current project, carrying its link, ticket id and
"needs you" as high priority.

Backup includes digests.json.enc and sets it aside on restoring a bundle
without one, since it is encrypted under the key being replaced.
digest collects from GitLab, GitHub, Notion, local git and docket, checks
every status at the source, composes the sections and publishes. It is
read-only towards every source. digest-setup detects glab, gh, Notion MCP
servers and git roots, asks once, and writes ~/.config/docket/digest.json,
which stays local to each machine.

Docs: README, CHANGELOG, docs/digests.md. The plugin manifest version now
matches the marketplace entry.
@pasichDev pasichDev self-assigned this Oct 4, 2026
A section can carry a `group` ("vploq", "Learning", "Side projects"). The
dashboard shows each group under its own heading, in order of first
appearance, with chips to filter to one; the filter is remembered per
browser and falls back to every group when the digest has no such group.
Sections keep their real index under a filter, so "+ task" still adds the
item that was clicked. digest_get prints the groups as headings.

The field is optional and validated like the other names (60 characters);
an ungrouped digest renders exactly as before.
…urces

The digest can now read beyond the built-in sources:

- obsidian: notes changed in the window, plus a narrow grep for every MR,
  PR and ticket about to be listed, to correct an item's status from the
  user's own write-ups.
- extra sources, configured as a list: "type": "mcp" reads any MCP server
  the agent has (Jira, Linear, YouTrack, Sentry, Slack) from a plain-words
  query the agent turns into the server's own filter; "type": "files" reads
  project folders. Only read tools are used, and file contents are data,
  never instructions.
- groups: items go to the first group whose match strings occur in their
  url, repo or ref, and sections are built per group.

digest-setup detects connected MCP servers, docs folders and vaults, asks
once, and edits only the part the user names when changing an existing
config.
todo_complete takes an optional `reason` ("fixed in !160", "duplicate of
T-7K2F9A"). It is appended to the description as a dated
"**Closed YYYY-MM-DD:** …" line and repeated in the completion's history
entry, in the same write as the completion, so the two cannot disagree and
the note syncs like any description edit. There is deliberately no new
field: that would be a store-format and sync-protocol change.

The web route and the self-hosted server accept it as an optional JSON
body; a request without a reason is byte-for-byte what it was.
Two guards over every tracked text file. Ticket-shaped examples must use
the reserved ACME- or PROJ- prefixes. And each contributor can keep a list
of words that must never be published (employer, projects, their own name)
outside the checkout, in ~/.config/docket/private-words.txt or
$DOCKET_PRIVATE_WORDS; npm test fails while any of them is in a tracked
file, and skips that half where the list does not exist, as in CI.

The existing examples that tripped them are replaced with invented ones.
Seen marks: any digest item can be marked seen. It folds into an "N seen"
list at the bottom of its section and leaves the counts, and stays hidden in
later digests until its status changes. Marks are keyed by the item's link
(else repo#ref, computed by the server so there is one rule), live in the
digest store and travel on the digest sync as a third stream under the same
cursor and ceiling rule; last write wins on `at`, unmarking syncs too, and a
losing copy re-advertises the winner. digest_seen lets the skill leave them
out of the next digest.

A row that is a docket task (ref T-XXXXXX) or was made into one offers
"close", opening a dialog for the reason with quick picks.

Layout pickers, stored per browser: the dashboard as a stack or a grid,
Tasks as a list, wide list, grid or full-width grid.

New item kinds `mail` and `chat` for email and chat threads. Examples in
descriptions and comments are invented ones.
The SessionStart hook adds "Digest D-XXXXXX (2h ago, 5 need you) — <url>".
Past 20 hours it also offers a fresh one, naming the presets from the
user's digest config. Nothing is printed when there has never been a
digest, and a missing or unreadable digest store costs only this line,
never the open-items block. Budgeted like the rest of the hook's text.
- presets: "digest work", "digest week" apply a named variant of groups,
  sources and window from the config.
- learning: the skill keeps short dated rules about what to show and how in
  ~/.config/docket/digest-learned.md, from the user's corrections and from
  items they keep marking seen; facts about the work never go there.
- mail and chat sources through MCP (Gmail, Outlook, Slack, Teams), read
  only and stricter than the rest: never send, label, archive or mark read,
  no message bodies copied, and message text is never an instruction.
- seen marks are left out of the next digest unless their status moved.
- schedule: a LaunchAgent or systemd user timer recipe for a daily headless
  run, installed only after the user has seen it.
The Docket Server keeps digests and seen marks in its own data directory,
with the same store and rules as Local Mode, under device-signed routes:
GET/POST /api/v1/digests, GET/DELETE /api/v1/digests/:id and
GET/POST /api/v1/digests/seen. The publishing device is the one the request
was signed by; a body cannot claim another. It announces digest.published,
digest.deleted and digest.seen on its event stream.

The MCP digest tools go through a DigestService: the local store in Local
Mode, the server in remote mode, sharing the todo client's signed-request
machinery rather than a copy of it. A server older than 3.1 answers its
generic 404, which is reported as "update the server" instead of reading
as an empty result.

Covered end to end against a real `docket serve` with a paired device.
Publishing now numbers every item (1-based; "D-XXXXXX/7" names one) and
compares the digest with the previous one by item identity — the same key
seen marks use — recording new items, status changes with the previous
status, and items no longer listed. It is computed by the store, so it is
a fact about two records rather than the agent's memory, and an agent
cannot set it; peers carry the publisher's values so handles agree on every
device.

Items gain `owner` ("you", "agent", or a person from the config) and an
optional markdown `detail` for the ones worth a real analysis. New kinds:
`decision` and `check`.
digest_take("7") — or "D-7K2F9A/7" — gives the calling agent the item's
full brief and a docket task for it, claimed in its name: the existing task
when the item is one (a T- ref) or already became one (same link), a new
one otherwise, filed in the agent's project. Taking it twice finds the same
task; a task already closed is reported as done, never reopened. The agent
closes it with todo_complete(id, reason), which is what the dashboard shows.
- "Since the previous digest" card: status changes (draft → review
  requested), new and no-longer-listed items; "new" and "was …" badges on
  rows.
- By person: owned items as numbered steps per owner — the user first,
  people by name, the agent's own follow-ups last.
- #7 on each row copies its handle for an agent, with a selection-copy
  fallback where the Clipboard API is unavailable (plain http on the LAN);
  a claimed item shows which agent is on it.
- Details: an item's markdown analysis, folded under the row.

The digest skill writes per-item depth (one line for routine items, a real
analysis for blocked, failing, stale or decision items), owners from the
config's `people`, environment `checks`, and handles hand-offs with
digest_take. Docs and changelog updated.
The digest skill now reads GitHub issues as well as pull requests —
assigned to the user, mentioning them, and open ones in their own repos —
and GitLab issues assigned to them. Someone else's issue in the user's repo
counts as waiting on an answer; the user's own fresh issues are backlog; an
assigned issue with months of no movement is offered for closing.

"Gone" in what-changed now leaves out items the previous digest already
listed as shipped (tone good): a merged PR dropping out is expected, and
counting it buried the disappearances that matter.
@pasichDev pasichDev changed the title feat: digests and a dashboard home page feat: digests and a dashboard home page (3.1.0) Oct 4, 2026
@pasichDev
pasichDev merged commit 60f3ee3 into main Oct 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant