taskops-cli 0.4.2__tar.gz → 0.5.1__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.2 → taskops_cli-0.5.1}/ARCHITECTURE.md +175 -10
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/CHANGELOG.md +77 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/CLAUDE.md +11 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/PKG-INFO +27 -5
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/README.md +26 -4
- taskops_cli-0.5.1/smoke.sh +164 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/_version.py +1 -1
- taskops_cli-0.5.1/src/taskops/gitwork/mirror.py +132 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/auth.py +19 -1
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/gitdoor.py +29 -48
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/handler.py +37 -15
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/mounts.py +11 -12
- taskops_cli-0.5.1/src/taskops/http/repos.py +98 -0
- taskops_cli-0.5.1/src/taskops/http/routes.py +32 -0
- taskops_cli-0.5.1/src/taskops/http/stale.py +62 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/static.py +49 -15
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/ui/app.js +7 -7
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/ui/index.html +4 -2
- taskops_cli-0.5.1/tests/test_mirror.py +164 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_topology.py +367 -6
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/index.html +4 -2
- taskops_cli-0.5.1/ui/smoke/sections/board-root-mount.tsx +75 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/git-diff.tsx +6 -2
- taskops_cli-0.5.1/ui/smoke/sections/hosted-window.tsx +96 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/client.ts +24 -3
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/card/Patch.tsx +1 -1
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/links.tsx +5 -3
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/main.tsx +6 -8
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/pages/Reports.tsx +6 -5
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.claude/settings.json +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.gitattributes +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.github/workflows/ci.yml +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.github/workflows/workflow.yml +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.gitignore +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.mcp.json +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board/cache.sqlite +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board/cache.sqlite-shm +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board/cache.sqlite-wal +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board/events.jsonl +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board/live.sqlite +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board/live.sqlite-shm +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board/live.sqlite-wal +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/board.json +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/reports/lifecycle-chapter.html +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/reports/reports-chapter.html +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/.taskops/ui.lock +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/pyproject.toml +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/_clock.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/_errors.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/_ids.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/_json.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/_locate.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/_wire.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/board.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/__main__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/admin.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/claude.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/commands.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/enrol.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/github.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/grants.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/hooks.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/main.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/operate.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/paging.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/parser.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/pull.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/push.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/remote.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/rm.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/serving.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/team.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/watch.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/window.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/cli/wording.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/actors.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/challenge.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/chapters.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/event.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/forge.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/graph.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/holding.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/hours.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/kinds.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/machine.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/mentions.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/replay.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/reports.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/review.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/scope.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/seams.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/core/types.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/bind.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/catchup.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/claudefiles.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/diff.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/install.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/landing.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/patch.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/remote.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/run.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/sig.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/trailer.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/gitwork/trees.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/admin.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/feed.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/grants.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/ingest.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/login.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/members.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/removal.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/rpc.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/scoped.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/server.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/upstream.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/http/watcher.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/identity.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/__main__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/activity.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/before.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/boards.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/boardview.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/brief.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/chapter.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/dossier.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/fields.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/gitmoves.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/hello.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/integrate.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/orders.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/render.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/schema.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/server.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/thread.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/mcp/tools.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/py.typed +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/session.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/cache.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/creds.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/handover.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/live.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/log.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/pubkeys.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/reviews.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/server.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/store/stores.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/ui/style.css +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_args.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_cards.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_chapter.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_context.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_facts.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_mentions.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_rows.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_stories.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_waiting.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/_windows.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/activity.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/assign.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/card.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/events.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/filed.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/plan.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/project.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/pulse.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/record.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/report.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/review.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/take.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/src/taskops/verbs/update.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/__init__.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/conftest.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_architecture.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_claude.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_core.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_git.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_mcp.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_report.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_store.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_ui.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_verbs.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/test_window.py +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/tests/ui_harness.js +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/build.mjs +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/package-lock.json +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/package.json +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/fixture.json +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/main.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/run.mjs +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/actors-window-filter.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/actors.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/all-chapters-scope.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/board-columns.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/board-flow-view.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/chapter-landed.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/chapters-open.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/comment-box.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/diff-page.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/dossier.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/event-stream.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/feed-reconnect.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/forge-links.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/monitor-panes.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/overlay-escape.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/report-markdown.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/report-sandbox.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/reviewing-skew.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/section.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/swarm-topology.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/thread-closing-note.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections/worktrees-index.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/smoke/sections.mjs +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/App.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/completed.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/actors/Daysheet.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/actors/DevPanel.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/board/CardTile.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/board/Column.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/board/ViewToggle.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/board/flip.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/board/useFlip.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/card/CommentBox.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/card/Drawer.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/card/Sections.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/card/Thread.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/card/split.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/card/tokens.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/chrome/AvatarStack.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/chrome/Header.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/chrome/KpiRail.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/chrome/TabNav.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/flow/FlowView.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/flow/layout.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/Chapter.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/DependencyChain.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/EditSurface.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/EventStream.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/LiveLeases.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/Mentions.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/Pane.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/Swarm.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/Throughput.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/monitor/panels.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/reports/ReportFrame.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/shared/Avatar.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/shared/Markdown.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/shared/Overlay.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/shared/Segmented.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/shared/overlayStack.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/story/ChapterStory.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/story/stats.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/toasts/ToastStack.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/toasts/model.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/components/toasts/useToasts.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/format.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/hoursWindow.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/markdown.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/pages/Actors.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/pages/Board.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/pages/Monitor.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/pages/WorktreeDiff.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/pages/Worktrees.tsx +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/theme/theme.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/theme/tokens.css +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/types.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/useBoard.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/src/useEvents.ts +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/styles/index.css +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/ui/tsconfig.json +0 -0
- {taskops_cli-0.4.2 → taskops_cli-0.5.1}/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
|
|
@@ -1505,15 +1506,21 @@ construction and never sniffed per request** (`Mounts(repo=…)`,
|
|
|
1505
1506
|
| `taskops serve` | a directory of boards, no checkout | **404 with the reason spelled out**, and nothing faked | **410 and one sentence** |
|
|
1506
1507
|
|
|
1507
1508
|
The `/ui/` column arrived later, and it is the same `repo` deciding both — see
|
|
1508
|
-
"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. And the page's ADDRESS moved
|
|
1512
|
+
one notch after that: on a serve-mode host the last column is the board's own
|
|
1513
|
+
`/<board>/`, with `/<board>/ui/` kept as an alias — "The board's own address IS
|
|
1514
|
+
the page" at the end of this section.
|
|
1509
1515
|
|
|
1510
1516
|
A viewer meets that gap whenever this clone cannot answer — a ref not fetched
|
|
1511
1517
|
yet, a shallow checkout, a commit from before this repo had the branch — and
|
|
1512
1518
|
there the UI's declared cascade takes over: numstat from the event → patch from
|
|
1513
1519
|
/git → the forge link if a slug exists → one honest sentence. No dead anchor, no
|
|
1514
1520
|
empty pane pretending. (The gap used to have a second, worse shape: a browser
|
|
1515
|
-
with no clone at all, pointed at the hosted page.
|
|
1516
|
-
|
|
1521
|
+
with no clone at all, pointed at the hosted page. That page was withdrawn — see
|
|
1522
|
+
below — and comes back only standing on a real history: the forge mirror, in
|
|
1523
|
+
"The hosted window".)
|
|
1517
1524
|
|
|
1518
1525
|
That cascade is ONE function — `ui/src/links.tsx::cascade`, beside the slug and
|
|
1519
1526
|
the link templates it already owned — and `ui/src/components/card/Patch.tsx`
|
|
@@ -1595,9 +1602,11 @@ doubled separator and the directory itself, and never repairs one: a traversal
|
|
|
1595
1602
|
normalised into something acceptable is the bug. Anything outside
|
|
1596
1603
|
`.taskops/reports/` is a 400 that names the directory, and `tests/test_topology.py`
|
|
1597
1604
|
walks every shape (a real secret one directory up included, asserting its
|
|
1598
|
-
contents appear nowhere in the answer). `taskops serve`
|
|
1599
|
-
|
|
1600
|
-
question underneath it opens no door.
|
|
1605
|
+
contents appear nowhere in the answer). `taskops serve` mounts no `/git`
|
|
1606
|
+
from a checkout it does not have: the switch is `Mounts.repo`, decided once at
|
|
1607
|
+
construction, and a new question underneath it opens no door. The hosted-window
|
|
1608
|
+
amendment below changes what `repo` may point AT — a bare mirror of the
|
|
1609
|
+
declared forge — never the switch itself.
|
|
1601
1610
|
|
|
1602
1611
|
### The window: `taskops ui` serves locally even for a REMOTE board (2026-08-08)
|
|
1603
1612
|
|
|
@@ -1636,7 +1645,7 @@ The routes do not change, so the committed bundle is untouched by any of this:
|
|
|
1636
1645
|
decision). The server used to serve the bundle too, from a `--ui` flag with a
|
|
1637
1646
|
packaged default — so the box above answered `/ui/` with a real dashboard, and
|
|
1638
1647
|
that dashboard could not draw a single patch, because the machine it was served
|
|
1639
|
-
from deliberately
|
|
1648
|
+
from deliberately had no clone. It was a degraded window presented as *the*
|
|
1640
1649
|
window; the redirect above merely pointed people at it. **The binary serves the
|
|
1641
1650
|
window; the server serves the truth.**
|
|
1642
1651
|
|
|
@@ -1644,7 +1653,9 @@ So: `Mounts.ui` is not a parameter, it is `repo`'s shadow — one construction-t
|
|
|
1644
1653
|
switch mounts `/git` AND the bundle, and only a process standing in a checkout
|
|
1645
1654
|
has either. `/ui/` on a board host answers `static.py::NO_UI` as plain text with
|
|
1646
1655
|
**410 Gone**, not 404: 404 says "no idea, maybe later", and this page was
|
|
1647
|
-
withdrawn on purpose and is never coming back to a host with no clone
|
|
1656
|
+
withdrawn on purpose and is never coming back to a host with no clone — and a
|
|
1657
|
+
host holding the board's forge MIRROR is no longer that host ("The hosted
|
|
1658
|
+
window", below). Plain
|
|
1648
1659
|
text and not the JSON envelope because the only reader who reaches that door is
|
|
1649
1660
|
a human with a browser. The `--ui` flag is REMOVED, not deprecated: there is no
|
|
1650
1661
|
command line that puts the decision back. The bundle still ships inside the
|
|
@@ -1709,6 +1720,66 @@ move a branch under a worktree somebody is sitting in.
|
|
|
1709
1720
|
|
|
1710
1721
|
A repo joined to nothing behaves exactly as it did.
|
|
1711
1722
|
|
|
1723
|
+
### The hosted window: the host may hold a MIRROR of the declared forge (decided 2026-08-30)
|
|
1724
|
+
|
|
1725
|
+
The first amendment narrowed "no server reads a repo" to "a host that sits in a
|
|
1726
|
+
repo may read it". This one narrows the other clause — *the server deliberately
|
|
1727
|
+
has no clone* — and, as before, the change is recorded here rather than quietly
|
|
1728
|
+
made:
|
|
1729
|
+
|
|
1730
|
+
> **A board host MAY hold a bare, READ-ONLY mirror of the board's DECLARED
|
|
1731
|
+
> forge — and only then does it answer `/git` and `/ui/`.** A board with no
|
|
1732
|
+
> declared forge is exactly what it was: 404 on `/git`, 410 on `/ui/`, each
|
|
1733
|
+
> refusal naming `taskops board forge <owner>/<repo>` as the door.
|
|
1734
|
+
|
|
1735
|
+
Why this is not the degraded window "API ONLY" withdrew. That window's flaw was
|
|
1736
|
+
never that a server drew a dashboard — it was that the machine drawing it had
|
|
1737
|
+
NO git at all, so every diff fell through the whole cascade to a forge link or
|
|
1738
|
+
a sentence, a blank presented as *the* window. The mirror removes the flaw, not
|
|
1739
|
+
the caution: it is **the same history the forge already holds** — `git clone
|
|
1740
|
+
--mirror` of the repo the owner declared with `board forge`, refreshed by ONE
|
|
1741
|
+
bounded on-demand fetch when a requested ref is missing, then answered stale
|
|
1742
|
+
rather than blocking — so a diff served from it is the diff GitHub would show,
|
|
1743
|
+
read by the same `gitwork/diff.py` doors, behind the same visibility rules as
|
|
1744
|
+
`/rpc`. And the dev-side story does not move an inch: `taskops ui` still serves
|
|
1745
|
+
the window from the checkout it stands in, reading the dev's OWN clone,
|
|
1746
|
+
including refs the forge has never seen. The hosted window is for the reader
|
|
1747
|
+
who has no clone — the case the local window, by construction, cannot serve.
|
|
1748
|
+
|
|
1749
|
+
**Where it lives**: `<root>/<board>/mirror.git`, a bare repo beside the board's
|
|
1750
|
+
`events.jsonl` — later cards follow this wording. It is DERIVED and disposable
|
|
1751
|
+
in exactly the sense `cache.sqlite` is: delete it and it re-clones from the
|
|
1752
|
+
declared forge; nothing in it is truth the forge does not hold. Its ONLY source
|
|
1753
|
+
is that forge. It takes no client pushes — the host still exposes no receive
|
|
1754
|
+
door of any kind — and it is not a second trunk: nothing lands there, nothing
|
|
1755
|
+
is read back into the log from it, and `taskops_merge` still happens in the
|
|
1756
|
+
dev's shared checkout.
|
|
1757
|
+
|
|
1758
|
+
**The credential story** is §19's, unchanged in kind. A public repo needs no
|
|
1759
|
+
credential at all. A private repo is fetched with an **ssh deploy key on the
|
|
1760
|
+
host's filesystem** — read-only, minted and installed by the OWNER, revocable
|
|
1761
|
+
on the forge like any deploy key — which is the ONE new credential this chapter
|
|
1762
|
+
adds, and it is the owner's business exactly as `board forge` itself is. What
|
|
1763
|
+
remains banned is what §11 and §19 already ban: a stored GitHub token (the
|
|
1764
|
+
mirror's remote is an address, never a token), a write credential in any form,
|
|
1765
|
+
GitHub as a second identity system, and any dev credential travelling to the
|
|
1766
|
+
host — nobody's `gh auth token` is involved, because the owner already declared
|
|
1767
|
+
the repo and the mirror pulls from the outside like any anonymous (or
|
|
1768
|
+
deploy-keyed) clone.
|
|
1769
|
+
|
|
1770
|
+
**Still banned, spelled out**: client pushes to the host and git replication
|
|
1771
|
+
between clones (the mirror replicates FROM the forge, one direction, and no
|
|
1772
|
+
clone ever pushes to it); a write credential on the host; a second trunk; a
|
|
1773
|
+
`--ui` flag or any per-request sniffing (whether the mirror exists is decided
|
|
1774
|
+
at mount time, the same `Mounts.repo` switch, now pointable at `mirror.git`);
|
|
1775
|
+
and anonymous writes — a public board's hosted window is a READ, and it leaves
|
|
1776
|
+
no `presence` row.
|
|
1777
|
+
|
|
1778
|
+
So `static.py::NO_UI` and `gitdoor.py::NO_REPO` change meaning, not words of
|
|
1779
|
+
intent: each stops saying "this host never has a repo" and starts saying "this
|
|
1780
|
+
BOARD declared no forge, so this host holds no mirror to read from" — a state
|
|
1781
|
+
the owner ends with one `taskops board forge`. The refusal still names the door.
|
|
1782
|
+
|
|
1712
1783
|
### A concept named by two cards is a seam — the fan-out rule, third notch
|
|
1713
1784
|
|
|
1714
1785
|
`gitwork/remote.py` was born TWICE in one wave: two workers, each over the
|
|
@@ -1755,6 +1826,98 @@ starts from the server's real shape, and `null` (fewer than two ready cards,
|
|
|
1755
1826
|
nothing to decide) is deliberately a different state from absent (a board older
|
|
1756
1827
|
than the wave).
|
|
1757
1828
|
|
|
1829
|
+
### A bare `Bearer` is NO credential, not an unknown one (2026-08-31)
|
|
1830
|
+
|
|
1831
|
+
The hosted window shipped on 2026-08-30 and was unusable the next morning for
|
|
1832
|
+
the exact reader it was built for: a PUBLIC board, opened from its own hosted
|
|
1833
|
+
page, answered *"unknown credential — run: taskops join"*. Nothing about
|
|
1834
|
+
visibility was wrong. The page simply had no token, so the browser sent
|
|
1835
|
+
`Authorization: Bearer ` with an empty value; `auth.token_in`'s `removeprefix`
|
|
1836
|
+
did not match the header without its space-and-value, and it returned the
|
|
1837
|
+
literal string `Bearer` as if that were somebody's token. `creds.check` then did
|
|
1838
|
+
its job perfectly on a token nobody ever minted.
|
|
1839
|
+
|
|
1840
|
+
So the fault was never in the visibility rule and never in the client. It was in
|
|
1841
|
+
the one place that decides what a header MEANS:
|
|
1842
|
+
|
|
1843
|
+
> **An `Authorization` header that carries no value is the ABSENCE of a
|
|
1844
|
+
> credential, identical to sending no header at all. A header that carries a
|
|
1845
|
+
> value is a claim, and a claim that does not check out is refused with the
|
|
1846
|
+
> sentence it has always used.**
|
|
1847
|
+
|
|
1848
|
+
Both halves are load-bearing, and the second is why this is not the tempting
|
|
1849
|
+
one-line generalisation. "Treat anything unparseable as anonymous" would make a
|
|
1850
|
+
typo'd token read a private board as far as its visibility allows and, worse,
|
|
1851
|
+
would turn an expired credential into a silent downgrade instead of the loud
|
|
1852
|
+
`unknown credential` it has always been. Empty is not wrong; wrong is wrong —
|
|
1853
|
+
and the two refusals stay different sentences: a caller who presented NOTHING to
|
|
1854
|
+
a private board is told how to join, a caller who presented something unknown is
|
|
1855
|
+
told it is unknown.
|
|
1856
|
+
|
|
1857
|
+
It lives in ONE extractor (`http/auth.py::token_in`) because every door — `rpc`,
|
|
1858
|
+
`feed`, `git`, the static page — asks that function and nothing else. A second
|
|
1859
|
+
door with its own opinion about a header is how the fleet ends up with two
|
|
1860
|
+
credential rules, which is the failure mode §19 exists to prevent. `taskops
|
|
1861
|
+
ui`'s window never saw this: it mints a token before it opens a tab, so the only
|
|
1862
|
+
client that sends an empty bearer is the hosted page, and the hosted page is
|
|
1863
|
+
younger than the extractor.
|
|
1864
|
+
|
|
1865
|
+
The lesson generalises past auth: **anonymous worked only because no client
|
|
1866
|
+
existed**. Every test that pinned "no header reads a public board" sent no
|
|
1867
|
+
header, because the only readers were `curl` and the MCP client. A browser
|
|
1868
|
+
cannot express "no header" through a fetch wrapper that always sets one — so a
|
|
1869
|
+
contract that is only ever exercised by hand-written clients is not pinned,
|
|
1870
|
+
it is merely unvisited. `tests/test_topology.py` now sends the bare header
|
|
1871
|
+
alongside the absent one, on a public board AND a private one, which is the pair
|
|
1872
|
+
that says which of the two rules above is being tested.
|
|
1873
|
+
|
|
1874
|
+
### The board's own address IS the page (2026-08-31)
|
|
1875
|
+
|
|
1876
|
+
The same report carried a second complaint, and it is a design one: the page was
|
|
1877
|
+
at `/<board>/ui/`. A board host serves exactly one thing to a human, and it had
|
|
1878
|
+
been given a sub-path — so the URL a person pastes says `ui` twice over (once in
|
|
1879
|
+
the product's name for the thing, once in the path) and the shortest address on
|
|
1880
|
+
the host, `/<board>/`, answered 404. Whatever a host serves to people belongs at
|
|
1881
|
+
its own address; a prefix is for the doors people never type.
|
|
1882
|
+
|
|
1883
|
+
> **A board's page is served at `/<board>/` (and `/<board>`, bare). The MACHINE
|
|
1884
|
+
> doors move under `/<board>/api/…` — `rpc`, `git/…`, `feed`, `invite/redeem`.**
|
|
1885
|
+
|
|
1886
|
+
Two facts make that router unambiguous rather than a guess per request. The
|
|
1887
|
+
prefix is stripped ONCE, in `http/routes.py::split`, shared by GET and POST, so
|
|
1888
|
+
`/<board>/api/rpc` IS `/<board>/rpc` — same handler, same credential, same
|
|
1889
|
+
words, and no door can drift from the other spelling. And the page's own
|
|
1890
|
+
relative links (`./app.js`, `./style.css` off `index.html`) now arrive as board
|
|
1891
|
+
tails, which the router separates from doors by a CLOSED SET: `static.py::asset`
|
|
1892
|
+
asks the packaged bundle what files it actually ships, flat, with a suffix in
|
|
1893
|
+
`TYPES`. None is named `rpc`, `git`, `feed`, `ui` or `api`, so an asset can
|
|
1894
|
+
never shadow a door and a door never shadows an asset — and a bundle that grows
|
|
1895
|
+
a font ships without a router edit. The host's own one-segment doors (`/login`,
|
|
1896
|
+
`/rpc`, `/healthz`) are untouched, because the strip lives in the TAIL and never
|
|
1897
|
+
sees them; a board literally named `api` still answers at `/api/…`.
|
|
1898
|
+
|
|
1899
|
+
**The 0.5.0 spellings are kept, not deprecated** — `/<board>/ui/`,
|
|
1900
|
+
`/<board>/rpc`, `/<board>/git/…`, `/<board>/feed` all answer, unprefixed and
|
|
1901
|
+
without a redirect. Not out of politeness: links to them were pasted the day
|
|
1902
|
+
they existed, `.mcp.json` files and agents are configured against them, four
|
|
1903
|
+
legacy-bearer production boards speak them, and `taskops ui`'s upstream forward
|
|
1904
|
+
builds them. A redirect would have been the tidier story and would have broken
|
|
1905
|
+
every POST that follows one badly. So the rule this chapter adds about old
|
|
1906
|
+
addresses is the one it also obeys: **an address that ever worked keeps
|
|
1907
|
+
answering, and the new one is where the page POINTS people.** `ui/src/client.ts`
|
|
1908
|
+
derives its base from the page's own location (`baseOf`) precisely so the page
|
|
1909
|
+
never hardcodes which of the two it was loaded from.
|
|
1910
|
+
|
|
1911
|
+
**Both faults are pinned live, not only in the suite.** `smoke.sh` at the repo
|
|
1912
|
+
root is the re-runnable list — every assertion in it is one of these two reports
|
|
1913
|
+
or something 0.5.0 already promised, read-only, and it exits on the first
|
|
1914
|
+
failure:
|
|
1915
|
+
|
|
1916
|
+
```sh
|
|
1917
|
+
sh smoke.sh # taskops.bernardocastro.dev, taskops-v2
|
|
1918
|
+
sh smoke.sh <host> <board> <private-board>
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1758
1921
|
---
|
|
1759
1922
|
|
|
1760
1923
|
## 17. Deployed — taskops.bernardocastro.dev is v2 (2026-08-08)
|
|
@@ -1803,7 +1966,9 @@ request (`http/mounts.py::stores`), so a just-restarted process says `1`, not
|
|
|
1803
1966
|
|
|
1804
1967
|
**That md5-of-the-served-bundle check is history, not the procedure any more.**
|
|
1805
1968
|
Since §16's "API ONLY" amendment the host serves no bundle: `/<board>/ui/`
|
|
1806
|
-
answers `410` and one sentence
|
|
1969
|
+
answers `410` and one sentence (until the hosted-window chapter deploys a board
|
|
1970
|
+
whose forge is declared — §16, "The hosted window"), so there is nothing at the
|
|
1971
|
+
domain to md5. The
|
|
1807
1972
|
wheel is still checked for its markers *before* it ships — it is the same wheel
|
|
1808
1973
|
teammates install to get `taskops ui` — and the domain is verified by DATA,
|
|
1809
1974
|
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.1 — the hosted window, actually usable
|
|
7
|
+
|
|
8
|
+
0.5.0 shipped a window nobody could use. Both faults were reported the next
|
|
9
|
+
morning against the live host, and both were contract-level: the URL people
|
|
10
|
+
paste, and the credential rule every door shares.
|
|
11
|
+
|
|
12
|
+
- **A public board was unreadable from its own hosted page.** The client sends
|
|
13
|
+
`Authorization: "Bearer " + token()`; with no token that header arrives as
|
|
14
|
+
`Bearer`, and `auth.token_in`'s `removeprefix("Bearer ")` did not match — so
|
|
15
|
+
the literal string `"Bearer"` came back AS the token and the reader was told
|
|
16
|
+
"unknown credential — run: taskops join". Anonymous reads worked only when NO
|
|
17
|
+
Authorization header was sent at all, which no browser client does: the
|
|
18
|
+
anonymous path was only ever green because nothing but curl had walked it.
|
|
19
|
+
Scheme and value are now parsed as separate fields, the scheme
|
|
20
|
+
case-insensitive per RFC 7235, and a bearer scheme with no value is the
|
|
21
|
+
ABSENCE of a credential. Deliberately narrow: a header carrying any actual
|
|
22
|
+
value — wrong, expired, or under a scheme this server never spoke — is a
|
|
23
|
+
credential that WAS presented and still refuses exactly as before.
|
|
24
|
+
"Unparseable" is not "anonymous".
|
|
25
|
+
- **The board's own address IS the page.** `https://<host>/<board>/` (and
|
|
26
|
+
`/<board>`) serves the dashboard, with `app.js` and `style.css` beside it; the
|
|
27
|
+
machine doors also answer under `/<board>/api/{rpc,git,feed}`, stripped once
|
|
28
|
+
in `http/routes.py::split` so no door can drift from another. A sub-path was
|
|
29
|
+
wrong for the primary thing a board host serves.
|
|
30
|
+
- **Nothing that worked in 0.5.0 stopped working.** `/<board>/ui/`,
|
|
31
|
+
`/<board>/rpc`, `/<board>/git/…` and `/<board>/feed` all still answer — agents,
|
|
32
|
+
the MCP client, `taskops ui`'s forward and the legacy production boards speak
|
|
33
|
+
them, and a link somebody already pasted is a contract, not a legacy.
|
|
34
|
+
- **`smoke.sh`** — the live, read-only smoke for a board host: the page at the
|
|
35
|
+
board root, a bare `Bearer` reading a public board, a private board still
|
|
36
|
+
refusing, a card branch's diff rendered from the forge mirror. Every line is
|
|
37
|
+
there because it failed once in production, and each says which report it
|
|
38
|
+
came from.
|
|
39
|
+
|
|
40
|
+
## 0.5.0 — the hosted window
|
|
41
|
+
|
|
42
|
+
- **A board can be READ without a clone.** Until now the dashboard was only ever
|
|
43
|
+
served by `taskops ui`, a binary standing in a checkout: a host running
|
|
44
|
+
`taskops serve` answered `/git` with a 404 and `/ui/` with a 410, because a
|
|
45
|
+
dashboard draws DIFFS and the host deliberately had no repository to read them
|
|
46
|
+
from. Serving a page that could not draw a single patch was a degraded window
|
|
47
|
+
presented as *the* window, so it was withdrawn. That reasoning had one
|
|
48
|
+
unexamined premise — that the host can never have git — and this release
|
|
49
|
+
removes it, argued in ARCHITECTURE §16 ("The hosted window") before a line of
|
|
50
|
+
code moved.
|
|
51
|
+
- **The host may hold a bare, READ-ONLY mirror of the board's DECLARED forge**,
|
|
52
|
+
at `<root>/<board>/mirror.git`, and only then does it answer `/git` and
|
|
53
|
+
`/ui/`. The mirror is derived and disposable in exactly the sense
|
|
54
|
+
`cache.sqlite` is: delete it and it re-clones; nothing in it is truth the
|
|
55
|
+
forge does not already hold. Its ONLY source is the repo the owner declared
|
|
56
|
+
with `taskops board forge` — it takes no client pushes, it is not a second
|
|
57
|
+
trunk, and nothing is ever read back into the log from it.
|
|
58
|
+
- **A board that declared no forge is exactly what it was**: 404 on `/git`,
|
|
59
|
+
410 on `/ui/`, and each refusal now names the door — `taskops board forge
|
|
60
|
+
<owner>/<repo>` — instead of stranding the reader in a sentence about a host
|
|
61
|
+
they cannot change.
|
|
62
|
+
- **No new credential type.** A public repo is mirrored anonymously; a private
|
|
63
|
+
one is fetched with an ssh deploy key on the host's filesystem, installed by
|
|
64
|
+
the OWNER, revocable on the forge — the one new credential this chapter adds,
|
|
65
|
+
and it is the owner's business exactly as `board forge` itself is. No stored
|
|
66
|
+
GitHub token, no dev credential travelling, no second identity system.
|
|
67
|
+
- **The page opens on the FACT, never on a clone.** `/ui/` is granted by the
|
|
68
|
+
declared forge alone: the bundle's bytes come from the installed package, so
|
|
69
|
+
putting a network clone in front of `index.html` would take a static page down
|
|
70
|
+
whenever the forge is unreachable. The mirror resolves lazily the moment a
|
|
71
|
+
diff is actually read — which is where a failure belongs, in the diff pane, in
|
|
72
|
+
the door's own words.
|
|
73
|
+
- **A missing ref buys exactly ONE bounded fetch**, then answers stale rather
|
|
74
|
+
than blocking a page on a forge that is down — and only on a mirror. A
|
|
75
|
+
window's own clone still fetches nothing on the reader's behalf: a background
|
|
76
|
+
fetch inside a read-only door would move a branch under a worktree somebody is
|
|
77
|
+
sitting in.
|
|
78
|
+
- **Reads stay reads.** `/ui/` sits behind the credential `/rpc` asks for —
|
|
79
|
+
a `public` board serves the page to an anonymous reader, a private one refuses
|
|
80
|
+
with the same join sentence — and an anonymous page load writes nothing at
|
|
81
|
+
all, not even the invisible write: no presence row.
|
|
82
|
+
|
|
6
83
|
## 0.4.2 — the filter that filtered nothing, and the window that stopped signing in
|
|
7
84
|
|
|
8
85
|
- **"All chapters" is an ARGUMENT now, and it really shows every chapter.** The
|
|
@@ -228,6 +228,17 @@ what it exists to report, not a fault.
|
|
|
228
228
|
- One card rebuilds the bundle at the end of a wave. N cards rebuilding it is
|
|
229
229
|
N-1 conflicts by construction.
|
|
230
230
|
|
|
231
|
+
**The hosted page is at the board's OWN address** — `https://<host>/<board>/`
|
|
232
|
+
(and `/<board>`), its assets beside it, the machine doors under
|
|
233
|
+
`/<board>/api/{rpc,git,feed}`. 0.5.0's spellings (`/<board>/ui/`,
|
|
234
|
+
`/<board>/rpc`, `/<board>/git/…`, `/<board>/feed`) still answer and are a
|
|
235
|
+
contract, not a legacy — ARCHITECTURE §16, "The board's own address IS the
|
|
236
|
+
page". The page derives its base from its own location
|
|
237
|
+
(`ui/src/client.ts::baseOf`) and hardcodes neither. `sh smoke.sh` re-derives all
|
|
238
|
+
of it against the live host, plus the credential rule a browser exposed:
|
|
239
|
+
**an `Authorization` header with no value is the ABSENCE of a credential, a
|
|
240
|
+
header with a wrong value still refuses loudly** (`http/auth.py::token_in`).
|
|
241
|
+
|
|
231
242
|
## Never re-introduce
|
|
232
243
|
|
|
233
244
|
Each has its line in ARCHITECTURE.md §11 saying what it cost and where it is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskops-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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 — the page 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,31 @@ 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>`.
|
|
391
|
+
|
|
392
|
+
**The hosted page is at the board's OWN address.** `https://<host>/<board>/`
|
|
393
|
+
IS the dashboard — not a sub-path under it — and the machine doors sit under a
|
|
394
|
+
prefix that can never collide with a page asset:
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
https://<host>/<board>/ the page (also /<board>, no slash)
|
|
398
|
+
https://<host>/<board>/app.js its assets style.css, index.html
|
|
399
|
+
https://<host>/<board>/api/rpc the verbs also /<board>/rpc
|
|
400
|
+
https://<host>/<board>/api/git/… diffs from the mirror also /<board>/git/…
|
|
401
|
+
https://<host>/<board>/api/feed the live feed also /<board>/feed
|
|
402
|
+
https://<host>/healthz the host itself
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The right-hand spellings are 0.5.0's and they keep answering, unprefixed and
|
|
406
|
+
un-redirected — including `/<board>/ui/`, which was the page's address for one
|
|
407
|
+
day. Links were pasted, agents and the MCP client are configured against them,
|
|
408
|
+
and `taskops ui`'s upstream forward speaks them, so they are a contract now
|
|
409
|
+
rather than a legacy. Re-derivable at any time with `sh smoke.sh <host>
|
|
410
|
+
<board>`.
|
|
389
411
|
|
|
390
412
|
**Reading one, from the other side.** Everything above is the author's half; a
|
|
391
413
|
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 — the page 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,31 @@ 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>`.
|
|
379
|
+
|
|
380
|
+
**The hosted page is at the board's OWN address.** `https://<host>/<board>/`
|
|
381
|
+
IS the dashboard — not a sub-path under it — and the machine doors sit under a
|
|
382
|
+
prefix that can never collide with a page asset:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
https://<host>/<board>/ the page (also /<board>, no slash)
|
|
386
|
+
https://<host>/<board>/app.js its assets style.css, index.html
|
|
387
|
+
https://<host>/<board>/api/rpc the verbs also /<board>/rpc
|
|
388
|
+
https://<host>/<board>/api/git/… diffs from the mirror also /<board>/git/…
|
|
389
|
+
https://<host>/<board>/api/feed the live feed also /<board>/feed
|
|
390
|
+
https://<host>/healthz the host itself
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The right-hand spellings are 0.5.0's and they keep answering, unprefixed and
|
|
394
|
+
un-redirected — including `/<board>/ui/`, which was the page's address for one
|
|
395
|
+
day. Links were pasted, agents and the MCP client are configured against them,
|
|
396
|
+
and `taskops ui`'s upstream forward speaks them, so they are a contract now
|
|
397
|
+
rather than a legacy. Re-derivable at any time with `sh smoke.sh <host>
|
|
398
|
+
<board>`.
|
|
377
399
|
|
|
378
400
|
**Reading one, from the other side.** Everything above is the author's half; a
|
|
379
401
|
reader needs no ceremony at all. Three doors onto the same bytes:
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The live smoke for a taskops board HOST. Read-only, no credential, no state:
|
|
3
|
+
# every request below is a GET or a `board` read, so it is safe to run against
|
|
4
|
+
# production at any time, including from a laptop that never joined the board.
|
|
5
|
+
#
|
|
6
|
+
# sh smoke.sh # the author's host, defaults below
|
|
7
|
+
# sh smoke.sh <host> <public-board> <private-board> <card-branch>
|
|
8
|
+
#
|
|
9
|
+
# Every assertion is here because it FAILED once, in production, against a real
|
|
10
|
+
# reader — the comment above each one says which report it comes from. Nothing
|
|
11
|
+
# is asserted because it seemed prudent: a smoke list that grows by taste stops
|
|
12
|
+
# being read, and a green line that never could have gone red is a lie about
|
|
13
|
+
# coverage. It exits non-zero on the FIRST failure, so the first red line is the
|
|
14
|
+
# whole diagnosis.
|
|
15
|
+
#
|
|
16
|
+
# `pass` prints what was actually observed, never just OK — a smoke you cannot
|
|
17
|
+
# read the evidence out of is a smoke you end up re-running by hand anyway.
|
|
18
|
+
|
|
19
|
+
set -eu
|
|
20
|
+
|
|
21
|
+
HOST=${1:-taskops.bernardocastro.dev}
|
|
22
|
+
BOARD=${2:-taskops-v2}
|
|
23
|
+
PRIVATE=${3:-axion}
|
|
24
|
+
BRANCH=${4:-tk-32d2ba}
|
|
25
|
+
|
|
26
|
+
BASE="https://$HOST"
|
|
27
|
+
C="curl -sS -m 15"
|
|
28
|
+
|
|
29
|
+
fail() { printf '\nFAIL %s\n %s\n' "$1" "$2" >&2; exit 1; }
|
|
30
|
+
pass() { printf 'ok %s\n %s\n' "$1" "$2"; }
|
|
31
|
+
head_ln() { printf '\n-- %s\n' "$1"; }
|
|
32
|
+
|
|
33
|
+
# ── (e) the host itself ───────────────────────────────────────────────────────
|
|
34
|
+
# First, because every failure below is ambiguous until this one is green: a
|
|
35
|
+
# 502 from the proxy and a broken board answer the same shape of nothing.
|
|
36
|
+
head_ln "healthz — the host is up"
|
|
37
|
+
body=$($C "$BASE/healthz") || fail "GET /healthz" "no answer from $BASE"
|
|
38
|
+
case $body in
|
|
39
|
+
*'"ok": true'*|*'"ok":true'*) pass "GET /healthz" "$body" ;;
|
|
40
|
+
*) fail "GET /healthz" "$body" ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
# ── (a) the board's own address IS the page ───────────────────────────────────
|
|
44
|
+
# Reported 2026-08-31: the page was at /<board>/ui/ and /<board>/ was a 404 —
|
|
45
|
+
# the shortest address on the host, the one a person types, answered nothing.
|
|
46
|
+
# Asserted on the BARE path too: /<board> without the trailing slash is what
|
|
47
|
+
# gets pasted into Slack, and a router that only handles one of the two looks
|
|
48
|
+
# fine in a browser (it follows the redirect) and breaks every copied link.
|
|
49
|
+
head_ln "the page at the board's own address"
|
|
50
|
+
for path in "/$BOARD/" "/$BOARD"; do
|
|
51
|
+
page=$($C "$BASE$path") || fail "GET $path" "no answer"
|
|
52
|
+
case $page in
|
|
53
|
+
*'app.js'*) pass "GET $path" "html referencing app.js ($(printf %s "$page" | wc -c | tr -d ' ') bytes)" ;;
|
|
54
|
+
*) fail "GET $path" "not the page: $(printf %s "$page" | head -c 200)" ;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
# And its assets resolve from that address: the page's own links are relative
|
|
58
|
+
# (./app.js off index.html), so they arrive as board TAILS and the router has to
|
|
59
|
+
# tell them from the machine doors. If this 404s, the page loads unstyled and
|
|
60
|
+
# blank with nothing in the server log to say why.
|
|
61
|
+
for asset in app.js style.css; do
|
|
62
|
+
code=$($C -o /dev/null -w '%{http_code} %{content_type}' "$BASE/$BOARD/$asset") \
|
|
63
|
+
|| fail "GET /$BOARD/$asset" "no answer"
|
|
64
|
+
case $code in
|
|
65
|
+
200*) pass "GET /$BOARD/$asset" "$code" ;;
|
|
66
|
+
*) fail "GET /$BOARD/$asset" "$code" ;;
|
|
67
|
+
esac
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
# ── (b) a bare `Bearer` reads a PUBLIC board ──────────────────────────────────
|
|
71
|
+
# THE fault of this chapter, shipped in 0.5.0 and reported 2026-08-31: the page
|
|
72
|
+
# sends `Authorization: "Bearer " + token()`, so with no token the header
|
|
73
|
+
# arrives as `Bearer`, and the old extractor returned the literal string
|
|
74
|
+
# "Bearer" as somebody's token — "unknown credential — run: taskops join" to a
|
|
75
|
+
# reader who presented nothing. Anonymous reads worked only with NO header at
|
|
76
|
+
# all, which no browser client can express. Both shapes are asserted: the
|
|
77
|
+
# no-header stranger (what the suite always tested) and the bare bearer (what
|
|
78
|
+
# every browser actually sends).
|
|
79
|
+
head_ln "a bare Bearer is no credential — public board reads"
|
|
80
|
+
rpc() { # rpc <path> [<authorization value>]
|
|
81
|
+
if [ $# -gt 1 ]; then
|
|
82
|
+
$C -o /dev/null -w '%{http_code}' -X POST "$BASE$1" \
|
|
83
|
+
-H 'Content-Type: application/json' -H "Authorization: $2" \
|
|
84
|
+
-d '{"verb": "board", "args": {}}'
|
|
85
|
+
else
|
|
86
|
+
$C -o /dev/null -w '%{http_code}' -X POST "$BASE$1" \
|
|
87
|
+
-H 'Content-Type: application/json' -d '{"verb": "board", "args": {}}'
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
code=$(rpc "/$BOARD/api/rpc" "Bearer") || fail "POST /$BOARD/api/rpc (bare Bearer)" "no answer"
|
|
91
|
+
[ "$code" = 200 ] || fail "POST /$BOARD/api/rpc (bare Bearer)" "HTTP $code — the 0.5.0 fault is back"
|
|
92
|
+
pass "POST /$BOARD/api/rpc Authorization: Bearer" "HTTP 200"
|
|
93
|
+
code=$(rpc "/$BOARD/api/rpc") || fail "POST /$BOARD/api/rpc (no header)" "no answer"
|
|
94
|
+
[ "$code" = 200 ] || fail "POST /$BOARD/api/rpc (no header)" "HTTP $code"
|
|
95
|
+
pass "POST /$BOARD/api/rpc no Authorization" "HTTP 200"
|
|
96
|
+
# The other half of that fix, and the reason it is not a one-line
|
|
97
|
+
# generalisation: a credential that WAS presented and does not check out still
|
|
98
|
+
# refuses loudly. "Unparseable is anonymous" would turn an expired token into a
|
|
99
|
+
# silent read-only downgrade instead of a sentence naming the way back in.
|
|
100
|
+
body=$($C -X POST "$BASE/$BOARD/api/rpc" -H 'Content-Type: application/json' \
|
|
101
|
+
-H 'Authorization: Bearer not-a-token' -d '{"verb": "board", "args": {}}') \
|
|
102
|
+
|| fail "POST with a wrong token" "no answer"
|
|
103
|
+
case $body in
|
|
104
|
+
*'unknown credential'*) pass "POST /$BOARD/api/rpc Bearer not-a-token" "refused: unknown credential" ;;
|
|
105
|
+
*) fail "POST with a wrong token" "$body" ;;
|
|
106
|
+
esac
|
|
107
|
+
|
|
108
|
+
# ── (c) a PRIVATE board still refuses ─────────────────────────────────────────
|
|
109
|
+
# The wall moved for public boards; it must not have moved here. And the
|
|
110
|
+
# SENTENCE matters as much as the status: somebody who presented nothing is told
|
|
111
|
+
# how to join, which is a different answer from "unknown credential".
|
|
112
|
+
head_ln "a private board still refuses the same stranger"
|
|
113
|
+
body=$($C -X POST "$BASE/$PRIVATE/api/rpc" -H 'Content-Type: application/json' \
|
|
114
|
+
-H 'Authorization: Bearer' -d '{"verb": "board", "args": {}}') \
|
|
115
|
+
|| fail "POST /$PRIVATE/api/rpc" "no answer"
|
|
116
|
+
# Grepped for `no credential` and NOT for `taskops join`: both refusals name
|
|
117
|
+
# that command, so the looser grep passes on the 0.5.0 fault itself — where a
|
|
118
|
+
# private board answered `unknown credential` to a stranger who presented
|
|
119
|
+
# nothing. The whole point is WHICH sentence comes back.
|
|
120
|
+
case $body in
|
|
121
|
+
*'no credential'*) pass "POST /$PRIVATE/api/rpc Authorization: Bearer" "refused: no credential — run: taskops join" ;;
|
|
122
|
+
*) fail "POST /$PRIVATE/api/rpc" "expected the no-credential refusal, got: $body" ;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
# ── (d) a card branch renders a diff from the forge mirror ────────────────────
|
|
126
|
+
# The hosted window's whole point: a reader with NO clone. Before the mirror,
|
|
127
|
+
# every diff fell through the UI's cascade to a forge link — a blank presented
|
|
128
|
+
# as the window. A tk-* branch on purpose: `master` is fetched by any clone, and
|
|
129
|
+
# the failure this catches is a mirror whose on-demand fetch never ran, which
|
|
130
|
+
# only shows on a ref the mirror did not have at clone time.
|
|
131
|
+
head_ln "a card branch diffs from the mirror"
|
|
132
|
+
body=$($C "$BASE/$BOARD/api/git/commit/$BRANCH") || fail "GET git/commit/$BRANCH" "no answer"
|
|
133
|
+
case $body in
|
|
134
|
+
*'"patch"'*) pass "GET /$BOARD/api/git/commit/$BRANCH" "a diff ($(printf %s "$body" | wc -c | tr -d ' ') bytes)" ;;
|
|
135
|
+
*) fail "GET git/commit/$BRANCH" "no diff: $(printf %s "$body" | head -c 300)" ;;
|
|
136
|
+
esac
|
|
137
|
+
|
|
138
|
+
# ── (f) every 0.5.0 address still answers ─────────────────────────────────────
|
|
139
|
+
# Links were pasted the day they existed, .mcp.json files are configured against
|
|
140
|
+
# them, four legacy-bearer production boards speak them, and `taskops ui`'s
|
|
141
|
+
# upstream forward builds them. An old address that 404s in silence is this
|
|
142
|
+
# chapter's own rule broken.
|
|
143
|
+
head_ln "the 0.5.0 addresses, unprefixed"
|
|
144
|
+
code=$($C -o /dev/null -w '%{http_code}' "$BASE/$BOARD/ui/") || fail "GET /$BOARD/ui/" "no answer"
|
|
145
|
+
[ "$code" = 200 ] || fail "GET /$BOARD/ui/" "HTTP $code"
|
|
146
|
+
pass "GET /$BOARD/ui/" "HTTP 200"
|
|
147
|
+
code=$(rpc "/$BOARD/rpc" "Bearer") || fail "POST /$BOARD/rpc" "no answer"
|
|
148
|
+
[ "$code" = 200 ] || fail "POST /$BOARD/rpc" "HTTP $code"
|
|
149
|
+
pass "POST /$BOARD/rpc" "HTTP 200"
|
|
150
|
+
code=$($C -o /dev/null -w '%{http_code}' "$BASE/$BOARD/git/commit/$BRANCH") \
|
|
151
|
+
|| fail "GET /$BOARD/git/commit/$BRANCH" "no answer"
|
|
152
|
+
[ "$code" = 200 ] || fail "GET /$BOARD/git/commit/$BRANCH" "HTTP $code"
|
|
153
|
+
pass "GET /$BOARD/git/commit/$BRANCH" "HTTP 200"
|
|
154
|
+
# /feed is a stream that never ends by design, so this reads the HEADERS and
|
|
155
|
+
# hangs up. curl's own timeout is therefore EXPECTED here and not a failure —
|
|
156
|
+
# what is asserted is the content type, which is what tells an SSE client it may
|
|
157
|
+
# start reading.
|
|
158
|
+
headers=$(curl -sS -m 3 -D - -o /dev/null "$BASE/$BOARD/feed" 2>/dev/null || true)
|
|
159
|
+
case $headers in
|
|
160
|
+
*event-stream*) pass "GET /$BOARD/feed" "text/event-stream" ;;
|
|
161
|
+
*) fail "GET /$BOARD/feed" "no event-stream header: $(printf %s "$headers" | tr '\r\n' ' ')" ;;
|
|
162
|
+
esac
|
|
163
|
+
|
|
164
|
+
printf '\nall green — %s, board %s\n' "$HOST" "$BOARD"
|