taskops-cli 0.4.1__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ARCHITECTURE.md +106 -11
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/CHANGELOG.md +77 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/PKG-INFO +7 -5
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/README.md +6 -4
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_version.py +1 -1
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/serving.py +9 -3
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/types.py +12 -0
- taskops_cli-0.5.0/src/taskops/gitwork/mirror.py +132 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/gitdoor.py +29 -48
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/handler.py +28 -7
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/mounts.py +11 -12
- taskops_cli-0.5.0/src/taskops/http/repos.py +98 -0
- taskops_cli-0.5.0/src/taskops/http/stale.py +62 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/static.py +30 -15
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/upstream.py +59 -7
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/schema.py +1 -1
- taskops_cli-0.5.0/src/taskops/ui/app.js +61 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_facts.py +4 -4
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/activity.py +7 -1
- taskops_cli-0.5.0/tests/test_mirror.py +164 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_topology.py +277 -6
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_verbs.py +29 -1
- taskops_cli-0.5.0/ui/smoke/sections/all-chapters-scope.tsx +99 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/git-diff.tsx +6 -2
- taskops_cli-0.5.0/ui/smoke/sections/hosted-window.tsx +96 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/App.tsx +24 -2
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Patch.tsx +1 -1
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/MilestonePicker.tsx +28 -9
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/links.tsx +5 -3
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Reports.tsx +6 -5
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/useBoard.ts +6 -3
- taskops_cli-0.4.1/src/taskops/ui/app.js +0 -61
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.claude/settings.json +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.gitattributes +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.github/workflows/ci.yml +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.github/workflows/workflow.yml +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.gitignore +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.mcp.json +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/cache.sqlite +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/cache.sqlite-shm +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/cache.sqlite-wal +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/events.jsonl +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/live.sqlite +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/live.sqlite-shm +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board/live.sqlite-wal +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/board.json +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/reports/lifecycle-chapter.html +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/reports/reports-chapter.html +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/.taskops/ui.lock +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/CLAUDE.md +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/pyproject.toml +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_clock.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_errors.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_ids.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_json.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_locate.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/_wire.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/board.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/__main__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/admin.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/claude.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/commands.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/enrol.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/github.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/grants.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/hooks.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/main.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/operate.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/paging.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/parser.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/pull.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/push.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/remote.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/rm.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/team.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/watch.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/window.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/cli/wording.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/actors.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/challenge.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/chapters.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/event.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/forge.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/graph.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/holding.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/hours.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/kinds.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/machine.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/mentions.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/replay.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/reports.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/review.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/scope.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/core/seams.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/bind.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/catchup.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/claudefiles.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/diff.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/install.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/landing.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/patch.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/remote.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/run.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/sig.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/trailer.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/gitwork/trees.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/admin.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/auth.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/feed.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/grants.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/ingest.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/login.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/members.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/removal.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/rpc.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/scoped.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/server.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/http/watcher.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/identity.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/__main__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/activity.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/before.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/boards.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/boardview.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/brief.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/chapter.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/dossier.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/fields.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/gitmoves.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/hello.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/integrate.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/orders.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/render.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/server.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/thread.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/mcp/tools.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/py.typed +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/session.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/cache.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/creds.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/handover.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/live.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/log.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/pubkeys.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/reviews.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/server.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/store/stores.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/ui/index.html +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/ui/style.css +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_args.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_cards.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_chapter.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_context.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_mentions.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_rows.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_stories.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_waiting.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/_windows.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/assign.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/card.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/events.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/filed.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/plan.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/project.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/pulse.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/record.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/report.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/review.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/take.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/src/taskops/verbs/update.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/__init__.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/conftest.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_architecture.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_claude.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_core.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_git.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_mcp.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_report.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_store.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_ui.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/test_window.py +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/tests/ui_harness.js +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/build.mjs +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/index.html +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/package-lock.json +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/package.json +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/fixture.json +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/main.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/run.mjs +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/actors-window-filter.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/actors.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/board-columns.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/board-flow-view.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapter-landed.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/chapters-open.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/comment-box.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/diff-page.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/dossier.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/event-stream.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/feed-reconnect.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/forge-links.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/monitor-panes.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/overlay-escape.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/report-markdown.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/report-sandbox.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/reviewing-skew.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/section.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/swarm-topology.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/thread-closing-note.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections/worktrees-index.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/smoke/sections.mjs +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/client.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/completed.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/actors/Daysheet.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/actors/DevPanel.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/CardTile.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/Column.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/ViewToggle.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/flip.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/board/useFlip.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/CommentBox.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Drawer.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Sections.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/Thread.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/split.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/card/tokens.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/AvatarStack.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/Header.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/KpiRail.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/chrome/TabNav.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/flow/FlowView.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/flow/layout.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Chapter.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/DependencyChain.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/EditSurface.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/EventStream.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/LiveLeases.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Mentions.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Pane.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Swarm.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/Throughput.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/monitor/panels.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/reports/ReportFrame.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Avatar.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Markdown.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Overlay.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/Segmented.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/shared/overlayStack.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/story/ChapterStory.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/story/stats.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/toasts/ToastStack.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/toasts/model.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/components/toasts/useToasts.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/format.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/hoursWindow.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/main.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/markdown.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Actors.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Board.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Monitor.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/WorktreeDiff.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/pages/Worktrees.tsx +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/theme/theme.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/theme/tokens.css +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/types.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/src/useEvents.ts +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/styles/index.css +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/ui/tsconfig.json +0 -0
- {taskops_cli-0.4.1 → taskops_cli-0.5.0}/uv.lock +0 -0
|
@@ -1012,7 +1012,8 @@ Both remaining items are done. The migration ran against all four v1 boards
|
|
|
1012
1012
|
directory of board logs, so it is `pip install` into a venv and one pm2 entry
|
|
1013
1013
|
(README, "Deployed"). **The box runs this tree** since 2026-08-09 (tk-df8e64,
|
|
1014
1014
|
§17), so `https://taskops.bernardocastro.dev/<board>/ui/` answers `410` and one
|
|
1015
|
-
sentence, exactly as the trunk does (§16, §17)
|
|
1015
|
+
sentence, exactly as the trunk does (§16, §17) — until a board with a declared
|
|
1016
|
+
forge gets its hosted window (§16, "The hosted window"). Re-derivable:
|
|
1016
1017
|
`curl -s https://taskops.bernardocastro.dev/healthz` and
|
|
1017
1018
|
`curl -w '%{http_code}' https://taskops.bernardocastro.dev/axion/ui/`
|
|
1018
1019
|
— measured `{"boards": 4}` (after all four boards had been addressed through
|
|
@@ -1361,7 +1362,17 @@ files that answer it:
|
|
|
1361
1362
|
* **the card dossier drawer**, opening over Monitor and Board through `App`'s
|
|
1362
1363
|
`openCard` — it renders the acceptance criteria no v1 screen ever drew, and
|
|
1363
1364
|
carries the UI's ONE write, the comment box with its mention picker
|
|
1364
|
-
. The header's milestone picker scopes every tab at once
|
|
1365
|
+
. The header's milestone picker scopes every tab at once, and **"all
|
|
1366
|
+
chapters" is an ARGUMENT** — `milestone=*` (`core/types.py::EVERYTHING`), not
|
|
1367
|
+
the absence of one. It sent nothing for a wave, which the server reads as
|
|
1368
|
+
"resolve the scope yourself" and resolves to the single open chapter: the page
|
|
1369
|
+
arrived narrowed to that chapter with the ✓ sitting on "all chapters" the whole
|
|
1370
|
+
time, and clicking the option changed no argument (2026-08-18). Absence still
|
|
1371
|
+
means exactly that — it is the dashboard's OPENING state, and `App::scopeOf`
|
|
1372
|
+
resolves it against the chapter the answer came back scoped to, so the ✓ marks
|
|
1373
|
+
the scope the page is actually drawn at. `verbs/activity.py` refuses `*` in the
|
|
1374
|
+
words of its own doors: a story is one chapter's cards by definition.
|
|
1375
|
+
`ui/smoke/sections/all-chapters-scope.tsx`.
|
|
1365
1376
|
|
|
1366
1377
|
* **the comment toasts**, a stack bottom-right over every tab
|
|
1367
1378
|
(`ui/src/components/toasts/`), because the UI's one write was invisible to
|
|
@@ -1495,15 +1506,18 @@ construction and never sniffed per request** (`Mounts(repo=…)`,
|
|
|
1495
1506
|
| `taskops serve` | a directory of boards, no checkout | **404 with the reason spelled out**, and nothing faked | **410 and one sentence** |
|
|
1496
1507
|
|
|
1497
1508
|
The `/ui/` column arrived later, and it is the same `repo` deciding both — see
|
|
1498
|
-
"API ONLY is now literal" below.
|
|
1509
|
+
"API ONLY is now literal" below. Both cells of the `serve` row are narrowed a
|
|
1510
|
+
second time by "The hosted window" below: a board that DECLARED a forge may be
|
|
1511
|
+
answered from a bare read-only mirror of it, and only then.
|
|
1499
1512
|
|
|
1500
1513
|
A viewer meets that gap whenever this clone cannot answer — a ref not fetched
|
|
1501
1514
|
yet, a shallow checkout, a commit from before this repo had the branch — and
|
|
1502
1515
|
there the UI's declared cascade takes over: numstat from the event → patch from
|
|
1503
1516
|
/git → the forge link if a slug exists → one honest sentence. No dead anchor, no
|
|
1504
1517
|
empty pane pretending. (The gap used to have a second, worse shape: a browser
|
|
1505
|
-
with no clone at all, pointed at the hosted page.
|
|
1506
|
-
|
|
1518
|
+
with no clone at all, pointed at the hosted page. That page was withdrawn — see
|
|
1519
|
+
below — and comes back only standing on a real history: the forge mirror, in
|
|
1520
|
+
"The hosted window".)
|
|
1507
1521
|
|
|
1508
1522
|
That cascade is ONE function — `ui/src/links.tsx::cascade`, beside the slug and
|
|
1509
1523
|
the link templates it already owned — and `ui/src/components/card/Patch.tsx`
|
|
@@ -1585,9 +1599,11 @@ doubled separator and the directory itself, and never repairs one: a traversal
|
|
|
1585
1599
|
normalised into something acceptable is the bug. Anything outside
|
|
1586
1600
|
`.taskops/reports/` is a 400 that names the directory, and `tests/test_topology.py`
|
|
1587
1601
|
walks every shape (a real secret one directory up included, asserting its
|
|
1588
|
-
contents appear nowhere in the answer). `taskops serve`
|
|
1589
|
-
|
|
1590
|
-
question underneath it opens no door.
|
|
1602
|
+
contents appear nowhere in the answer). `taskops serve` mounts no `/git`
|
|
1603
|
+
from a checkout it does not have: the switch is `Mounts.repo`, decided once at
|
|
1604
|
+
construction, and a new question underneath it opens no door. The hosted-window
|
|
1605
|
+
amendment below changes what `repo` may point AT — a bare mirror of the
|
|
1606
|
+
declared forge — never the switch itself.
|
|
1591
1607
|
|
|
1592
1608
|
### The window: `taskops ui` serves locally even for a REMOTE board (2026-08-08)
|
|
1593
1609
|
|
|
@@ -1626,7 +1642,7 @@ The routes do not change, so the committed bundle is untouched by any of this:
|
|
|
1626
1642
|
decision). The server used to serve the bundle too, from a `--ui` flag with a
|
|
1627
1643
|
packaged default — so the box above answered `/ui/` with a real dashboard, and
|
|
1628
1644
|
that dashboard could not draw a single patch, because the machine it was served
|
|
1629
|
-
from deliberately
|
|
1645
|
+
from deliberately had no clone. It was a degraded window presented as *the*
|
|
1630
1646
|
window; the redirect above merely pointed people at it. **The binary serves the
|
|
1631
1647
|
window; the server serves the truth.**
|
|
1632
1648
|
|
|
@@ -1634,7 +1650,9 @@ So: `Mounts.ui` is not a parameter, it is `repo`'s shadow — one construction-t
|
|
|
1634
1650
|
switch mounts `/git` AND the bundle, and only a process standing in a checkout
|
|
1635
1651
|
has either. `/ui/` on a board host answers `static.py::NO_UI` as plain text with
|
|
1636
1652
|
**410 Gone**, not 404: 404 says "no idea, maybe later", and this page was
|
|
1637
|
-
withdrawn on purpose and is never coming back to a host with no clone
|
|
1653
|
+
withdrawn on purpose and is never coming back to a host with no clone — and a
|
|
1654
|
+
host holding the board's forge MIRROR is no longer that host ("The hosted
|
|
1655
|
+
window", below). Plain
|
|
1638
1656
|
text and not the JSON envelope because the only reader who reaches that door is
|
|
1639
1657
|
a human with a browser. The `--ui` flag is REMOVED, not deprecated: there is no
|
|
1640
1658
|
command line that puts the decision back. The bundle still ships inside the
|
|
@@ -1659,6 +1677,21 @@ local process**; the browser holds only the token `taskops ui` mints into
|
|
|
1659
1677
|
`ui.json`, which is local and revocable. `tests/test_topology.py` searches every
|
|
1660
1678
|
served byte, and their headers, for the team token.
|
|
1661
1679
|
|
|
1680
|
+
**A SESSION RUNS OUT AND THE WINDOW DOES NOT** (2026-08-18). A host session is
|
|
1681
|
+
twelve hours (`http/login.py::SESSION_TTL`), and a window is a thing you leave
|
|
1682
|
+
open: `_upstream` read the token lying in `remote.json` and `Upstream` held it
|
|
1683
|
+
for the life of the process, so the morning after, every read answered "that
|
|
1684
|
+
credential expired" — and the habit that taught was `taskops join`, a human
|
|
1685
|
+
re-pasting a credential the machine can mint itself, which is the exact thing
|
|
1686
|
+
the session chapter exists to end. Two halves, both fixed: the command opens
|
|
1687
|
+
with `session.fresh`, like every other command has since that chapter landed,
|
|
1688
|
+
and the window is handed `session.refresher` so it can sign in AGAIN mid-flight
|
|
1689
|
+
— `board.py::RemoteBoard.call`'s retry, narrow for the same reason (the server
|
|
1690
|
+
said `EXPIRED`, this process holds a key). A window with no way back in — a
|
|
1691
|
+
standing bearer, a public viewer — is unchanged: `refresh` is None and nobody
|
|
1692
|
+
replaces a token behind its owner's back (`session.py`). `Upstream` still knows
|
|
1693
|
+
nothing about keys or files: it is handed a callable.
|
|
1694
|
+
|
|
1662
1695
|
**A refusal from the remote arrives in the SERVER's own words**, status and all:
|
|
1663
1696
|
`upstream.py` returns `(status, bytes)` and re-wraps only a body that is not an
|
|
1664
1697
|
envelope. A board that says "held by agent:berna/w1" with a 409 must not reach
|
|
@@ -1684,6 +1717,66 @@ move a branch under a worktree somebody is sitting in.
|
|
|
1684
1717
|
|
|
1685
1718
|
A repo joined to nothing behaves exactly as it did.
|
|
1686
1719
|
|
|
1720
|
+
### The hosted window: the host may hold a MIRROR of the declared forge (decided 2026-08-30)
|
|
1721
|
+
|
|
1722
|
+
The first amendment narrowed "no server reads a repo" to "a host that sits in a
|
|
1723
|
+
repo may read it". This one narrows the other clause — *the server deliberately
|
|
1724
|
+
has no clone* — and, as before, the change is recorded here rather than quietly
|
|
1725
|
+
made:
|
|
1726
|
+
|
|
1727
|
+
> **A board host MAY hold a bare, READ-ONLY mirror of the board's DECLARED
|
|
1728
|
+
> forge — and only then does it answer `/git` and `/ui/`.** A board with no
|
|
1729
|
+
> declared forge is exactly what it was: 404 on `/git`, 410 on `/ui/`, each
|
|
1730
|
+
> refusal naming `taskops board forge <owner>/<repo>` as the door.
|
|
1731
|
+
|
|
1732
|
+
Why this is not the degraded window "API ONLY" withdrew. That window's flaw was
|
|
1733
|
+
never that a server drew a dashboard — it was that the machine drawing it had
|
|
1734
|
+
NO git at all, so every diff fell through the whole cascade to a forge link or
|
|
1735
|
+
a sentence, a blank presented as *the* window. The mirror removes the flaw, not
|
|
1736
|
+
the caution: it is **the same history the forge already holds** — `git clone
|
|
1737
|
+
--mirror` of the repo the owner declared with `board forge`, refreshed by ONE
|
|
1738
|
+
bounded on-demand fetch when a requested ref is missing, then answered stale
|
|
1739
|
+
rather than blocking — so a diff served from it is the diff GitHub would show,
|
|
1740
|
+
read by the same `gitwork/diff.py` doors, behind the same visibility rules as
|
|
1741
|
+
`/rpc`. And the dev-side story does not move an inch: `taskops ui` still serves
|
|
1742
|
+
the window from the checkout it stands in, reading the dev's OWN clone,
|
|
1743
|
+
including refs the forge has never seen. The hosted window is for the reader
|
|
1744
|
+
who has no clone — the case the local window, by construction, cannot serve.
|
|
1745
|
+
|
|
1746
|
+
**Where it lives**: `<root>/<board>/mirror.git`, a bare repo beside the board's
|
|
1747
|
+
`events.jsonl` — later cards follow this wording. It is DERIVED and disposable
|
|
1748
|
+
in exactly the sense `cache.sqlite` is: delete it and it re-clones from the
|
|
1749
|
+
declared forge; nothing in it is truth the forge does not hold. Its ONLY source
|
|
1750
|
+
is that forge. It takes no client pushes — the host still exposes no receive
|
|
1751
|
+
door of any kind — and it is not a second trunk: nothing lands there, nothing
|
|
1752
|
+
is read back into the log from it, and `taskops_merge` still happens in the
|
|
1753
|
+
dev's shared checkout.
|
|
1754
|
+
|
|
1755
|
+
**The credential story** is §19's, unchanged in kind. A public repo needs no
|
|
1756
|
+
credential at all. A private repo is fetched with an **ssh deploy key on the
|
|
1757
|
+
host's filesystem** — read-only, minted and installed by the OWNER, revocable
|
|
1758
|
+
on the forge like any deploy key — which is the ONE new credential this chapter
|
|
1759
|
+
adds, and it is the owner's business exactly as `board forge` itself is. What
|
|
1760
|
+
remains banned is what §11 and §19 already ban: a stored GitHub token (the
|
|
1761
|
+
mirror's remote is an address, never a token), a write credential in any form,
|
|
1762
|
+
GitHub as a second identity system, and any dev credential travelling to the
|
|
1763
|
+
host — nobody's `gh auth token` is involved, because the owner already declared
|
|
1764
|
+
the repo and the mirror pulls from the outside like any anonymous (or
|
|
1765
|
+
deploy-keyed) clone.
|
|
1766
|
+
|
|
1767
|
+
**Still banned, spelled out**: client pushes to the host and git replication
|
|
1768
|
+
between clones (the mirror replicates FROM the forge, one direction, and no
|
|
1769
|
+
clone ever pushes to it); a write credential on the host; a second trunk; a
|
|
1770
|
+
`--ui` flag or any per-request sniffing (whether the mirror exists is decided
|
|
1771
|
+
at mount time, the same `Mounts.repo` switch, now pointable at `mirror.git`);
|
|
1772
|
+
and anonymous writes — a public board's hosted window is a READ, and it leaves
|
|
1773
|
+
no `presence` row.
|
|
1774
|
+
|
|
1775
|
+
So `static.py::NO_UI` and `gitdoor.py::NO_REPO` change meaning, not words of
|
|
1776
|
+
intent: each stops saying "this host never has a repo" and starts saying "this
|
|
1777
|
+
BOARD declared no forge, so this host holds no mirror to read from" — a state
|
|
1778
|
+
the owner ends with one `taskops board forge`. The refusal still names the door.
|
|
1779
|
+
|
|
1687
1780
|
### A concept named by two cards is a seam — the fan-out rule, third notch
|
|
1688
1781
|
|
|
1689
1782
|
`gitwork/remote.py` was born TWICE in one wave: two workers, each over the
|
|
@@ -1778,7 +1871,9 @@ request (`http/mounts.py::stores`), so a just-restarted process says `1`, not
|
|
|
1778
1871
|
|
|
1779
1872
|
**That md5-of-the-served-bundle check is history, not the procedure any more.**
|
|
1780
1873
|
Since §16's "API ONLY" amendment the host serves no bundle: `/<board>/ui/`
|
|
1781
|
-
answers `410` and one sentence
|
|
1874
|
+
answers `410` and one sentence (until the hosted-window chapter deploys a board
|
|
1875
|
+
whose forge is declared — §16, "The hosted window"), so there is nothing at the
|
|
1876
|
+
domain to md5. The
|
|
1782
1877
|
wheel is still checked for its markers *before* it ships — it is the same wheel
|
|
1783
1878
|
teammates install to get `taskops ui` — and the domain is verified by DATA,
|
|
1784
1879
|
through `/rpc`.
|
|
@@ -3,6 +3,83 @@
|
|
|
3
3
|
The source of truth for release notes — GitHub Releases are extracted from
|
|
4
4
|
here, never written twice.
|
|
5
5
|
|
|
6
|
+
## 0.5.0 — the hosted window
|
|
7
|
+
|
|
8
|
+
- **A board can be READ without a clone.** Until now the dashboard was only ever
|
|
9
|
+
served by `taskops ui`, a binary standing in a checkout: a host running
|
|
10
|
+
`taskops serve` answered `/git` with a 404 and `/ui/` with a 410, because a
|
|
11
|
+
dashboard draws DIFFS and the host deliberately had no repository to read them
|
|
12
|
+
from. Serving a page that could not draw a single patch was a degraded window
|
|
13
|
+
presented as *the* window, so it was withdrawn. That reasoning had one
|
|
14
|
+
unexamined premise — that the host can never have git — and this release
|
|
15
|
+
removes it, argued in ARCHITECTURE §16 ("The hosted window") before a line of
|
|
16
|
+
code moved.
|
|
17
|
+
- **The host may hold a bare, READ-ONLY mirror of the board's DECLARED forge**,
|
|
18
|
+
at `<root>/<board>/mirror.git`, and only then does it answer `/git` and
|
|
19
|
+
`/ui/`. The mirror is derived and disposable in exactly the sense
|
|
20
|
+
`cache.sqlite` is: delete it and it re-clones; nothing in it is truth the
|
|
21
|
+
forge does not already hold. Its ONLY source is the repo the owner declared
|
|
22
|
+
with `taskops board forge` — it takes no client pushes, it is not a second
|
|
23
|
+
trunk, and nothing is ever read back into the log from it.
|
|
24
|
+
- **A board that declared no forge is exactly what it was**: 404 on `/git`,
|
|
25
|
+
410 on `/ui/`, and each refusal now names the door — `taskops board forge
|
|
26
|
+
<owner>/<repo>` — instead of stranding the reader in a sentence about a host
|
|
27
|
+
they cannot change.
|
|
28
|
+
- **No new credential type.** A public repo is mirrored anonymously; a private
|
|
29
|
+
one is fetched with an ssh deploy key on the host's filesystem, installed by
|
|
30
|
+
the OWNER, revocable on the forge — the one new credential this chapter adds,
|
|
31
|
+
and it is the owner's business exactly as `board forge` itself is. No stored
|
|
32
|
+
GitHub token, no dev credential travelling, no second identity system.
|
|
33
|
+
- **The page opens on the FACT, never on a clone.** `/ui/` is granted by the
|
|
34
|
+
declared forge alone: the bundle's bytes come from the installed package, so
|
|
35
|
+
putting a network clone in front of `index.html` would take a static page down
|
|
36
|
+
whenever the forge is unreachable. The mirror resolves lazily the moment a
|
|
37
|
+
diff is actually read — which is where a failure belongs, in the diff pane, in
|
|
38
|
+
the door's own words.
|
|
39
|
+
- **A missing ref buys exactly ONE bounded fetch**, then answers stale rather
|
|
40
|
+
than blocking a page on a forge that is down — and only on a mirror. A
|
|
41
|
+
window's own clone still fetches nothing on the reader's behalf: a background
|
|
42
|
+
fetch inside a read-only door would move a branch under a worktree somebody is
|
|
43
|
+
sitting in.
|
|
44
|
+
- **Reads stay reads.** `/ui/` sits behind the credential `/rpc` asks for —
|
|
45
|
+
a `public` board serves the page to an anonymous reader, a private one refuses
|
|
46
|
+
with the same join sentence — and an anonymous page load writes nothing at
|
|
47
|
+
all, not even the invisible write: no presence row.
|
|
48
|
+
|
|
49
|
+
## 0.4.2 — the filter that filtered nothing, and the window that stopped signing in
|
|
50
|
+
|
|
51
|
+
- **"All chapters" is an ARGUMENT now, and it really shows every chapter.** The
|
|
52
|
+
option sent no `milestone=` at all — and an absent milestone never meant "the
|
|
53
|
+
whole board": it means "server, resolve the scope yourself", which resolves to
|
|
54
|
+
the single open chapter when there is exactly one. So on a board with one
|
|
55
|
+
chapter open and seven landed (Berna's, reported 2026-08-18) the page came back
|
|
56
|
+
narrowed to that chapter, the menu drew its ✓ beside a scope that had never
|
|
57
|
+
been in effect, and clicking the option changed no argument at all — the only
|
|
58
|
+
way to see other work was to name every chapter in turn. Board-wide is now
|
|
59
|
+
asked for by name (`milestone=*`, `core/types.py::EVERYTHING`). Absence still
|
|
60
|
+
means exactly what it meant, so no agent and no existing caller sees any
|
|
61
|
+
change: it is the dashboard's OPENING state, and the ✓ now falls on the chapter
|
|
62
|
+
the server actually resolved, so the pill can no longer claim a scope the page
|
|
63
|
+
is not drawn at. `taskops_activity` refuses `*` in the words of its own doors —
|
|
64
|
+
a story is one chapter's cards by definition — where it used to answer
|
|
65
|
+
`milestone * does not exist`, a lookup's answer to a question that was never a
|
|
66
|
+
lookup.
|
|
67
|
+
- **A window whose session ran out signs in again instead of teaching `taskops
|
|
68
|
+
join`.** A host session is twelve hours and a window is a thing you leave open.
|
|
69
|
+
Two halves of one hole, both on the `taskops ui` path and nowhere else: the
|
|
70
|
+
command read the token lying in `remote.json` instead of `session.fresh` —
|
|
71
|
+
which every other command has taken since sessions landed — so a window opened
|
|
72
|
+
on yesterday's session was dead on arrival; and the forwarder held the token it
|
|
73
|
+
was built with for the life of the process, so a window that was open when the
|
|
74
|
+
session expired answered "that credential expired" to every read. Neither path
|
|
75
|
+
could come back, so the habit it taught was re-joining: a human pasting a
|
|
76
|
+
credential the machine can mint itself from the key it already has. The retry
|
|
77
|
+
is `RemoteBoard.call`'s and just as narrow — the SERVER said EXPIRED, and this
|
|
78
|
+
process holds a key — and it is a callable passed in, so the forwarder still
|
|
79
|
+
knows nothing about keys, files or logins. A window with no way back in (a
|
|
80
|
+
standing bearer token, a public viewer) is unchanged: nobody replaces a token
|
|
81
|
+
behind its owner's back.
|
|
82
|
+
|
|
6
83
|
## 0.4.1 — the board comes alive: the forge enrols the team, hours stop lying, comments become visible
|
|
7
84
|
|
|
8
85
|
- **A comment now SAYS so, on every open dashboard.** The UI's one write was
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskops-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A shared work board for teams of coding agents, with a human who decides.
|
|
5
5
|
Project-URL: Repository, https://github.com/bernatch22/taskops
|
|
6
6
|
License-Expression: MIT
|
|
@@ -70,7 +70,7 @@ importable leaves commits un-stamped, silently.
|
|
|
70
70
|
taskops init a local board in this repo
|
|
71
71
|
taskops join [<name>] [--invite <id>] join a board, install the hooks
|
|
72
72
|
taskops remote add <url> the host this checkout operates, like git's origin
|
|
73
|
-
taskops serve host boards —
|
|
73
|
+
taskops serve host boards — /ui and /git open per declared forge
|
|
74
74
|
taskops server init bootstrap THIS host: its owner and their ssh key
|
|
75
75
|
taskops board create|ls|push|pull|rm the boards on a host
|
|
76
76
|
taskops board visibility|forge who may read one · which repo's team is enrolled
|
|
@@ -383,9 +383,11 @@ to bytes that are not in history yet is a pointer to nothing.
|
|
|
383
383
|
```
|
|
384
384
|
|
|
385
385
|
Then `taskops ui` lists it under the chapter's **Reports** tab and renders it
|
|
386
|
-
full width, read out of **your own clone** at that sha — the dashboard
|
|
387
|
-
asks the server for the bytes
|
|
388
|
-
|
|
386
|
+
full width, read out of **your own clone** at that sha — the local dashboard
|
|
387
|
+
never asks the server for the bytes. A host running `taskops serve` answers
|
|
388
|
+
`/git` too, from a bare read-only mirror of the board's DECLARED forge — the
|
|
389
|
+
hosted window, for the reader with no clone. A board that never declared one
|
|
390
|
+
refuses with a sentence naming `taskops board forge <owner>/<repo>`.
|
|
389
391
|
|
|
390
392
|
**Reading one, from the other side.** Everything above is the author's half; a
|
|
391
393
|
reader needs no ceremony at all. Three doors onto the same bytes:
|
|
@@ -58,7 +58,7 @@ importable leaves commits un-stamped, silently.
|
|
|
58
58
|
taskops init a local board in this repo
|
|
59
59
|
taskops join [<name>] [--invite <id>] join a board, install the hooks
|
|
60
60
|
taskops remote add <url> the host this checkout operates, like git's origin
|
|
61
|
-
taskops serve host boards —
|
|
61
|
+
taskops serve host boards — /ui and /git open per declared forge
|
|
62
62
|
taskops server init bootstrap THIS host: its owner and their ssh key
|
|
63
63
|
taskops board create|ls|push|pull|rm the boards on a host
|
|
64
64
|
taskops board visibility|forge who may read one · which repo's team is enrolled
|
|
@@ -371,9 +371,11 @@ to bytes that are not in history yet is a pointer to nothing.
|
|
|
371
371
|
```
|
|
372
372
|
|
|
373
373
|
Then `taskops ui` lists it under the chapter's **Reports** tab and renders it
|
|
374
|
-
full width, read out of **your own clone** at that sha — the dashboard
|
|
375
|
-
asks the server for the bytes
|
|
376
|
-
|
|
374
|
+
full width, read out of **your own clone** at that sha — the local dashboard
|
|
375
|
+
never asks the server for the bytes. A host running `taskops serve` answers
|
|
376
|
+
`/git` too, from a bare read-only mirror of the board's DECLARED forge — the
|
|
377
|
+
hosted window, for the reader with no clone. A board that never declared one
|
|
378
|
+
refuses with a sentence naming `taskops board forge <owner>/<repo>`.
|
|
377
379
|
|
|
378
380
|
**Reading one, from the other side.** Everything above is the author's half; a
|
|
379
381
|
reader needs no ceremony at all. Three doors onto the same bytes:
|
|
@@ -25,7 +25,7 @@ if TYPE_CHECKING: # a NAME for the annotation, never an import at start-up
|
|
|
25
25
|
from ..http.upstream import Upstream
|
|
26
26
|
|
|
27
27
|
from . import window
|
|
28
|
-
from .. import _clock
|
|
28
|
+
from .. import _clock, session
|
|
29
29
|
from .._json import as_object
|
|
30
30
|
from ..board import DIR, find_root, is_project, read_config
|
|
31
31
|
from .._errors import TaskopsError
|
|
@@ -133,7 +133,13 @@ def _upstream(root: Path) -> Upstream | None:
|
|
|
133
133
|
url = str(config.get("url", ""))
|
|
134
134
|
if not url:
|
|
135
135
|
return None
|
|
136
|
-
|
|
136
|
+
# The SESSION, refreshed here if it has run out and refreshable again from
|
|
137
|
+
# inside the window if it runs out while somebody is reading (2026-08-18,
|
|
138
|
+
# `http/upstream.py`): a host session is twelve hours and this window is
|
|
139
|
+
# meant to be left open. `fresh` falls back to the cached token whenever it
|
|
140
|
+
# cannot sign in, and `refresher` is None for a standing bearer — neither
|
|
141
|
+
# replaces a token nobody may replace, which is `session.py`'s own rule.
|
|
142
|
+
token = str(session.fresh(root, config, _clock.now()))
|
|
137
143
|
if not token and config.get("readonly"):
|
|
138
144
|
# A viewer's window (`commands.py::_watch`): an Upstream with no bearer.
|
|
139
145
|
# `Mounts.public` reads exactly this — no token means this window is
|
|
@@ -145,7 +151,7 @@ def _upstream(root: Path) -> Upstream | None:
|
|
|
145
151
|
f"{root / DIR}/board.json points at {url} but there is no credential in "
|
|
146
152
|
"remote.json — run: taskops join <url with ?token= or ?invite=>"
|
|
147
153
|
)
|
|
148
|
-
return Remote(url, token)
|
|
154
|
+
return Remote(url, token, refresh=session.refresher(root, config))
|
|
149
155
|
|
|
150
156
|
|
|
151
157
|
def _open(url: str) -> int:
|
|
@@ -161,6 +161,18 @@ MILESTONE_STATUSES = ("open", "done", "dropped")
|
|
|
161
161
|
CLOSED = ("done", "dropped")
|
|
162
162
|
LEASE_TTL = 900.0 # 15 min; every MCP call renews it
|
|
163
163
|
PROJECT = "project" # the `task` of board-level events
|
|
164
|
+
EVERYTHING = "*"
|
|
165
|
+
"""`milestone=*`: the WHOLE board, said out loud — no chapter in scope.
|
|
166
|
+
|
|
167
|
+
An ABSENT milestone means "resolve it yourself" (`verbs/_facts.py::in_scope`),
|
|
168
|
+
which with exactly one open chapter IS that chapter. Right for an agent, and it
|
|
169
|
+
left the dashboard's "all chapters" unreachable: it sent no `milestone=`, the
|
|
170
|
+
board came back narrowed anyway, and the picker drew a ✓ beside a scope that had
|
|
171
|
+
never been in effect and that clicking could not change (2026-08-18). Absent and
|
|
172
|
+
"all" are two questions; they now have two arguments, and nothing that omitted
|
|
173
|
+
it sees any change. `verbs/activity.py` is where the two genuinely part — it
|
|
174
|
+
REQUIRES a chapter, and answered `milestone * does not exist`, a lookup's answer
|
|
175
|
+
to a question that was never a lookup."""
|
|
164
176
|
|
|
165
177
|
|
|
166
178
|
# Fields of a Card that `edited` may target. Anything else is a BadRequest, so
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""The board's bare mirror — the forge's history, held so a host can answer.
|
|
2
|
+
|
|
3
|
+
ARCHITECTURE §16 ("The hosted window") decided this on 2026-08-30: a board
|
|
4
|
+
host MAY hold `<root>/<board>/mirror.git`, a bare `git clone --mirror` of the
|
|
5
|
+
forge the owner declared with `board forge` — and only then does it answer
|
|
6
|
+
`/git` and `/ui/`. The mirror is DERIVED and disposable in exactly the sense
|
|
7
|
+
`cache.sqlite` is: delete it and `ensure` re-clones; nothing in it is truth
|
|
8
|
+
the forge does not hold. Its ONLY source is that forge — it takes no client
|
|
9
|
+
pushes, it is not a second trunk, and nothing here ever writes to it beyond
|
|
10
|
+
what `clone` and `fetch` are.
|
|
11
|
+
|
|
12
|
+
**The address is derived, never a credential.** The default remote is
|
|
13
|
+
`https://<host>/<repo>.git`, spelled straight out of the declared fact
|
|
14
|
+
(`core/forge.py` owns that shape) — an anonymous clone, which is all a public
|
|
15
|
+
repo needs. A private repo is the owner's business: they point the mirror at
|
|
16
|
+
an ssh remote backed by a deploy key on the host's filesystem (`url=` at
|
|
17
|
+
clone time, or `git remote set-url` in a mirror that already exists), and
|
|
18
|
+
this module neither knows nor stores anything about it. What §11 bans stays
|
|
19
|
+
banned: no stored token, no write credential, no dev credential travelling.
|
|
20
|
+
|
|
21
|
+
**Failures return None/False, never raise into a request.** These functions
|
|
22
|
+
sit behind HTTP doors serving readers who cannot fix the host's network; a
|
|
23
|
+
mirror that cannot answer is a stale page, not a 500. The same reasoning
|
|
24
|
+
gave `remote.py::push` its 10-second budget, and `fetch` inherits the number:
|
|
25
|
+
a refresh is never a gate, so it may never cost more than a moment. The one
|
|
26
|
+
bounded on-demand fetch lives in `refresh_if_missing` — a requested ref that
|
|
27
|
+
is absent buys exactly one fetch, then the answer is whatever is true, stale
|
|
28
|
+
included. Answering stale beats blocking a page on a forge that is down.
|
|
29
|
+
|
|
30
|
+
All subprocess goes through `run.py`, the one module allowed to import it —
|
|
31
|
+
and `run.git` RAISES on a timeout (right for workers, wrong for a request),
|
|
32
|
+
so the catch here is part of the contract, not belt-and-braces.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from . import run
|
|
40
|
+
from ..core import forge as forge_facts
|
|
41
|
+
from .._errors import TaskopsError
|
|
42
|
+
|
|
43
|
+
__all__ = ["ensure", "fetch", "refresh_if_missing"]
|
|
44
|
+
|
|
45
|
+
MIRROR = "mirror.git"
|
|
46
|
+
"""§16's wording, verbatim: `<root>/<board>/mirror.git`, beside events.jsonl."""
|
|
47
|
+
|
|
48
|
+
FETCH_TIMEOUT = 10.0
|
|
49
|
+
"""`remote.py::PUSH_TIMEOUT`'s argument, pointing the other way: a refresh is
|
|
50
|
+
never a gate. Worst case a reader waits ten seconds ONCE and gets the truth
|
|
51
|
+
about this disk — which is what `gitdoor` answers anyway."""
|
|
52
|
+
|
|
53
|
+
CLONE_TIMEOUT = 120.0
|
|
54
|
+
"""A first clone moves the whole history, once, at mount time — not per
|
|
55
|
+
request — so it gets `run.TIMEOUT`'s patience rather than `fetch`'s."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _url(forge: object) -> str:
|
|
59
|
+
"""The declared fact → the anonymous https remote, or "".
|
|
60
|
+
|
|
61
|
+
`forge_facts.understood` already collapses absent, cleared and
|
|
62
|
+
unintelligible to None, so an https URL is only ever built from a fact a
|
|
63
|
+
door would also act on. No token, no user@ — the address IS the whole
|
|
64
|
+
credential story for a public repo.
|
|
65
|
+
"""
|
|
66
|
+
fact = forge_facts.understood(forge)
|
|
67
|
+
if fact is None:
|
|
68
|
+
return ""
|
|
69
|
+
return f"https://{fact['host']}/{fact['repo']}.git"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ensure(board_dir: Path, forge: object, *, url: str = "") -> Path | None:
|
|
73
|
+
"""The mirror's path — cloning it into existence if this is the first ask.
|
|
74
|
+
|
|
75
|
+
An EXISTING `mirror.git` is returned untouched, whatever remote it holds:
|
|
76
|
+
that is how an owner's ssh remote (a deploy key's address) survives every
|
|
77
|
+
later call. `url=` overrides the derived address at clone time — the same
|
|
78
|
+
owner's move, and how tests clone from a local fixture instead of the
|
|
79
|
+
network. No forge and no url means no mirror, which is the state every
|
|
80
|
+
board is born in; a clone that fails leaves nothing behind, so the next
|
|
81
|
+
ask is a clean retry rather than a corpse mistaken for a mirror.
|
|
82
|
+
"""
|
|
83
|
+
dest = board_dir / MIRROR
|
|
84
|
+
if dest.is_dir():
|
|
85
|
+
return dest
|
|
86
|
+
remote = url or _url(forge)
|
|
87
|
+
if not remote:
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
result = run.git("clone", "--mirror", remote, str(dest), timeout=CLONE_TIMEOUT)
|
|
91
|
+
except TaskopsError:
|
|
92
|
+
result = None # timeout, or no git at all — a request never hears it
|
|
93
|
+
if result is None or not result.ok:
|
|
94
|
+
return None
|
|
95
|
+
return dest
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def fetch(mirror: Path) -> bool:
|
|
99
|
+
"""One bounded fetch from the mirror's own remote. True means it ran clean.
|
|
100
|
+
|
|
101
|
+
A `--mirror` clone's fetch refspec already maps every ref one-to-one, so
|
|
102
|
+
plain `git fetch` IS the whole refresh — no refspec spelled here that
|
|
103
|
+
could drift from what clone configured. False covers everything a reader
|
|
104
|
+
cannot fix: a dead remote, a timeout, a directory that stopped being a
|
|
105
|
+
repo. The caller answers with what it has.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
return run.git("fetch", cwd=mirror, timeout=FETCH_TIMEOUT).ok
|
|
109
|
+
except TaskopsError: # timeout, or no git at all — never the reader's problem
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _has(mirror: Path, ref: str) -> bool:
|
|
114
|
+
"""`^{commit}` is load-bearing: bare `rev-parse --verify <40-hex>` answers
|
|
115
|
+
ok for ANY well-formed sha, present or not — a ref arriving as a full sha
|
|
116
|
+
(which is what a diff door holds) would always read as here."""
|
|
117
|
+
return run.git("rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}", cwd=mirror).ok
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def refresh_if_missing(mirror: Path, ref: str) -> bool:
|
|
121
|
+
"""Is `ref` here — after at most ONE fetch if it was not?
|
|
122
|
+
|
|
123
|
+
§16's exact promise: a missing ref triggers one bounded on-demand fetch
|
|
124
|
+
before answering stale. A ref already present costs nothing — no network
|
|
125
|
+
touched, so a page over known history never waits on a forge. A ref still
|
|
126
|
+
absent after the fetch is answered False, truthfully: "not here" is the
|
|
127
|
+
truth about this disk, and the door already knows how to say so.
|
|
128
|
+
"""
|
|
129
|
+
if _has(mirror, ref):
|
|
130
|
+
return True
|
|
131
|
+
fetch(mirror)
|
|
132
|
+
return _has(mirror, ref)
|