Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
68947a0
fix: give delivery its own cursor, and close three silent data-loss p…
pasichDev Sep 4, 2026
6d824c6
docs: reposition around cross-tool capture, and add the workspace docs
pasichDev Sep 4, 2026
9755de5
fix: three more silent sync failures, and break up the four files the…
pasichDev Sep 5, 2026
8afcce5
chore: cut 3.0.0-rc.1
pasichDev Sep 5, 2026
93f0b89
refactor: make the dashboard's client real TypeScript, not a string
pasichDev Sep 5, 2026
8880387
fix: stop treating a loopback source address as admin authorization
pasichDev Sep 5, 2026
b07fd72
fix: four more ways data could go missing without anyone being told
pasichDev Sep 5, 2026
c24dabd
chore: make CI run the tests it thinks it runs, and RC updates reach RCs
pasichDev Sep 5, 2026
96b7657
test: tear the admin-token server down before removing its directory
pasichDev Sep 5, 2026
4fadaa8
fix: durable, fenced persistence for every on-disk write (audit B01–B05)
pasichDev Sep 5, 2026
be46705
fix: backup, restore and history as transactions (audit B06–B09)
pasichDev Sep 5, 2026
6942dda
fix: workspace snapshots and one source of truth for mode and data (B…
pasichDev Sep 5, 2026
b15d30c
fix: identity, installer safety, containers and release gates (B19–B27)
pasichDev Sep 5, 2026
d8f875b
docs: regenerate the dashboard screenshots, and fix the guard they ex…
pasichDev Sep 5, 2026
06ecb93
ci: stop SIGPIPE from failing the workflow steps that check output
pasichDev Sep 5, 2026
a72c074
test: stop asserting that check-update can reach the internet
pasichDev Sep 5, 2026
20fe895
fix: an item that outlives a peer's deletion never reached that peer …
pasichDev Sep 5, 2026
c11aaa7
chore: 3.0.0-rc.2
pasichDev Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"name": "pasichDev",
"url": "https://github.com/pasichDev"
},
"description": "Marketplace for the docket claim/release workflow skill.",
"description": "Marketplace for the docket skill.",
"plugins": [
{
"name": "docket-claim",
"name": "docket",
"source": "./",
"description": "Claim/release workflow skill for the docket MCP server's shared backlog.",
"version": "1.0.0"
"description": "Field and tool reference for docket, the shared list every AI tool and project writes to.",
"version": "2.0.0"
}
]
}
6 changes: 3 additions & 3 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "docket-claim",
"description": "Claim/release workflow skill for the docket MCP server's shared backlog.",
"version": "1.0.0",
"name": "docket",
"description": "Field and tool reference for docket, the shared list every AI tool and project writes to.",
"version": "2.0.0",
"author": {
"name": "pasichDev",
"url": "https://github.com/pasichDev"
Expand Down
89 changes: 87 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,38 @@ on:
branches: [main]

jobs:
build:
# The engines field claims ">=18". Until this matrix existed, that claim was evidenced by
# exactly one moving `lts/*` on Ubuntu — so a syntax or API that only exists in a newer
# Node would ship green, and the first person to hear about it would be a user on 18.
test:
name: test (node ${{ matrix.node }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node: ["18", "20", "22", "24"]
include:
# macOS is the other platform this is actually used on daily. One version is
# enough: what differs there is the filesystem and process behaviour the lock,
# the atomic writes and the daemon probes depend on — not the language level.
- os: macos-latest
node: "20"
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm run build
# `npm test`, not a copy of its glob. The duplicate silently stopped running the
# browser-client tests the moment they moved into dist/web/client/app, and CI stayed
# green while a whole directory went unexecuted.
- run: npm test

# What a user actually installs, rather than what the repository happens to contain: the
# packed tarball, unpacked into an isolated HOME, exercised through its published bins.
pack-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -16,4 +47,58 @@ jobs:
node-version: "lts/*"
- run: npm ci
- run: npm run build
- run: node --test dist/*.test.js dist/web/*.test.js dist/server/*.test.js dist/remote/*.test.js
- name: Install the packed artifact into an isolated HOME
run: |
set -euo pipefail
# Output is captured and matched in memory, never piped into `grep -q` or `head`.
# Those exit as soon as they have what they need, which closes the pipe under a
# `docket` that is still writing — SIGPIPE, exit 141, and with `pipefail` that is
# a failed step reporting nothing about what actually went wrong.
tarball="$(npm pack --silent | tail -1)"
scratch="$(mktemp -d)"
export HOME="$scratch"
export DOCKET_DATA_DIR="$scratch/data"
npm install -g "$PWD/$tarball"
docket help > /dev/null
docket import /dev/stdin <<'EOF'
# Docket
- [ ] packed artifact smoke test
EOF
listed="$(docket list --all)"
grep -q "packed artifact smoke test" <<< "$listed"
exported="$(docket export --format json)"
grep -q "packed artifact smoke test" <<< "$exported"
status="$(docket status)"
grep -q "^Mode: local" <<< "$status"
echo "packed artifact works end to end"

# The documented build path — `npm run build` inside the image — was never executed by CI,
# which is how the Dockerfile came to copy one of the three tsconfigs it needs.
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Build the image
run: docker build -t docket:ci .
- name: The documented commands must work inside it
run: |
set -euo pipefail
docker run -d --name docket-ci -v docket-ci-data:/data docket:ci
for i in $(seq 1 30); do
if docker exec docket-ci wget -q --spider http://127.0.0.1:8788/api/v1/health; then break; fi
sleep 1
done
health="$(docker exec docket-ci wget -qO- http://127.0.0.1:8788/api/v1/health)"
grep -q '"ok":true' <<< "$health"
# docs/headless.md's first instruction to a new self-hoster. It needs `docket` on
# PATH inside the runtime image, which the image did not have.
#
# Captured whole, then matched: `… | head -1` closes the pipe after the first of
# this command's four lines, and the SIGPIPE that kills the writer is exit 141.
pairing="$(docker exec docket-ci docket devices pair)"
grep -qE '^[A-Z0-9]{6}$' <<< "$(head -1 <<< "$pairing")"
# Non-root, on the volume the compose file actually uses.
whoami_out="$(docker exec docket-ci id)"
grep -q 'uid=100(docket)' <<< "$whoami_out"
docker rm -f docket-ci
docker volume rm docket-ci-data
147 changes: 132 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,152 @@ on:
tags: ["v*"] # e.g. git tag v1.0.0 && git push origin v1.0.0

jobs:
# Nothing reaches npm before this passes. The previous version of this workflow built the
# package and published it: a tag pointing at any commit on any branch — a fork's, an
# abandoned experiment's, one whose tests had never run — became a published release, and
# the only gate was that `tsc` succeeded.
verify:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
dist_tag: ${{ steps.version.outputs.dist_tag }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # the ancestry check below needs history, not a shallow tip

- name: The tag must point at a commit that is on main
run: |
set -euo pipefail
git fetch origin main --quiet
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::${GITHUB_REF_NAME} points at ${GITHUB_SHA}, which is not an ancestor of origin/main."
echo "Releases are cut from reviewed main. Merge first, then tag the merged commit."
exit 1
fi
echo "${GITHUB_SHA} is on main."

- name: The tag must match the version it claims to publish
id: version
run: |
set -euo pipefail
version="$(node -p "require('./package.json').version")"
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
echo "::error::tag $GITHUB_REF_NAME does not match package.json version $version."
exit 1
fi
# A prerelease MUST NOT become `latest`. npm's default dist-tag is latest, so
# publishing 3.0.0-rc.1 from this workflow used to make every `npm install
# @pasichdev/docket` and every unpinned `npx` in the world resolve to a release
# candidate — including the update checker, which would then offer it to stable
# users as an upgrade.
if [[ "$version" == *-* ]]; then dist_tag=next; else dist_tag=latest; fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "dist_tag=$dist_tag" >> "$GITHUB_OUTPUT"
echo "publishing $version to the '$dist_tag' dist-tag"

- uses: actions/setup-node@v5
with:
node-version: "lts/*"
- run: npm ci

# The full gate, re-run on the exact tagged tree rather than trusted from whatever CI
# ran on the branch.
- run: npm run build
- run: npm test
- name: Dependency audit
run: npm audit --omit=dev --audit-level=high

- name: The packed artifact must work in an isolated HOME
run: |
set -euo pipefail
# Captured and matched in memory rather than piped into `grep -q`, which exits on
# its first match and SIGPIPEs the still-writing `docket` — exit 141 under pipefail.
tarball="$(npm pack --silent | tail -1)"
scratch="$(mktemp -d)"
export HOME="$scratch"
export DOCKET_DATA_DIR="$scratch/data"
npm install -g "$PWD/$tarball"
installed="$(docket --version)"
if [ "$installed" != "${{ steps.version.outputs.version }}" ]; then
echo "::error::the packed artifact reports $installed, not ${{ steps.version.outputs.version }}."
exit 1
fi
docket import /dev/stdin <<'EOF'
# Docket
- [ ] release gate smoke test
EOF
listed="$(docket list --all)"
grep -q "release gate smoke test" <<< "$listed"
status="$(docket status)"
grep -q "^Mode: local" <<< "$status"

- name: The published server metadata must match this version
run: |
set -euo pipefail
if [ -f server.json ]; then
metadata_version="$(node -p "require('./server.json').version ?? ''")"
if [ -n "$metadata_version" ] && [ "$metadata_version" != "${{ steps.version.outputs.version }}" ]; then
echo "::error::server.json says $metadata_version but package.json says ${{ steps.version.outputs.version }}."
exit 1
fi
fi

- name: The image must build and answer on the exact tagged tree
run: |
set -euo pipefail
docker build -t docket:release .
docker run -d --name docket-release -v docket-release-data:/data docket:release
for i in $(seq 1 30); do
if docker exec docket-release wget -q --spider http://127.0.0.1:8788/api/v1/health; then break; fi
sleep 1
done
health="$(docker exec docket-release wget -qO- http://127.0.0.1:8788/api/v1/health)"
grep -q '"ok":true' <<< "$health"
pairing="$(docker exec docket-release docket devices pair)"
grep -qE '^[A-Z0-9]{6}$' <<< "$(head -1 <<< "$pairing")"
docker rm -f docket-release
docker volume rm docket-release-data

publish:
needs: verify
runs-on: ubuntu-latest
# A protected environment: the release credentials are only reachable from a job that
# required an approval, so a pushed tag alone cannot spend them.
environment: release
permissions:
contents: read
id-token: write # required for `npm publish --provenance` (Sigstore-backed attestation)

steps:
- name: Checkout code
uses: actions/checkout@v5

# --- Publish the npm package ---

- name: Set up Node.js
uses: actions/setup-node@v5
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "lts/*"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build

- name: Install dependencies
run: npm ci

- name: Build package
run: npm run build

- name: Publish package to npm
run: npm publish --provenance
- name: Publish to npm
run: npm publish --provenance --tag "${{ needs.verify.outputs.dist_tag }}"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Confirm the dist-tags landed where they were meant to
run: |
set -euo pipefail
version="${{ needs.verify.outputs.version }}"
for attempt in $(seq 1 10); do
latest="$(npm view @pasichdev/docket dist-tags.latest 2>/dev/null || echo '')"
if [ -n "$latest" ]; then break; fi
sleep 5
done
echo "latest is now $latest"
if [[ "$version" == *-* ]] && [ "$latest" = "$version" ]; then
echo "::error::prerelease $version became the 'latest' dist-tag. Fix with: npm dist-tag add @pasichdev/docket@<previous stable> latest"
exit 1
fi

# --- Publish server metadata to the official MCP Registry ---
# Requires a server.json in the repo root (generate once locally with
# `mcp-publisher init` + `mcp-publisher login github`, see README) and
Expand Down
Loading