taskops-cli 0.5.6__tar.gz → 0.5.8__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.5.6 → taskops_cli-0.5.8}/ARCHITECTURE.md +70 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/CHANGELOG.md +79 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/PKG-INFO +3 -2
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/README.md +2 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/_version.py +1 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/machine.py +14 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/types.py +5 -0
- taskops_cli-0.5.8/src/taskops/gitwork/inhabited.py +105 -0
- taskops_cli-0.5.8/src/taskops/gitwork/reading.py +165 -0
- taskops_cli-0.5.8/src/taskops/gitwork/scan.py +196 -0
- taskops_cli-0.5.8/src/taskops/http/editor.py +188 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/gitdoor.py +2 -9
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/handler.py +4 -12
- taskops_cli-0.5.8/src/taskops/http/health.py +27 -0
- taskops_cli-0.5.8/src/taskops/http/listing.py +39 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/routes.py +14 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/server.py +5 -0
- taskops_cli-0.5.8/src/taskops/http/treewatch.py +107 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/boardview.py +5 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/brief.py +2 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/dossier.py +4 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/schema.py +8 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/server.py +2 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/tools.py +2 -2
- taskops_cli-0.5.8/src/taskops/ui/app.js +70 -0
- taskops_cli-0.5.8/src/taskops/ui/style.css +1 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_rows.py +2 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/update.py +27 -2
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_core.py +25 -0
- taskops_cli-0.5.8/tests/test_editor.py +492 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_mcp.py +17 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_ui.py +102 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_verbs.py +74 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/fixture.json +325 -120
- taskops_cli-0.5.8/ui/smoke/sections/board-flip-abandoned-play.tsx +252 -0
- taskops_cli-0.5.8/ui/smoke/sections/card-progress.tsx +97 -0
- taskops_cli-0.5.8/ui/smoke/sections/editor-page.tsx +562 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/report-sandbox.tsx +11 -4
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/section.ts +26 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/App.tsx +53 -5
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/client.ts +21 -6
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/board/CardTile.tsx +58 -0
- taskops_cli-0.5.8/ui/src/components/board/flip.ts +281 -0
- taskops_cli-0.5.8/ui/src/components/board/useFlip.ts +71 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/card/Drawer.tsx +39 -3
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/card/Sections.tsx +27 -10
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/chrome/Header.tsx +5 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/chrome/TabNav.tsx +12 -7
- taskops_cli-0.5.8/ui/src/components/editor/CodeView.tsx +212 -0
- taskops_cli-0.5.8/ui/src/components/editor/FileTree.tsx +344 -0
- taskops_cli-0.5.8/ui/src/components/editor/Icon.tsx +96 -0
- taskops_cli-0.5.8/ui/src/components/editor/QuickOpen.tsx +150 -0
- taskops_cli-0.5.8/ui/src/components/editor/Tabs.tsx +150 -0
- taskops_cli-0.5.8/ui/src/components/editor/fuzzy.ts +113 -0
- taskops_cli-0.5.8/ui/src/components/editor/highlight.ts +479 -0
- taskops_cli-0.5.8/ui/src/components/editor/icons.ts +89 -0
- taskops_cli-0.5.8/ui/src/components/editor/tree.ts +194 -0
- taskops_cli-0.5.8/ui/src/components/editor/useWorktree.ts +306 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/panels.ts +36 -0
- taskops_cli-0.5.8/ui/src/pages/Editor.tsx +617 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/pages/WorktreeDiff.tsx +16 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/pages/Worktrees.tsx +32 -1
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/theme/tokens.css +72 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/types.ts +121 -0
- taskops_cli-0.5.6/src/taskops/ui/app.js +0 -61
- taskops_cli-0.5.6/src/taskops/ui/style.css +0 -1
- taskops_cli-0.5.6/ui/src/components/board/flip.ts +0 -105
- taskops_cli-0.5.6/ui/src/components/board/useFlip.ts +0 -96
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.claude/settings.json +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.gitattributes +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.github/workflows/ci.yml +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.github/workflows/workflow.yml +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.gitignore +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.mcp.json +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board/cache.sqlite +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board/cache.sqlite-shm +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board/cache.sqlite-wal +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board/events.jsonl +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board/live.sqlite +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board/live.sqlite-shm +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board/live.sqlite-wal +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/board.json +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/reports/lifecycle-chapter.html +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/reports/reports-chapter.html +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/.taskops/ui.lock +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/CLAUDE.md +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/pyproject.toml +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/smoke.sh +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/_clock.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/_errors.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/_ids.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/_json.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/_locate.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/_wire.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/board.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/__main__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/admin.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/claude.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/commands.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/enrol.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/github.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/gitremote.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/grants.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/hooks.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/main.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/operate.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/paging.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/parser.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/pull.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/push.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/remote.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/rm.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/serving.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/team.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/watch.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/window.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/cli/wording.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/actors.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/challenge.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/chapters.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/event.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/forge.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/graph.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/holding.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/hours.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/kinds.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/mentions.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/replay.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/reports.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/review.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/scope.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/core/seams.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/bare.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/bind.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/catchup.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/claudefiles.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/diff.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/install.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/landing.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/onward.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/patch.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/remote.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/run.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/sig.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/trailer.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/gitwork/trees.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/admin.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/auth.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/feed.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/gitbody.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/gitpack.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/grants.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/ingest.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/login.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/members.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/mounts.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/page.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/removal.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/repos.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/rpc.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/scoped.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/stale.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/static.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/upstream.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/http/watcher.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/identity.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/__main__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/activity.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/before.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/boards.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/chapter.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/fields.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/gitmoves.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/hello.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/integrate.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/orders.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/render.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/mcp/thread.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/py.typed +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/session.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/cache.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/creds.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/handover.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/live.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/log.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/mirroring.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/pubkeys.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/reviews.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/server.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/store/stores.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/ui/index.html +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_args.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_cards.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_chapter.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_context.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_facts.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_mentions.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_stories.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_waiting.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/_windows.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/activity.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/assign.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/card.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/events.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/filed.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/plan.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/project.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/pulse.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/record.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/report.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/review.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/src/taskops/verbs/take.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/__init__.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/conftest.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_architecture.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_claude.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_git.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_mirror.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_report.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_store.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_topology.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/test_window.py +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/tests/ui_harness.js +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/build.mjs +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/index.html +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/package-lock.json +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/package.json +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/main.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/run.mjs +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/actors-window-filter.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/actors.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/all-chapters-scope.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/board-columns.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/board-flow-view.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/board-root-mount.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/card-diff-range.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/chapter-landed.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/chapters-open.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/comment-box.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/daysheet-midnight-spill.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/diff-page.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/dossier.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/event-stream.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/feed-reconnect.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/forge-links.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/git-diff.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/hosted-window.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/monitor-panes.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/overlay-escape.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/report-markdown.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/reviewing-skew.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/swarm-topology.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/thread-closing-note.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/worktree-page-range.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections/worktrees-index.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/smoke/sections.mjs +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/completed.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/actors/Daysheet.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/actors/DevPanel.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/board/Column.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/board/ViewToggle.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/card/CommentBox.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/card/Patch.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/card/Thread.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/card/split.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/card/tokens.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/chrome/AvatarStack.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/chrome/KpiRail.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/flow/FlowView.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/flow/layout.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/Chapter.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/DependencyChain.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/EditSurface.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/EventStream.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/LiveLeases.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/Mentions.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/Pane.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/Swarm.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/monitor/Throughput.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/reports/ReportFrame.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/shared/Avatar.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/shared/Markdown.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/shared/Overlay.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/shared/Segmented.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/shared/overlayStack.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/story/ChapterStory.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/story/stats.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/toasts/ToastStack.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/toasts/model.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/components/toasts/useToasts.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/format.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/hoursWindow.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/links.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/main.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/markdown.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/pages/Actors.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/pages/Board.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/pages/Monitor.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/pages/Reports.tsx +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/theme/theme.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/useBoard.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/src/useEvents.ts +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/styles/index.css +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/ui/tsconfig.json +0 -0
- {taskops_cli-0.5.6 → taskops_cli-0.5.8}/uv.lock +0 -0
|
@@ -107,6 +107,7 @@ erDiagram
|
|
|
107
107
|
list files "edit surface, a hint, never a lock"
|
|
108
108
|
list labels
|
|
109
109
|
string assignee "who it is FOR — not a claim"
|
|
110
|
+
int progress "0-100, the worker's own REPORT — absent until one is made"
|
|
110
111
|
string created_by
|
|
111
112
|
float created
|
|
112
113
|
float updated "replay arbiter, newer-wins"
|
|
@@ -183,6 +184,18 @@ turns it on derives exactly as it did before the feature existed
|
|
|
183
184
|
(`tests/test_core.py::test_a_card_without_review_derives_exactly_as_before`). There is deliberately no path in
|
|
184
185
|
this diagram labelled "recover": nothing is ever wrong for a recover to fix.
|
|
185
186
|
|
|
187
|
+
`progress` (0–100) is the one field that is neither: STORED, like the three
|
|
188
|
+
statuses, and yet no state at all — nothing derives from it, nothing gates on
|
|
189
|
+
it, `done` does not write 100, and a card nobody has reported on has no key
|
|
190
|
+
rather than a zero. It is a worker's own sentence about its own card
|
|
191
|
+
(`verbs/update.py::_progress`), written with `taskops_update progress=` every
|
|
192
|
+
5–10 points, guarded exactly as a release is (yours, and never a stranger's).
|
|
193
|
+
It is not a derived state because deriving it would mean the board guessing how
|
|
194
|
+
far along somebody is; it is not a lifecycle status because nothing may ever
|
|
195
|
+
branch on it. What it buys beyond the number is the heartbeat: the write is MCP
|
|
196
|
+
traffic, and MCP traffic is the lease's only proof of life (§12), so a worker
|
|
197
|
+
that reports is a worker that cannot read as `stalled` while it is reporting.
|
|
198
|
+
|
|
186
199
|
One more fact is derived the same way and is about a *reader*, not a card: a
|
|
187
200
|
**pending mention** (`core/mentions.py`). Being named in a comment's optional
|
|
188
201
|
`mentions[]` is pending until that actor writes any event on the card, or the
|
|
@@ -3042,3 +3055,60 @@ many slots and Actors anchors on a calendar month, so one number cannot be both,
|
|
|
3042
3055
|
and moving between those tabs costs one request. A board one version behind
|
|
3043
3056
|
sends no `window` key at all, and `windowSaid()` falls back to the day-bucket
|
|
3044
3057
|
sentence — a degradation, never a blank.
|
|
3058
|
+
|
|
3059
|
+
---
|
|
3060
|
+
|
|
3061
|
+
## 22. The Editor — a worktree's files, live off the disk (2026-09-06)
|
|
3062
|
+
|
|
3063
|
+
Berna asked for "an editor inside taskops: the worktrees, a sidebar with the
|
|
3064
|
+
file directory, the code on the right in tabs, highlighted — the real code of
|
|
3065
|
+
each worktree in real time, from local". It is the sixth tab
|
|
3066
|
+
(`ui/src/pages/Editor.tsx`), read-only by construction, and it answers the one
|
|
3067
|
+
question the Worktrees page cannot: not what a branch ADDS as a patch, but what
|
|
3068
|
+
the code LOOKS LIKE right now in the directory a worker is sitting in.
|
|
3069
|
+
|
|
3070
|
+
**The contract** (`http/editor.py`, same envelope and same token door as `/git`):
|
|
3071
|
+
|
|
3072
|
+
```
|
|
3073
|
+
GET /<board>/editor/trees every inhabited directory, the checkout (`main`) first
|
|
3074
|
+
GET /<board>/editor/tree?tree=<name> files: path, size, mtime, git state · capped, total, cap, seq
|
|
3075
|
+
GET /<board>/editor/file?tree=&path=[&base=] text or binary, truncated+cap, tracked, base {ref, sha}, marks [[from,to,kind]]
|
|
3076
|
+
GET /<board>/editor/diff?tree=&path=[&base=] the same file as a unified patch — the Worktrees page's own renderer draws it
|
|
3077
|
+
GET /<board>/editor/feed?tree=<name> WebSocket/SSE: {"type":"change","tree","seq"} — a SIGNAL, never the files
|
|
3078
|
+
```
|
|
3079
|
+
|
|
3080
|
+
**Only a WINDOW answers.** The files live on the disk `taskops ui` runs on; a
|
|
3081
|
+
serve-mode host (taskops.bernardocastro.dev) holds boards and at most a bare
|
|
3082
|
+
`repo.git`, so it refuses with ONE sentence naming `taskops ui`
|
|
3083
|
+
(`editor.NO_CHECKOUT`) and the page quotes it — never an empty tree.
|
|
3084
|
+
|
|
3085
|
+
**The security line** is `gitwork/inhabited.py`, three sentences: a tree is a
|
|
3086
|
+
directory NAME matched before it is joined to anything; a file is resolved on
|
|
3087
|
+
both ends and refused — never repaired — when it leaves the tree, names `.git`,
|
|
3088
|
+
or is a symlink out; and a base ref passes `diff.usable`, the wall every ref of
|
|
3089
|
+
the /git door passes. `tests/test_editor.py` drives every shape through the door.
|
|
3090
|
+
|
|
3091
|
+
**Live, without a watcher dependency** (§11 stands): `gitwork/scan.py` lists a
|
|
3092
|
+
tree with git (index + untracked walk honouring every `.gitignore`, the
|
|
3093
|
+
directories nobody reads pruned at the directory) and measures it with one
|
|
3094
|
+
`stat` per file; `http/treewatch.py` runs that once a second for a tree
|
|
3095
|
+
somebody is listening to — `watcher.py`'s lifetime rule — and publishes when
|
|
3096
|
+
two scans differ. The page re-reads the listing on a signal and re-reads only
|
|
3097
|
+
the open tabs whose mtime moved, lighting the lines `changedSpan` names and
|
|
3098
|
+
keeping the scroll (`components/editor/useWorktree.ts`). Measured on a
|
|
3099
|
+
worker's tree in pinecall/v2: a new file in the tree 0.8 s after the write, an
|
|
3100
|
+
open tab re-rendered 0.2 s after it.
|
|
3101
|
+
|
|
3102
|
+
**The caps, all stated in the answer:** `scan.FILE_CAP` = 4000 files per tree
|
|
3103
|
+
(`capped` + `total` beside the list), `reading.CAP` = `patch.CAP` = 512 KiB per
|
|
3104
|
+
file and per patch (`truncated` + `cap`), binary decided on the first 8 KiB
|
|
3105
|
+
(a NUL or non-UTF-8) and never decoded. The gutter's base is the card's own
|
|
3106
|
+
chapter branch through `diff.compare_range` — the merge-base in flight, the
|
|
3107
|
+
fork point once integrated — and `HEAD` for the checkout.
|
|
3108
|
+
|
|
3109
|
+
**Highlighting is a compact tokenizer, not a dependency**
|
|
3110
|
+
(`components/editor/highlight.ts`, ~4 kB minified against ~25–45 kB for Prism
|
|
3111
|
+
or highlight.js with the eleven grammars): keyword, string, comment, number,
|
|
3112
|
+
tag, attribute — enough to read by, in six `--code-*` tokens that live in
|
|
3113
|
+
`theme/tokens.css` beside the palette rather than borrowing its status colours.
|
|
3114
|
+
The bundle grew 288 307 → 313 294 bytes for the whole feature.
|
|
@@ -3,6 +3,85 @@
|
|
|
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.8 — the Editor is the whole window
|
|
7
|
+
|
|
8
|
+
- **Nothing is drawn above the Editor any more** — no KPI rail, no header, no
|
|
9
|
+
tab bar. It was a card on a canvas while also being the only view that wants
|
|
10
|
+
the entire screen, and the cost was visible: a rounded top edge under a bar
|
|
11
|
+
with nothing above it, 24px of canvas down both flanks of a file tree, and a
|
|
12
|
+
bottom edge that read as a scroll that was not there. Padding zero, radius
|
|
13
|
+
zero, no outer border: the panel IS the window. It had shrunk to a small bar
|
|
14
|
+
before, and a small bar is still a lid.
|
|
15
|
+
- **One thin bar inside the page** carries the way out, which tree is open and
|
|
16
|
+
what the door said about it. It sits outside the panel deliberately: the
|
|
17
|
+
refusal and the loading states replace the panel whole, and the exit must not
|
|
18
|
+
go with it. The exit is an arrow — a glyph in a bar of monospace facts, with
|
|
19
|
+
its words in `aria-label` and `title` so nothing is lost to a reader who
|
|
20
|
+
cannot see it — and `onBack` is REQUIRED by the page's props, because this is
|
|
21
|
+
the one view with no tab bar and a caller must not be able to strand somebody
|
|
22
|
+
on it by omission.
|
|
23
|
+
- **`Header.compact` is deleted**: a second shape of that component nothing
|
|
24
|
+
could reach any more, still asking to be kept in step.
|
|
25
|
+
- Four assertions pin the new geometry and each was measured red at its own
|
|
26
|
+
site. One of them was born FALSE and is fixed here — it read the `style`
|
|
27
|
+
attribute with the pattern's attribute order backwards, matched nothing, and
|
|
28
|
+
`undefined !== true` passed with the padding put straight back. It now has to
|
|
29
|
+
FIND the node before it says anything about it, and that blind spot is
|
|
30
|
+
mutated red too.
|
|
31
|
+
|
|
32
|
+
## 0.5.7 — the Editor, and a card that says how far along it is
|
|
33
|
+
|
|
34
|
+
- **A card reports its own progress** — `taskops_update progress=0-100`, an
|
|
35
|
+
argument on the verb that already changes the card and deliberately not a
|
|
36
|
+
twelfth tool. Stored as an `edited` event, so the log, the fold and the
|
|
37
|
+
thread renderer read it with no new kind and no new replay rule. A REPORT and
|
|
38
|
+
never a state (ARCHITECTURE §3): nothing derives from it, nothing gates on
|
|
39
|
+
it, `done` does not write 100, and a card nobody has spoken about carries NO
|
|
40
|
+
key rather than a zero — the tile draws a bar only once somebody has spoken.
|
|
41
|
+
Guarded exactly as a release is: yours, never a stranger's, refused on a
|
|
42
|
+
closed card, and backwards is allowed because a task that turned out bigger
|
|
43
|
+
is honest. The cadence rides where a worker actually reads it — the assign
|
|
44
|
+
brief, the take's footer and the MCP protocol text — and the write doubles as
|
|
45
|
+
the lease's heartbeat, so a worker that reports cannot read as STALLED while
|
|
46
|
+
it is reporting. On screen: a track and its number in the tile, one pill in
|
|
47
|
+
the drawer, `· 35%` on the board's text line.
|
|
48
|
+
- **The board stopped going grey when you switch chapter.** Every tile arrived
|
|
49
|
+
inverted and the play that reveals them never ran, so the columns drew their
|
|
50
|
+
counts over nothing. Three defects, each found only after the one above it
|
|
51
|
+
was fixed: a cancelled play was dropped with its styles instead of being
|
|
52
|
+
played; rects were measured WITH the animation's own transforms, so the
|
|
53
|
+
offsets cascaded (6px → -12px → -6.66px → 12.99px); and the release looked
|
|
54
|
+
ids up in a ref map React had already emptied, since children detach in the
|
|
55
|
+
mutation phase before a parent's cleanup runs. All three now decided in
|
|
56
|
+
`components/board/flip.ts`, where a headless test can reach them, and
|
|
57
|
+
verified in a browser on the only shape that reproduces it — two chapters
|
|
58
|
+
that both have tiles: 0 stuck rounds of 6, where the same probe read 5 of 6.
|
|
59
|
+
|
|
60
|
+
- **A sixth tab, Editor** (`ui/src/pages/Editor.tsx`): pick a worktree — the
|
|
61
|
+
checkout first, then every directory under `.taskops/trees/` — and read its
|
|
62
|
+
files as an editor draws them: a folder tree on the left with each file's git
|
|
63
|
+
state (M/A/S/U) and a dot on every folder something moved under, tabs on the
|
|
64
|
+
right, line numbers, syntax colour for Python, TS/JS, JSON, YAML, TOML,
|
|
65
|
+
Markdown, SQL, bash, HTML and CSS, a gutter mark on every line that changed
|
|
66
|
+
since the branch base, and a per-file "diff vs base" toggle through the same
|
|
67
|
+
patch renderer the Worktrees page uses. Read-only, always.
|
|
68
|
+
- **Live from local disk**: a tree somebody is looking at is scanned once a
|
|
69
|
+
second (git for the listing, `stat` for the measure — no watcher dependency),
|
|
70
|
+
the page re-reads only the open tabs that moved, lights the changed lines and
|
|
71
|
+
keeps the scroll; a "last change: <file> · <ago>" line says the project is
|
|
72
|
+
moving. Doors in from every worktree row, the diff page and the dossier.
|
|
73
|
+
- **The tree folds to the working set** — what the branch wrote since its
|
|
74
|
+
base (`committed`, `deleted`, one `git diff --name-status` per scan) plus
|
|
75
|
+
the dirt — every other folder closed; ⌘P/Ctrl+P quick open with VS Code's
|
|
76
|
+
fuzzy rule (one definition, also the tree's filter); an Explorer-shaped
|
|
77
|
+
sidebar with keyboard navigation, file icons from one inline sprite, a tab
|
|
78
|
+
strip that scrolls on its own axis, and the whole page at viewport height.
|
|
79
|
+
- **`GET /<board>/editor/{trees,tree,file,diff,feed}`** (`http/editor.py`),
|
|
80
|
+
same token door and envelope as `/git`, served ONLY by a window — the
|
|
81
|
+
deployed host says so in one sentence. Files come only from inside the
|
|
82
|
+
board's own worktrees; `..`, absolute paths, `.git` and symlinks out are
|
|
83
|
+
refused. Caps stated in every answer. ARCHITECTURE.md §22.
|
|
84
|
+
|
|
6
85
|
## 0.5.6 — the hours page draws every hour of a day that opened before midnight
|
|
7
86
|
|
|
8
87
|
Reported live on `/convo/`: the hours breakdown was empty. Monday said
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskops-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.8
|
|
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
|
|
@@ -412,7 +412,8 @@ taskops_merge integrate DONE cards into the milestone branch (--no-ff);
|
|
|
412
412
|
milestone= lands a finished chapter. main is never touched
|
|
413
413
|
taskops_take claim your card and get everything back
|
|
414
414
|
taskops_update change the CARD: close, hand in for review, hand back, drop,
|
|
415
|
-
|
|
415
|
+
report progress (progress=0-100, every 5-10 points as you
|
|
416
|
+
work), retitle, re-spec, re-prioritise, declare a dependency
|
|
416
417
|
taskops_review the verifier's one door: claim a submitted card, then
|
|
417
418
|
verdict=pass|changes note=…
|
|
418
419
|
taskops_comment say something on ANY card, including one somebody else holds
|
|
@@ -400,7 +400,8 @@ taskops_merge integrate DONE cards into the milestone branch (--no-ff);
|
|
|
400
400
|
milestone= lands a finished chapter. main is never touched
|
|
401
401
|
taskops_take claim your card and get everything back
|
|
402
402
|
taskops_update change the CARD: close, hand in for review, hand back, drop,
|
|
403
|
-
|
|
403
|
+
report progress (progress=0-100, every 5-10 points as you
|
|
404
|
+
work), retitle, re-spec, re-prioritise, declare a dependency
|
|
404
405
|
taskops_review the verifier's one door: claim a submitted card, then
|
|
405
406
|
verdict=pass|changes note=…
|
|
406
407
|
taskops_comment say something on ANY card, including one somebody else holds
|
|
@@ -111,6 +111,20 @@ def check_release(card: Card, facts: Facts, actor: str, note: str) -> None:
|
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
|
|
114
|
+
def check_progress(card: Card, facts: Facts, actor: str) -> None:
|
|
115
|
+
"""A percentage is the worker's own report, so it is the worker's to write.
|
|
116
|
+
|
|
117
|
+
The same wall as closing and releasing (`_not_somebody_elses`): a lapsed
|
|
118
|
+
lease does not cost you your own card, a live holder who is somebody else
|
|
119
|
+
does. Nothing else is judged — not the direction (40 after 60 is honest when
|
|
120
|
+
the task turned out bigger) and not the step, because a guard on either
|
|
121
|
+
would only teach a worker to lie in fives.
|
|
122
|
+
"""
|
|
123
|
+
if facts.status in CLOSED:
|
|
124
|
+
raise Refused(f"{card['id']} is {facts.status}; there is no progress left to report")
|
|
125
|
+
_not_somebody_elses(card, facts, actor)
|
|
126
|
+
|
|
127
|
+
|
|
114
128
|
def _not_somebody_elses(card: Card, facts: Facts, actor: str) -> None:
|
|
115
129
|
"""A lease that lapsed does NOT cost you your own card.
|
|
116
130
|
|
|
@@ -85,6 +85,10 @@ class Card(TypedDict):
|
|
|
85
85
|
files: list[str] # the edit surface as the planner understands it — a hint, never a lock
|
|
86
86
|
labels: list[str] # routing and search; anybody may edit them
|
|
87
87
|
assignee: str # "" is the open pool; dispatch writes it. NOT a claim — the lease is.
|
|
88
|
+
progress: NotRequired[int]
|
|
89
|
+
"""0–100, the worker's own estimate, written by `taskops_update progress=`
|
|
90
|
+
and absent until the first report. A REPORT, never a state: nothing derives
|
|
91
|
+
from it and nothing gates on it (`verbs/update.py::_progress` argues it)."""
|
|
88
92
|
created_by: str
|
|
89
93
|
created: float
|
|
90
94
|
updated: float # the replay arbiter (newer-wins)
|
|
@@ -189,5 +193,6 @@ EDITABLE = (
|
|
|
189
193
|
"labels",
|
|
190
194
|
"assignee",
|
|
191
195
|
"review",
|
|
196
|
+
"progress",
|
|
192
197
|
)
|
|
193
198
|
LIST_FIELDS = ("after", "files", "labels", "criteria") # edited as a whole list, never appended to
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The inhabited directories of a checkout — the main worktree and every tree
|
|
2
|
+
under `.taskops/trees` — named, found, and walled.
|
|
3
|
+
|
|
4
|
+
`trees.py` is the GEOMETRY (where a card's directory goes, how it is cut);
|
|
5
|
+
this is the READ of that geometry for a screen: which directories exist right
|
|
6
|
+
now, and whether a path a browser sent stays inside one of them. The Editor
|
|
7
|
+
(ARCHITECTURE.md §22) shows the real files of a real worktree, so the walls
|
|
8
|
+
here are the whole of its security posture, in three sentences:
|
|
9
|
+
|
|
10
|
+
* A tree is addressed by its DIRECTORY NAME under `.taskops/trees/` — `tk-…`,
|
|
11
|
+
`_ms-…` — or by `main`, the checkout itself. `NAME` admits no slash and no
|
|
12
|
+
`..`, so a name never becomes a path before `locate()` has joined it to the
|
|
13
|
+
one directory it may name.
|
|
14
|
+
* A file is addressed by a plain repo-relative path, and `inside()` decides by
|
|
15
|
+
RESOLVING both ends: the real path of the file must sit under the real path
|
|
16
|
+
of the tree. A `..`, an absolute path, a doubled slash, a NUL, or a symlink
|
|
17
|
+
that leaves the tree all collapse to the same answer, `None` — a path is
|
|
18
|
+
refused, never repaired.
|
|
19
|
+
* `.git` is never a file of the project. In a linked worktree it is a FILE
|
|
20
|
+
pointing at the common dir, and the common dir holds every branch's objects
|
|
21
|
+
and the hooks; in the checkout it is the directory itself. Neither is code.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from typing import NamedTuple
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from . import run
|
|
31
|
+
from .trees import trees_dir
|
|
32
|
+
|
|
33
|
+
MAIN = "main"
|
|
34
|
+
"""The checkout itself, listed first: the one directory that is not a card."""
|
|
35
|
+
|
|
36
|
+
NAME = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]{0,80}$")
|
|
37
|
+
"""A tree's name — one directory name, never a path. `..` cannot match it."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Tree(NamedTuple):
|
|
41
|
+
name: str
|
|
42
|
+
path: Path
|
|
43
|
+
dir: str # repo-relative: "" for the checkout, `.taskops/trees/<name>` otherwise
|
|
44
|
+
branch: str # "" on a detached HEAD
|
|
45
|
+
head: str # the commit checked out, 40-hex, "" when git could not say
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def listed(repo: Path) -> list[Tree]:
|
|
49
|
+
"""Every inhabited directory, the checkout first and the trees in name order.
|
|
50
|
+
|
|
51
|
+
A directory under `trees/` with no `.git` in it is not a worktree — a stray
|
|
52
|
+
folder, a removed tree git has not pruned — and is left out rather than
|
|
53
|
+
shown as one: a tree on this screen is a place a worker can be."""
|
|
54
|
+
found = [describe(MAIN, repo, "")]
|
|
55
|
+
root = trees_dir(repo)
|
|
56
|
+
if root.is_dir():
|
|
57
|
+
for path in sorted(root.iterdir()):
|
|
58
|
+
if path.is_dir() and (path / ".git").exists():
|
|
59
|
+
found.append(describe(path.name, path, str(path.relative_to(repo))))
|
|
60
|
+
return found
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def locate(repo: Path, name: str) -> Path | None:
|
|
64
|
+
"""The directory `name` addresses, or None — the ONLY door from a string to
|
|
65
|
+
a tree. The name is matched BEFORE it is joined to anything."""
|
|
66
|
+
if name == MAIN:
|
|
67
|
+
return repo
|
|
68
|
+
if not NAME.match(name):
|
|
69
|
+
return None
|
|
70
|
+
path = trees_dir(repo) / name
|
|
71
|
+
return path if path.is_dir() and (path / ".git").exists() else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def inside(tree: Path, rel: str) -> Path | None:
|
|
75
|
+
"""The real file `rel` names inside `tree`, or None.
|
|
76
|
+
|
|
77
|
+
Resolved on both ends, so a symlink that points outside the tree is caught
|
|
78
|
+
by the same comparison that catches `..` — there is one wall, not a list of
|
|
79
|
+
shapes to recognise. A path that resolves to the tree itself is refused
|
|
80
|
+
too: the door reads FILES."""
|
|
81
|
+
if not rel or rel.startswith(("/", "\\")) or "\\" in rel or any(ord(c) < 32 for c in rel):
|
|
82
|
+
return None
|
|
83
|
+
parts = rel.split("/")
|
|
84
|
+
if any(part in ("", ".", "..", ".git") for part in parts):
|
|
85
|
+
return None
|
|
86
|
+
base = tree.resolve()
|
|
87
|
+
try:
|
|
88
|
+
target = (tree / rel).resolve(strict=True)
|
|
89
|
+
except OSError:
|
|
90
|
+
return None
|
|
91
|
+
if base not in target.parents:
|
|
92
|
+
return None
|
|
93
|
+
return target if target.is_file() else None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def describe(name: str, path: Path, rel: str) -> Tree:
|
|
97
|
+
"""One inhabited directory, as the screen names it: two git questions."""
|
|
98
|
+
head = run.git("rev-parse", "--verify", "--quiet", "HEAD", cwd=path)
|
|
99
|
+
return Tree(
|
|
100
|
+
name=name,
|
|
101
|
+
path=path,
|
|
102
|
+
dir=rel,
|
|
103
|
+
branch=run.branch_at(path),
|
|
104
|
+
head=head.out.strip() if head.ok else "",
|
|
105
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""One file of a worktree, read for the screen: text or "binary", capped, and
|
|
2
|
+
the lines that moved since the branch base — as gutter marks, or as a patch.
|
|
3
|
+
|
|
4
|
+
The WORKING COPY is the subject, not a commit: the Editor (ARCHITECTURE.md §22)
|
|
5
|
+
shows what a worker's directory holds right now, saved and unsaved-to-git
|
|
6
|
+
alike, which is exactly what `patch.show` (a committed file at a sha) cannot
|
|
7
|
+
say. So the bytes come off the disk, and "what changed" is `git diff <base> --
|
|
8
|
+
<path>` run INSIDE the tree, which git reads as "this commit against the
|
|
9
|
+
working copy as the index sees it". An untracked file is not in the index and
|
|
10
|
+
that diff is empty for it; `--no-index` against `/dev/null` is what git itself
|
|
11
|
+
offers for "a file that is all addition", and it is the one exception spelled
|
|
12
|
+
out here rather than a synthetic patch assembled by hand.
|
|
13
|
+
|
|
14
|
+
The BASE is a merge-base, never the ref itself, for the reason `diff.py`
|
|
15
|
+
argues at length: a card's chapter has moved on since the card branched, and
|
|
16
|
+
diffing against its tip would draw every sibling's work as this file's
|
|
17
|
+
change. No ref given means HEAD — "what did this directory change since its
|
|
18
|
+
last commit" — which is the right question for the checkout itself.
|
|
19
|
+
|
|
20
|
+
The file cap is `patch.CAP`, the one number every capped answer in this
|
|
21
|
+
package states, and a cut file SAYS it was cut. Binary is decided on the
|
|
22
|
+
first `PROBE` bytes: a NUL, or bytes that are not UTF-8, and the answer is
|
|
23
|
+
"binary, N bytes" with no text at all — never a screen of replacement glyphs.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
from typing import NamedTuple
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from . import run, diff, patch
|
|
33
|
+
|
|
34
|
+
CAP = patch.CAP
|
|
35
|
+
PROBE = 8192
|
|
36
|
+
|
|
37
|
+
HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")
|
|
38
|
+
"""A hunk header: only the NEW side's start is read — every mark is a fact
|
|
39
|
+
about the file on screen — and the counts come from the body, because with
|
|
40
|
+
zero context git folds a changed line and the lines added right after it into
|
|
41
|
+
ONE hunk, and the header's two counts cannot say which is which."""
|
|
42
|
+
|
|
43
|
+
ADDED, MODIFIED, DELETED = "added", "modified", "deleted"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Read(NamedTuple):
|
|
47
|
+
text: str
|
|
48
|
+
binary: bool
|
|
49
|
+
truncated: bool
|
|
50
|
+
size: int
|
|
51
|
+
mtime: float
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Base(NamedTuple):
|
|
55
|
+
ref: str # what was asked for: a branch name, or HEAD
|
|
56
|
+
sha: str # what was diffed against: the merge-base, 40-hex
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Mark(NamedTuple):
|
|
60
|
+
start: int # 1-based, inclusive, on the file as it stands
|
|
61
|
+
end: int
|
|
62
|
+
kind: str # ADDED | MODIFIED | DELETED (something was removed after `start`)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def read(path: Path, cap: int = CAP) -> Read:
|
|
66
|
+
"""The bytes on disk, decided text or binary and capped. `path` has passed
|
|
67
|
+
`inhabited.inside` — this function trusts it and resolves nothing."""
|
|
68
|
+
found = path.stat()
|
|
69
|
+
with path.open("rb") as handle:
|
|
70
|
+
head = handle.read(cap + 1)
|
|
71
|
+
if _binary(head[:PROBE]):
|
|
72
|
+
return Read("", True, False, found.st_size, found.st_mtime)
|
|
73
|
+
cut = len(head) > cap
|
|
74
|
+
text = head[:cap].decode("utf-8", "ignore") if cut else head.decode("utf-8", "replace")
|
|
75
|
+
return Read(text, False, cut, found.st_size, found.st_mtime)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def base_of(tree: Path, ref: str) -> Base | None:
|
|
79
|
+
"""The commit the working copy is compared against, or None when `ref`
|
|
80
|
+
names nothing this tree can see.
|
|
81
|
+
|
|
82
|
+
`diff.compare_range(ref, HEAD)` is the whole arithmetic, REUSED: the
|
|
83
|
+
merge-base while the card is in flight, and — once the card is integrated
|
|
84
|
+
and its tip is an ancestor of the chapter — the point it forked from, read
|
|
85
|
+
off the merge that brought it in. Seen live on a card the orchestrator had
|
|
86
|
+
just merged: `merge-base(ms/…, HEAD)` was HEAD itself, and every line the
|
|
87
|
+
card wrote read as unchanged. Both names pass `diff.resolve` inside it, the
|
|
88
|
+
package's one door from a string to a sha."""
|
|
89
|
+
if not ref:
|
|
90
|
+
head = diff.resolve(tree, "HEAD")
|
|
91
|
+
return Base("HEAD", head) if head else None
|
|
92
|
+
found = diff.compare_range(tree, ref, "HEAD")
|
|
93
|
+
return Base(ref, found[0]) if found else None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def tracked(tree: Path, rel: str) -> bool:
|
|
97
|
+
"""Is this path in the index? `--error-unmatch` makes git say so by exit
|
|
98
|
+
code, which is the whole answer."""
|
|
99
|
+
return run.git("ls-files", "--error-unmatch", "--", rel, cwd=tree).ok
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def marks(tree: Path, sha: str, rel: str, *, is_tracked: bool, lines: int) -> list[Mark]:
|
|
103
|
+
"""Which lines of the file ON SCREEN differ from `sha`, from a zero-context
|
|
104
|
+
diff, paired POSITIONALLY inside each hunk the way `ui/src/components/card/
|
|
105
|
+
split.ts` pairs a patch: the first `+` lines opposite `-` lines are
|
|
106
|
+
modifications, the `+` lines past them are additions, and a hunk with no
|
|
107
|
+
`+` at all is a deletion after its anchor line."""
|
|
108
|
+
if not is_tracked:
|
|
109
|
+
return [Mark(1, lines, ADDED)] if lines else []
|
|
110
|
+
found: list[Mark] = []
|
|
111
|
+
opened, start, minus, plus = False, 0, 0, 0
|
|
112
|
+
for line in _diff(tree, sha, rel, context=0, is_tracked=True).splitlines():
|
|
113
|
+
hunk = HUNK.match(line)
|
|
114
|
+
if hunk is not None:
|
|
115
|
+
if opened:
|
|
116
|
+
found.extend(_marks_of(start, minus, plus))
|
|
117
|
+
opened, start, minus, plus = True, int(hunk.group(1)), 0, 0
|
|
118
|
+
elif opened and line.startswith("-"): # before the first `@@` the file
|
|
119
|
+
minus += 1 # headers `---`/`+++` are not lines of anybody's file
|
|
120
|
+
elif opened and line.startswith("+"):
|
|
121
|
+
plus += 1
|
|
122
|
+
if opened:
|
|
123
|
+
found.extend(_marks_of(start, minus, plus))
|
|
124
|
+
return found
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _marks_of(start: int, minus: int, plus: int) -> list[Mark]:
|
|
128
|
+
"""One hunk's marks, positionally paired."""
|
|
129
|
+
if plus == 0:
|
|
130
|
+
anchor = max(start, 1) # `+0,0`: the deletion sits at the very top
|
|
131
|
+
return [Mark(anchor, anchor, DELETED)]
|
|
132
|
+
paired = min(minus, plus)
|
|
133
|
+
found = [Mark(start, start + paired - 1, MODIFIED)] if paired else []
|
|
134
|
+
if plus > paired:
|
|
135
|
+
found.append(Mark(start + paired, start + plus - 1, ADDED))
|
|
136
|
+
return found
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def patch_of(tree: Path, sha: str, rel: str, *, is_tracked: bool, cap: int = CAP) -> tuple[str, bool]:
|
|
140
|
+
"""(text, truncated) — the same unified patch the Worktrees page draws,
|
|
141
|
+
for one file of the working copy against `sha`."""
|
|
142
|
+
return patch.capped(_diff(tree, sha, rel, context=3, is_tracked=is_tracked), cap)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _diff(tree: Path, sha: str, rel: str, *, context: int, is_tracked: bool) -> str:
|
|
146
|
+
"""`git diff` exits 1 when the two sides differ — that is an answer, not a
|
|
147
|
+
failure, so both codes are read and anything else is an empty patch."""
|
|
148
|
+
if is_tracked:
|
|
149
|
+
raw = run.git("diff", "--no-color", f"-U{context}", sha, "--", rel, cwd=tree)
|
|
150
|
+
else:
|
|
151
|
+
raw = run.git("diff", "--no-color", f"-U{context}", "--no-index", "--", "/dev/null", rel, cwd=tree)
|
|
152
|
+
return raw.out if raw.code in (0, 1) else ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _binary(probe: bytes) -> bool:
|
|
156
|
+
"""A NUL says binary outright. Bytes that are not UTF-8 say so too — unless
|
|
157
|
+
the failure sits in the last three bytes, where a codepoint the probe cut in
|
|
158
|
+
half looks exactly like one."""
|
|
159
|
+
if b"\0" in probe:
|
|
160
|
+
return True
|
|
161
|
+
try:
|
|
162
|
+
probe.decode("utf-8")
|
|
163
|
+
except UnicodeDecodeError as err:
|
|
164
|
+
return err.start < len(probe) - 3
|
|
165
|
+
return False
|