taskops-cli 0.3.1__tar.gz → 0.3.2__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.3.1 → taskops_cli-0.3.2}/.taskops/board/cache.sqlite-wal +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/events.jsonl +8 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/live.sqlite-wal +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ARCHITECTURE.md +1 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/CHANGELOG.md +25 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/PKG-INFO +1 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_version.py +1 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/serving.py +1 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/window.py +1 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/bind.py +11 -2
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/remote.py +14 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/gitdoor.py +14 -4
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/handler.py +2 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/static.py +30 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/upstream.py +1 -1
- taskops_cli-0.3.2/src/taskops/ui/app.js +60 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_git.py +32 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_topology.py +28 -2
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_ui.py +14 -2
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_window.py +58 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/fixture.json +109 -109
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/forge-links.tsx +18 -5
- taskops_cli-0.3.2/ui/smoke/sections/report-markdown.tsx +86 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/report-sandbox.tsx +28 -7
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/App.tsx +8 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/client.ts +5 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Drawer.tsx +9 -2
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Sections.tsx +32 -21
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/tokens.ts +10 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Chapter.tsx +1 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/reports/ReportFrame.tsx +36 -1
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/main.tsx +13 -3
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/types.ts +5 -2
- taskops_cli-0.3.1/src/taskops/ui/app.js +0 -60
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.claude/settings.json +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.gitattributes +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.github/workflows/ci.yml +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.github/workflows/workflow.yml +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.gitignore +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.mcp.json +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/cache.sqlite +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/cache.sqlite-shm +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/live.sqlite +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board/live.sqlite-shm +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/board.json +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/reports/lifecycle-chapter.html +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/reports/reports-chapter.html +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/.taskops/ui.lock +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/CLAUDE.md +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/README.md +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/pyproject.toml +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_clock.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_errors.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_ids.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_json.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_locate.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/_wire.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/board.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/__main__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/admin.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/claude.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/commands.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/enrol.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/grants.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/hooks.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/main.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/operate.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/paging.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/parser.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/pull.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/push.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/remote.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/rm.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/watch.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/cli/wording.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/actors.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/challenge.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/chapters.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/event.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/forge.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/graph.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/holding.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/hours.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/machine.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/mentions.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/replay.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/reports.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/review.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/scope.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/seams.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/core/types.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/catchup.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/claudefiles.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/diff.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/install.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/landing.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/patch.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/run.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/sig.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/trailer.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/gitwork/trees.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/admin.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/auth.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/feed.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/github.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/grants.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/ingest.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/login.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/mounts.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/removal.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/rpc.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/scoped.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/server.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/http/watcher.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/identity.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/__main__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/activity.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/before.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/boards.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/boardview.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/brief.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/dossier.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/fields.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/gitmoves.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/hello.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/integrate.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/render.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/schema.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/server.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/thread.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/mcp/tools.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/py.typed +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/session.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/cache.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/creds.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/live.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/log.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/pubkeys.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/reviews.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/server.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/store/stores.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/ui/index.html +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/ui/style.css +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_args.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_cards.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_context.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_facts.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_mentions.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_rows.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_stories.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/_waiting.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/activity.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/assign.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/card.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/events.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/filed.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/plan.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/project.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/pulse.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/record.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/report.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/review.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/take.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/src/taskops/verbs/update.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/__init__.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/conftest.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_architecture.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_claude.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_core.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_mcp.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_report.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_store.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/test_verbs.py +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/tests/ui_harness.js +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/build.mjs +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/index.html +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/package-lock.json +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/package.json +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/main.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/run.mjs +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/actors.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/board-columns.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/chapter-landed.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/chapters-open.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/comment-box.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/diff-page.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/dossier.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/event-stream.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/git-diff.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/monitor-panes.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/overlay-escape.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/reviewing-skew.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/section.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/swarm-topology.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/thread-closing-note.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections/worktrees-index.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/smoke/sections.mjs +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/actors/Daysheet.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/actors/DevPanel.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/board/CardTile.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/board/Column.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/CommentBox.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Patch.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/Thread.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/card/split.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/AvatarStack.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/Header.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/KpiRail.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/chrome/TabNav.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/DependencyChain.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/EditSurface.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/EventStream.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/LiveLeases.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Mentions.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Pane.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Swarm.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/Throughput.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/monitor/panels.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/shared/Markdown.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/shared/Overlay.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/components/shared/overlayStack.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/format.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/links.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/markdown.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Actors.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Board.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Monitor.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Reports.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/WorktreeDiff.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/pages/Worktrees.tsx +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/theme/theme.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/theme/tokens.css +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/useBoard.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/src/useEvents.ts +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/styles/index.css +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/ui/tsconfig.json +0 -0
- {taskops_cli-0.3.1 → taskops_cli-0.3.2}/uv.lock +0 -0
|
Binary file
|
|
@@ -772,3 +772,11 @@
|
|
|
772
772
|
{"actor":"dev:berna","body":{"into":"ms/production-catches-up-the-host-r","sha":"e8e71eb91f89c02286b29b7b6dce424653b9e87b"},"id":"0476b2b081ed474629c63868cc9a274d","kind":"merged","task":"tk-5c64d5","ts":1786392488.108064}
|
|
773
773
|
{"actor":"dev:berna","body":{"branch":"ms/the-lifecycle-runs-backwards-too","files":["ARCHITECTURE.md","CLAUDE.md","README.md","src/taskops/cli/commands.py","src/taskops/cli/main.py","src/taskops/cli/parser.py","src/taskops/core/scope.py","src/taskops/http/admin.py","tests/test_topology.py"],"numstat":{"ARCHITECTURE.md":[168,9],"CLAUDE.md":[17,3],"README.md":[21,3],"src/taskops/_wire.py":[45,0],"src/taskops/cli/commands.py":[42,33],"src/taskops/cli/enrol.py":[82,0],"src/taskops/cli/grants.py":[59,0],"src/taskops/cli/hooks.py":[50,0],"src/taskops/cli/main.py":[13,95],"src/taskops/cli/operate.py":[55,38],"src/taskops/cli/parser.py":[149,0],"src/taskops/core/forge.py":[85,0],"src/taskops/core/scope.py":[9,0],"src/taskops/http/admin.py":[22,0],"src/taskops/http/github.py":[180,0],"src/taskops/http/handler.py":[16,1],"src/taskops/http/ingest.py":[10,5],"src/taskops/http/login.py":[12,2],"src/taskops/http/server.py":[20,0],"src/taskops/verbs/project.py":[53,7],"tests/test_topology.py":[703,1],"tests/test_verbs.py":[82,1]},"sha":"ee54aa8aa0a22e381bc1fd0a127ba0f308c475ca","subject":"merge master — two chapters, one CLI, and the section number they both claimed"},"id":"e34f0f942fed0ce549fdb76b159cb21c","kind":"commit","task":"project","ts":1786392613.0}
|
|
774
774
|
{"actor":"dev:berna","body":{"criteria_met":true,"id":"ms-392423","into":"master","op":"landed","sha":"1dee83b688967650945a93c8305e313d8ac3481f"},"id":"521b59ad4d037fa2dcc66f9e4e119f97","kind":"milestone","task":"project","ts":1786392618.650824}
|
|
775
|
+
{"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board.json",".taskops/board/cache.sqlite",".taskops/board/cache.sqlite-shm",".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite",".taskops/board/live.sqlite-shm",".taskops/board/live.sqlite-wal",".taskops/ui.lock","CHANGELOG.md","src/taskops/_version.py"],"numstat":{".taskops/board.json":[1,0],".taskops/board/cache.sqlite":null,".taskops/board/cache.sqlite-shm":null,".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[774,0],".taskops/board/live.sqlite":null,".taskops/board/live.sqlite-shm":null,".taskops/board/live.sqlite-wal":null,".taskops/ui.lock":[0,0],"CHANGELOG.md":[31,0],"src/taskops/_version.py":[1,1]},"sha":"9b22232ff4c6ac5df0561296365e2f2576e94a0c","subject":"0.3.1 — three chapters, and the flood that made a remote board unusable"},"id":"4b8de6c0d7d4082c5b289221500e9bb2","kind":"commit","task":"project","ts":1786392799.0}
|
|
776
|
+
{"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","src/taskops/gitwork/bind.py","src/taskops/gitwork/remote.py","tests/test_git.py"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[1,0],".taskops/board/live.sqlite-wal":null,"src/taskops/gitwork/bind.py":[11,2],"src/taskops/gitwork/remote.py":[14,1],"tests/test_git.py":[32,0]},"sha":"3ae9dbfd62d036a8bee49a5e33ff045e06151e31","subject":"fix: a push lands on the branch it names, and never on its upstream"},"id":"797d85bd5d8d857922f491f7ff50889a","kind":"commit","task":"project","ts":1786393176.0}
|
|
777
|
+
{"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/ship1"},"id":"117335f0afc0c4057bd978286351cce0","kind":"edited","task":"tk-1dfa16","ts":1786393201.284905}
|
|
778
|
+
{"actor":"agent:berna/ship1","body":{"no_code":true,"reason":"Deployed taskops-cli 0.3.1 (the PyPI artifact, downloaded by the host itself) to taskops.bernardocastro.dev. No repo commit: the deploy touched no code here; the only file written was ~/.claude/reference/infra.md (runbook corrections, outside this repo).\n\nWHERE IT ACTUALLY RUNS — the runbook was wrong and cost ~6 calls to rediscover. 34.71.115.185 (gpu-relay) runs only nginx: proxy_pass http://10.8.0.2:8082 -> LXD device proxy -> container `bernardocastro` -> pm2 `taskops-v2` = ~/taskops-v2-app/.venv/bin/taskops serve --root ~/taskops-v2-server --host 127.0.0.1 --port 2181. Deploy host is `ssh bernardocastro-box`. infra.md now says so.\n\nROLLBACK, recorded BEFORE anything was replaced, and none of the stored wheels qualified: md5 of every taskops/*.py inside each of the 4 wheels in rollback/ vs site-packages gave mismatch=27 / 25 / 2 / 24 — even the newest (…-bffa26) differed in taskops/cli/push.py and taskops/http/ingest.py (the _birth -> _configuration change), i.e. previous deploys saved the wheel they REPLACED, never the one they installed. So the rollback was rebuilt from the live install: verified the dist-info RECORD against disk first (109 files, 0 bad -> the install was coherent, not hand-patched), then zipped site-packages/taskops + taskops-2.0.0a0.dist-info to rollback/20260810-1dfa16-preupgrade/taskops-2.0.0a0-py3-none-any.whl. Proof it IS the pre-upgrade code: re-extracted and compared — 98 .py compared, 0 mismatches, 98 .py in the wheel (no extras either way). Valid wheel filename inside a dated dir, so a rollback is one pip install with no renaming.\n\nTHE SWAP. Shipped to /tmp/tk-1dfa16/dl (never bare /tmp). Host did `pip download --no-deps --only-binary=:all: taskops-cli==0.3.1` itself (md5 592b03d485977ab1305e0f11e8b151de, 368428 bytes); simple index confirmed serving 0.3.1 first. The installed dist was named `taskops` 2.0.0a0 while PyPI ships `taskops-cli` — both own the taskops/ package, so installing over it would have left the old dist-info plus 2.0.0a0-only modules shadowing. `pip uninstall -y taskops` first (site-packages then held only pip), then `pip install --force-reinstall --no-deps`. pip list is now exactly pip + taskops-cli 0.3.1; taskops.__version__ == 0.3.1. pm2 restart taskops-v2; error log empty since.\n\nVERIFIED AGAINST THE HOST (all over https, not the deploy's exit code):\n- /healthz -> {\"ok\": true, \"seq\": 0, \"data\": {\"boards\": 6}}.\n- POST /axion/join/github: before \"nothing at /axion/join/github\" (404); after -> 409 refused \"this board is opened by invite, not by GitHub — its owner opts in by naming the repo whose membership counts: taskops board forge <owner>/<name> --need push\". Milestone criterion 1 met.\n- Second new-version route, /taskops-v2/git/README.md -> \"this host serves boards, not a repository — it was started outside a checkout\"; in 2.0.0a0 both fell through to \"nothing at\". Control: /axion/nope still \"nothing at /axion/nope\", so the 404 path is intact.\n- Six boards read identically to baseline: agenda/axion/notas/probe -> 409 \"no credential — run: taskops join …\" (same as before), taskops-v2 + scratch-tk-bffa26 -> 200 SSE `data: {\"type\":\"hello\"}`.\n- events.jsonl byte-identical across the upgrade, md5 before == after: agenda 35 b84c0a21…, axion 961 b1623f0a…, notas 48 c8bd750d…, probe 17 6c764e52…, taskops-v2 664 a6cee77d…, scratch-tk-bffa26 5 9ff99159…. 6 board dirs, unchanged. Criterion 2 met.\n\nONE SCARE, RESOLVED, NOT ASSUMED: the first /healthz after restart said {\"boards\": 0}. That is not board loss — http/mounts.py::count returns len(self._boards), a LAZY cache filled by stores(), so a just-restarted process legitimately reports 0 and the old process only said 6 because it had been up 29h. Read one feed per board and healthz returned to 6. Read the code before believing the number; documented in infra.md so the next deploy does not roll back over it. No rollback was needed.\n\nASSUMED, NOT VERIFIED: --no-deps is safe because the project declares zero runtime dependencies (pip resolved none anyway). The four 409 boards were verified as reachable reads (refusal is their pre-existing anonymous behaviour and all six Stores opened, per healthz==6), not as authenticated reads — I hold no credential for them. /taskops-v2/ui now answers 410 \"this host serves the board, not the dashboard\"; I had no pre-upgrade baseline for that path, so I cannot say whether it changed, only that it is a deliberate message and not an error.","to":"done"},"id":"ee24a141f16c3140bb79f1923d4df783","kind":"status","task":"tk-1dfa16","ts":1786393748.646178}
|
|
779
|
+
{"actor":"dev:berna","body":{"into":"ms/production-catches-up-the-host-r","sha":"e8e71eb91f89c02286b29b7b6dce424653b9e87b"},"id":"5aa9ee2076f4262b8bc0ac88bb8b9c88","kind":"merged","task":"tk-1dfa16","ts":1786393792.392233}
|
|
780
|
+
{"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/jp1"},"id":"d74b22b355d8797f6fc451e81aa105bc","kind":"edited","task":"tk-d1ff9b","ts":1786393796.008371}
|
|
781
|
+
{"actor":"dev:berna","body":{"text":"STOP — the ground moved under this card. I killed jp1 mid-work on purpose, because it was about to write an onboarding README naming a host the axion board no longer lives on.\n\nThe axion board MOVED, tonight, at Berna's request (the home box was too slow):\n\n from https://taskops.bernardocastro.dev/axion (nginx on the GCP relay → proxy_pass 10.8.0.2 → LXD container on the home box)\n to https://boards.t.bernardocastro.dev/axion (taskops 0.3.1 SERVED BY the GCP relay itself, systemd unit `taskops`, 127.0.0.1:8787 behind nginx)\n\nMeasured, which is why it moved: /healthz is 0.36s on the new host against 0.77s on the old one — the relay→box hop was 178ms each way.\n\nHow it moved, and what survives:\n- `board pull axion` brought 962 events down (\"all 962 event(s) are held here\"), then `board create` + `board push axion` on the new host: \"verified: 962 landed, seq 963\".\n- The OLD host still holds its copy — a pull destroys nothing. Nobody has removed it, and that is Berna's call, not this card's.\n- jp1's work was NOT wasted: it declared the forge before it was stopped (event 925540b6…, ts 1786393854, `{op: forge, repo: cloudacio/Axion, need: push}`), and that event travelled with the 962. The GitHub door is live on the new host — `POST /axion/join/github` answers 400 with the body shape it wants, no longer 409 \"opened by invite\".\n\nSo for whoever picks this up: every URL in the README is `boards.t.bernardocastro.dev`, the forge step is already done and must not be re-declared, and the milestone's rule still holds — every command in the README gets run at least once, and you say plainly which ones you could not run because you are the owner and not JP.\n\nOne papercut found while doing this, worth its own card and NOT this one: bare `taskops board pull` in a joined checkout defaults the board name to the DIRECTORY (`axion-v3`) instead of the board `board.json` names (`axion`), so it refused with \"no board named 'axion-v3'\". `board pull axion` works."},"id":"41b9109c6dd2c3feba08a6c0dd04eeeb","kind":"comment","task":"tk-d1ff9b","ts":1786394715.91416}
|
|
782
|
+
{"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","ARCHITECTURE.md","src/taskops/cli/serving.py","src/taskops/cli/window.py","src/taskops/http/handler.py","src/taskops/http/static.py","src/taskops/http/upstream.py","src/taskops/ui/app.js","tests/test_window.py","ui/src/client.ts","ui/src/main.tsx"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[6,0],".taskops/board/live.sqlite-wal":null,"ARCHITECTURE.md":[1,1],"src/taskops/cli/serving.py":[1,1],"src/taskops/cli/window.py":[1,1],"src/taskops/http/handler.py":[2,0],"src/taskops/http/static.py":[30,0],"src/taskops/http/upstream.py":[1,1],"src/taskops/ui/app.js":null,"tests/test_window.py":[58,1],"ui/src/client.ts":[5,1],"ui/src/main.tsx":[13,3]},"sha":"ab2da80683a6fb3f4358f01364f5f9b2c2aa0fe0","subject":"the window's address is its port — `taskops ui` opens `/`, not `/board/ui/`"},"id":"bdd27bc33f6137028dfab5546ebfcdfa","kind":"commit","task":"project","ts":1786395781.0}
|
|
Binary file
|
|
@@ -1406,7 +1406,7 @@ The routes do not change, so the committed bundle is untouched by any of this:
|
|
|
1406
1406
|
dev A (laptop) dev B (laptop)
|
|
1407
1407
|
┌────────────────────────────┐ ┌────────────────────────────┐
|
|
1408
1408
|
│ $ taskops ui │ │ $ taskops ui │
|
|
1409
|
-
│ /
|
|
1409
|
+
│ / ← the bundle │ │ / ← the bundle │
|
|
1410
1410
|
│ /board/rpc ─────┐ │ │ /board/rpc ─────┐ │
|
|
1411
1411
|
│ /board/git ─ A's clone │ │ /board/git ─ B's clone │
|
|
1412
1412
|
└──────────────────┼─────────┘ └──────────────────┼─────────┘
|
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
The source of truth for release notes — GitHub Releases are extracted from
|
|
4
4
|
here, never written twice.
|
|
5
5
|
|
|
6
|
+
## 0.3.2 — the window's address, prose that reads as prose, and a link that stays inside
|
|
7
|
+
|
|
8
|
+
- **`taskops ui` opens `http://127.0.0.1:<port>/`.** It used to hand out
|
|
9
|
+
`/board/ui/?token=…`, which leaked two implementation details into the one URL
|
|
10
|
+
a human types — `board` is only the name a window mounts its single board
|
|
11
|
+
under, and `/ui/` was a door on a HOST, which has served no page since the
|
|
12
|
+
dashboard moved to the binary. Worse, the page then rewrote the address bar to
|
|
13
|
+
a path recomputed from that mount, so a refresh landed on `/` and got a JSON
|
|
14
|
+
404. The board's own routes are unchanged and still hang off `/board`.
|
|
15
|
+
- **A markdown report is rendered as markdown.** `.md` reports — the ones written
|
|
16
|
+
to be read — were served as their own source, hashes and pipes and all: the
|
|
17
|
+
door typed them `text/plain` and the reader dropped them in a `<pre>`. The door
|
|
18
|
+
now answers `text/markdown` and the dashboard draws them with the same renderer
|
|
19
|
+
a card spec and a close note use. It needs no sandbox and gets none: that
|
|
20
|
+
renderer builds React elements and emits no HTML, so raw markup inside a report
|
|
21
|
+
is drawn as the characters it is. A self-contained HTML report still goes in
|
|
22
|
+
the `allow-scripts`-only frame, and an unrecognised type still degrades to
|
|
23
|
+
plain text.
|
|
24
|
+
- **The card's Worktree block opens the worktree, in the dashboard.** It offered
|
|
25
|
+
a forge `compare ↗` — two branches on somebody else's website, rendered for a
|
|
26
|
+
clone that may not be yours. It now opens the worktree diff view, reading your
|
|
27
|
+
own clone, with the card's thread beside it. It needs no forge slug, so a board
|
|
28
|
+
with no GitHub at all has it too.
|
|
29
|
+
- The chapter goal pane got a taller clamp — a long goal scrolls less.
|
|
30
|
+
|
|
6
31
|
## 0.3.1 — three chapters: reports, GitHub as an introduction, and the whole lifecycle
|
|
7
32
|
|
|
8
33
|
- **Reports.** A milestone's narration becomes part of the board: `taskops_activity`
|
|
@@ -110,7 +110,7 @@ def ui(here: Path) -> int:
|
|
|
110
110
|
port = httpd.server_address[1]
|
|
111
111
|
(root / DIR / "ui.json").write_text(json.dumps({"port": port, "token": token}) + "\n")
|
|
112
112
|
window.retire_when_idle(httpd.mounts, httpd)
|
|
113
|
-
_open(f"http://127.0.0.1:{port}
|
|
113
|
+
_open(f"http://127.0.0.1:{port}/?token={token}")
|
|
114
114
|
try:
|
|
115
115
|
httpd.serve_forever()
|
|
116
116
|
except KeyboardInterrupt:
|
|
@@ -104,7 +104,7 @@ def holder(folder: Path, checkout: Path) -> str:
|
|
|
104
104
|
f"ctrl-c in its terminal, or wait ~{int(IDLE_SECONDS / 60)} minutes: "
|
|
105
105
|
"a window with no open tab retires itself"
|
|
106
106
|
)
|
|
107
|
-
return f"http://127.0.0.1:{port}
|
|
107
|
+
return f"http://127.0.0.1:{port}/?token={token}"
|
|
108
108
|
raise TaskopsError(
|
|
109
109
|
f"something holds {folder / LOCK} but no window answers on port {port or '?'} — "
|
|
110
110
|
"it may be starting (run this again) or wedged: find it with "
|
|
@@ -144,7 +144,16 @@ def drain(board: Caller, repo: Path) -> int:
|
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def push_card(repo: Path, branch: str) -> bool:
|
|
147
|
-
"""Publish the card's branch so a PR (or the orchestrator) can see it.
|
|
147
|
+
"""Publish the card's branch so a PR (or the orchestrator) can see it.
|
|
148
|
+
|
|
149
|
+
BOTH sides of the refspec, for the reason `remote.push` carries in full: a
|
|
150
|
+
one-sided `origin <branch>` is resolved through `push.default`, and under
|
|
151
|
+
`upstream` that is the tracked branch and not this one. Here the tracking is
|
|
152
|
+
also what `--set-upstream` is about to write, so the wrong destination and
|
|
153
|
+
the record of it would agree with each other.
|
|
154
|
+
"""
|
|
148
155
|
if not trailer.card_of(branch):
|
|
149
156
|
return False
|
|
150
|
-
return run.git(
|
|
157
|
+
return run.git(
|
|
158
|
+
"push", "--set-upstream", "origin", f"{branch}:refs/heads/{branch}", cwd=repo
|
|
159
|
+
).ok
|
|
@@ -142,6 +142,19 @@ def push(repo: Path, *branches: str, cwd: Path | None = None) -> None:
|
|
|
142
142
|
return
|
|
143
143
|
for name in names:
|
|
144
144
|
try:
|
|
145
|
-
|
|
145
|
+
# BOTH sides of the refspec, always. `git push origin <name>` looks
|
|
146
|
+
# explicit and is not: with no colon, git resolves the destination
|
|
147
|
+
# through `push.default`, and under `upstream` — a setting a human
|
|
148
|
+
# may have in ~/.gitconfig for their own reasons — that destination
|
|
149
|
+
# is the branch's UPSTREAM, not its name. Measured here on
|
|
150
|
+
# 2026-08-10: milestone branches cut in v1 still tracked
|
|
151
|
+
# `origin/main`, so this best-effort visibility push was writing
|
|
152
|
+
# integration branches onto MAIN, in a function that swallows every
|
|
153
|
+
# error, in a repo whose whole point is that only the human lands
|
|
154
|
+
# anything on the trunk. `<name>:refs/heads/<name>` cannot be
|
|
155
|
+
# reinterpreted by any config.
|
|
156
|
+
run.git(
|
|
157
|
+
"push", "origin", f"{name}:refs/heads/{name}", cwd=where, timeout=PUSH_TIMEOUT
|
|
158
|
+
)
|
|
146
159
|
except TaskopsError: # timeout, or no git at all — never the caller's problem
|
|
147
160
|
return
|
|
@@ -94,6 +94,7 @@ ABSENT = (
|
|
|
94
94
|
|
|
95
95
|
HTML = "text/html"
|
|
96
96
|
TEXT = "text/plain"
|
|
97
|
+
MARKDOWN = "text/markdown"
|
|
97
98
|
|
|
98
99
|
|
|
99
100
|
def answer(repo: Path | None, tail: str, query: str) -> dict[str, Any]:
|
|
@@ -135,9 +136,8 @@ def _file(repo: Path, rev: str, wanted: str) -> dict[str, Any]:
|
|
|
135
136
|
reading a report is the UI reading a patch, and a second vocabulary would be
|
|
136
137
|
a second renderer. `content_type` is a FIELD, never this response's header —
|
|
137
138
|
every door answers `application/json`, so no path can make THIS origin, the
|
|
138
|
-
one the token lives in, serve HTML; the reader sandboxes it.
|
|
139
|
-
|
|
140
|
-
degrade to something no renderer will execute."""
|
|
139
|
+
one the token lives in, serve HTML; the reader sandboxes it. The three types
|
|
140
|
+
`_kind` answers are argued there."""
|
|
141
141
|
path = reports.under(wanted)
|
|
142
142
|
if not path:
|
|
143
143
|
raise BadRequest(NOT_A_REPORT.format(path=wanted or "a missing ?path="))
|
|
@@ -153,7 +153,7 @@ def _file(repo: Path, rev: str, wanted: str) -> dict[str, Any]:
|
|
|
153
153
|
return {
|
|
154
154
|
"path": path,
|
|
155
155
|
"rev": sha,
|
|
156
|
-
"content_type":
|
|
156
|
+
"content_type": _kind(path),
|
|
157
157
|
"text": text,
|
|
158
158
|
"truncated": cut,
|
|
159
159
|
"cap": patch.CAP,
|
|
@@ -186,3 +186,13 @@ def _param(query: str, key: str) -> str:
|
|
|
186
186
|
if name == key:
|
|
187
187
|
return unquote(value.replace("+", " "))
|
|
188
188
|
return ""
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _kind(path: str) -> str:
|
|
192
|
+
"""The three answers, and the default keeps itself safe: an unrecognised
|
|
193
|
+
type falls to `text/plain`, which every reader draws as characters. Decided
|
|
194
|
+
HERE and never in the browser — `ReportFrame` reads what the door SAYS a
|
|
195
|
+
file is, never the extension it can see, so the two cannot drift apart."""
|
|
196
|
+
if path.endswith(".html"):
|
|
197
|
+
return HTML
|
|
198
|
+
return MARKDOWN if path.endswith(".md") else TEXT
|
|
@@ -70,6 +70,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
70
70
|
self._git(board, tail[4:])
|
|
71
71
|
elif tail.startswith("ui"):
|
|
72
72
|
self._static(tail[2:])
|
|
73
|
+
elif (page := static.at_root(self.mounts.ui, self.path)) is not None:
|
|
74
|
+
self._send(200, *page) # a WINDOW serves its page at the ROOT
|
|
73
75
|
else:
|
|
74
76
|
self._fail(404, BadRequest(f"nothing at {self.path}"))
|
|
75
77
|
|
|
@@ -84,6 +84,36 @@ def payload(root: Path, rest: str) -> tuple[bytes, str] | None:
|
|
|
84
84
|
return (path.read_bytes(), content_type(path)) if path else None
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def at_root(root: Path | None, path: str) -> tuple[bytes, str] | None:
|
|
88
|
+
"""The bundle served at the ROOT of a window — `/`, `/app.js`, `/style.css`.
|
|
89
|
+
|
|
90
|
+
`taskops ui` used to hand out `/board/ui/?token=…`, and that path is an
|
|
91
|
+
implementation detail leaking into the one URL a human types: `board` is
|
|
92
|
+
just the name a window mounts its single board under, and `/ui/` was the
|
|
93
|
+
door on a HOST that no longer serves a page at all. A window is a window;
|
|
94
|
+
its address is its port.
|
|
95
|
+
|
|
96
|
+
Deliberately NOT `resolve`: that one falls back to the index for any
|
|
97
|
+
unknown tail, which is right for a door mounted under `/<board>/ui/` and
|
|
98
|
+
wrong at the root, where it would swallow every mistyped API path and
|
|
99
|
+
answer a mistake with a page. Only real files, plus `/` for the index — so
|
|
100
|
+
`/nope` is still the honest 404 the API gives.
|
|
101
|
+
|
|
102
|
+
`root is None` IS the host switch and the only one there is: `Mounts` sets
|
|
103
|
+
`ui` from the same `repo is not None` that /git and /healthz's identity
|
|
104
|
+
read, so a board host cannot start serving a dashboard by any route,
|
|
105
|
+
including this one — there is no second condition to keep in step.
|
|
106
|
+
"""
|
|
107
|
+
if root is None or not root.is_dir():
|
|
108
|
+
return None
|
|
109
|
+
rest = path.partition("?")[0].strip("/")
|
|
110
|
+
base = root.resolve()
|
|
111
|
+
target = (root / INDEX) if not rest else (root / rest).resolve()
|
|
112
|
+
if base != target and base not in target.parents:
|
|
113
|
+
return None # `..%2f..` and symlinks have already collapsed
|
|
114
|
+
return (target.read_bytes(), content_type(target)) if target.is_file() else None
|
|
115
|
+
|
|
116
|
+
|
|
87
117
|
def answer(root: Path | None, rest: str) -> tuple[int, bytes, str]:
|
|
88
118
|
"""The WHOLE /ui door: (status, body, content-type). `root is None` means
|
|
89
119
|
this process is a board host — see the module docstring for why that is a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""The remote board a LOCAL window is a window onto — the one thing that differs.
|
|
2
2
|
|
|
3
3
|
`taskops ui` always serves the dashboard from the checkout it stands in. The
|
|
4
|
-
routes are the same in both modes (
|
|
4
|
+
routes are the same in both modes (`/` for the page, `/board/rpc`, `/board/git/…`),
|
|
5
5
|
so the committed bundle never learns which kind of board it is talking to. The
|
|
6
6
|
whole difference is who answers `/rpc`:
|
|
7
7
|
|