The host never started on Windows: `setsid: command not found`, after
which the script wrote the dead setsid pid to .dev-host.pid and counted
out its full timeout printing dots. setsid is Linux-only, so this would
have hit macOS too.
Four Linux assumptions, replaced with probed alternatives:
setsid used when present; otherwise plain nohup, and stop
walks the process tree instead of killing the group
ss falls back to lsof (macOS), then netstat (Windows)
node -p require reads the version with sed instead: bash hands Node
/c/Users/..., which Node resolves as C:\c\Users\...
and throws MODULE_NOT_FOUND
--show-current falls back to `describe`, empty on the detached HEAD
a fresh submodule checkout leaves behind
Also, so the next failure is legible rather than a wall of dots:
the readiness loop now stops the moment the host process dies, and the
timeout prints the log's error lines instead of `tail -5`, which the
per-file session-sync chatter had made useless. Timeout raised to 120s
for cold first starts (tsx compile + vite optimizeDeps), overridable
with READY_TIMEOUT_SECONDS.
Unrelated bug found while there: stop_host used `exit 0` when nothing was
running, so `restart` on a stopped host never reached start_host.
Verified on Linux: the three port backends agree, and the no-setsid path
was exercised in isolation — group kill fails as expected and kill_tree
reaps parent and children. The netstat and ps branches are written to
documented behaviour but untested for lack of a Windows or macOS host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cloudcli-taskwork — workspace
Meta-repository for the Task Work plugin for CloudCLI UI (siteboon/claudecodeui).
It holds the specification, the developer scripts and two submodules. It contains
no code of its own — no package.json, no dependencies.
Specification: spec/SPEC-v3.md (current), design input:
spec/DESIGN-DOC.md.
Repository map
| Path | Repository | Role |
|---|---|---|
| (this repo) | gitea:ai-first/cloudcli-taskwork |
Workspace: spec, scripts, submodule pins |
plugin-taskwork/ |
gitea:ai-first/cloudcli-plugin-taskwork |
The product, developed here; manifest.json lives in its root |
| — | github:bvn13/cloudcli-plugin-taskwork |
Where users install the plugin from — public, anonymous HTTPS clone |
cloudcli/ |
github:bvn13/claudecodeui |
Fork of the host; source of the three upstream PRs |
| — | github:siteboon/claudecodeui |
Upstream: source of updates, target of the PRs |
| — | gitea:ai-first/cloudcli |
Pull mirror of the fork, read-only |
The plugin lives in two places on purpose: development and the submodule pin stay
on Gitea, while the host installs it over anonymous HTTPS, which a private Gitea
repository cannot serve. The GitHub copy is a push mirror — pushing to Gitea
is enough, it syncs on its own. Do not add a GitHub remote to plugin-taskwork/
and do not push there by hand.
⚠️ Never push to
gitea:ai-first/cloudcli. It is a pull mirror — anything pushed there is silently overwritten on the next sync. Thecloudcli/submodule points at the GitHub fork, and the mirror is deliberately not wired up as a remote.
Getting started
git clone --recurse-submodules ssh://git@gitea.bvn13.me:2221/ai-first/cloudcli-taskwork.git
cd cloudcli-taskwork
./scripts/bootstrap.sh
Then:
./scripts/dev-host.sh start # installs the built plugin and starts the host
It prints the URL — http://localhost:5183 by default. stop, restart,
status and logs do what they say; SERVER_PORT/VITE_PORT override the
ports.
The host runs against your real environment: the same ~/.claude projects,
the same ~/.cloudcli/auth.db, the same ~/.claude-code-ui/plugins. Nothing is
sandboxed, so you see what a user would see. Ports default to 3010/5183 rather
than 3001/5173 so an already installed CloudCLI keeps running — but note that
both instances then share one database. Set DATABASE_PATH to separate them.
After changing the plugin:
cd plugin-taskwork && npm run build
cd .. && ./scripts/link-plugin.sh # the host reads a copy, not a symlink
Client changes need only the plugin surface reopened; server changes need the host
restarted (./scripts/dev-host.sh restart), because the plugin's backend process
is spawned at host startup.
The plugin stores its data outside its own directory, in
~/.claude-code-ui/taskwork/tasks.json — the plugin directory is wiped on Update.
Access
All submodule URLs are SSH, so both submodules are writable with the same key
setup: ssh://git@gitea.bvn13.me:2221/… for the plugin (the Gitea repositories
are private) and git@github.com:bvn13/claudecodeui.git for the fork. The
upstream remote stays on HTTPS — it is fetch-only and public.
Cloning therefore needs an SSH key registered with both hosts; check with
ssh -T git@github.com and ssh -T -p 2221 git@gitea.bvn13.me.
Distribution goes through the public GitHub copy
(github:bvn13/cloudcli-plugin-taskwork), because the host clones a plugin
anonymously over HTTPS and the Gitea repository is private. The submodule keeps
pointing at Gitea: that is where the plugin is developed and pushed first.
Scripts
| Script | Purpose |
|---|---|
scripts/bootstrap.sh |
Init submodules, wire the upstream remote, install deps, build the host's native modules and the plugin |
scripts/dev-host.sh |
Run the patched host locally: start, stop, restart, status, logs |
scripts/export-patches.sh |
Regenerate plugin-taskwork/patches/*.patch from the fork's feature branches (§12.0) |
scripts/link-plugin.sh |
Symlink the local plugin into the host's plugin directory (--unlink to remove) |
scripts/check-submodule-pins.sh |
Verify every pinned submodule commit is reachable from its origin |
Branches in cloudcli/
Each feature branch is cut from the upstream tag v1.37.2, never from another
feature branch — that is what keeps the three pull requests independent.
| Branch | Base | Purpose |
|---|---|---|
main |
tracks upstream/main |
Upstream sync. Carries no commits of ours |
feat/resizable-sidebar |
v1.37.2 |
PR-1 — resizable sidebar |
feat/plugin-host-api |
v1.37.2 |
PR-2 — authenticated host API for plugins |
feat/plugin-sidebar-surface |
v1.37.2 |
PR-3 — sidebar surface for plugins |
dev/taskwork |
merge of the three | Local run and debugging; the submodule is pinned here |
dev/taskwork never takes part in a pull request and may be recreated at any time.
Working with the submodules
- Commit code inside the submodule, push it, and only then record the new pin
here with a separate commit
chore: bump <submodule> to <sha>. - Update with
git submodule update --remote --merge. - Run
./scripts/check-submodule-pins.shbefore pushing a pin bump.
Status
| Piece | State |
|---|---|
| Plugin | v1.0.0, pushed to Gitea; installs, builds and runs (npm install --ignore-scripts + npm run build in ~3 s) |
feat/resizable-sidebar |
Pushed to the fork. PR-1 not opened yet; no issue needed |
feat/plugin-host-api |
Pushed to the fork. Issue text in spec/upstream/issue-1-plugin-host-api.md, PR text in plugin-taskwork/docs/upstream-pr-2.md |
feat/plugin-sidebar-surface |
Pushed to the fork. Issue text in spec/upstream/issue-2-plugin-sidebar-surface.md, PR text in plugin-taskwork/docs/upstream-pr-3.md |
dev/taskwork |
Merge of the three plus one integration commit (host API in the sidebar surface) that never goes upstream; the cloudcli pin points here |
Remaining before upstream contact (§19 of the spec): open issue #1, then PR-2; issue #2, then PR-3; PR-1 can go on its own. Also outstanding: the manual acceptance checklist (§15.4), which needs a running host in a browser.
Release
See §13.2 of the spec. In short: export patches → bump the plugin version in
package.json and manifest.json → build, typecheck, test → commit build/,
patches/, CHANGELOG.md → tag vX.Y.Z in the plugin repo → bump the pins here.
Licence
The plugin and the fork are AGPL-3.0-or-later, following the upstream host.