taskops-cli 0.5.1__tar.gz → 0.5.3__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.1 → taskops_cli-0.5.3}/ARCHITECTURE.md +300 -5
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/CHANGELOG.md +77 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/CLAUDE.md +57 -7
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/PKG-INFO +80 -9
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/README.md +79 -8
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/smoke.sh +91 -10
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_version.py +1 -1
- taskops_cli-0.5.3/src/taskops/cli/gitremote.py +198 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/main.py +10 -1
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/operate.py +2 -2
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/parser.py +18 -3
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/remote.py +11 -1
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/forge.py +10 -0
- taskops_cli-0.5.3/src/taskops/gitwork/bare.py +122 -0
- taskops_cli-0.5.3/src/taskops/gitwork/onward.py +168 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/run.py +83 -1
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/auth.py +22 -0
- taskops_cli-0.5.3/src/taskops/http/gitbody.py +69 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/gitdoor.py +30 -28
- taskops_cli-0.5.3/src/taskops/http/gitpack.py +176 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/handler.py +16 -33
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/mounts.py +3 -3
- taskops_cli-0.5.3/src/taskops/http/page.py +42 -0
- taskops_cli-0.5.3/src/taskops/http/repos.py +109 -0
- taskops_cli-0.5.3/src/taskops/http/stale.py +114 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/static.py +24 -19
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/boardview.py +20 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/live.py +5 -0
- taskops_cli-0.5.3/src/taskops/store/mirroring.py +96 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/ui/app.js +9 -9
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/pulse.py +5 -0
- taskops_cli-0.5.3/tests/test_mirror.py +264 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_topology.py +760 -119
- taskops_cli-0.5.3/ui/smoke/sections/card-diff-range.tsx +119 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/git-diff.tsx +1 -1
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Patch.tsx +34 -21
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Sections.tsx +17 -15
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/links.tsx +77 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/WorktreeDiff.tsx +5 -2
- taskops_cli-0.5.1/src/taskops/gitwork/mirror.py +0 -132
- taskops_cli-0.5.1/src/taskops/http/repos.py +0 -98
- taskops_cli-0.5.1/src/taskops/http/stale.py +0 -62
- taskops_cli-0.5.1/tests/test_mirror.py +0 -164
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.claude/settings.json +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.gitattributes +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.github/workflows/ci.yml +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.github/workflows/workflow.yml +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.gitignore +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.mcp.json +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/cache.sqlite +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/cache.sqlite-shm +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/cache.sqlite-wal +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/events.jsonl +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/live.sqlite +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/live.sqlite-shm +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board/live.sqlite-wal +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/board.json +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/reports/lifecycle-chapter.html +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/reports/reports-chapter.html +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/.taskops/ui.lock +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/pyproject.toml +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_clock.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_errors.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_ids.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_json.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_locate.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/_wire.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/board.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/__main__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/admin.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/claude.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/commands.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/enrol.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/github.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/grants.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/hooks.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/paging.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/pull.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/push.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/rm.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/serving.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/team.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/watch.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/window.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/cli/wording.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/actors.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/challenge.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/chapters.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/event.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/graph.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/holding.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/hours.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/kinds.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/machine.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/mentions.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/replay.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/reports.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/review.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/scope.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/seams.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/core/types.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/bind.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/catchup.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/claudefiles.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/diff.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/install.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/landing.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/patch.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/remote.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/sig.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/trailer.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/gitwork/trees.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/admin.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/feed.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/grants.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/ingest.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/login.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/members.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/removal.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/routes.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/rpc.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/scoped.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/server.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/upstream.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/http/watcher.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/identity.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/__main__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/activity.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/before.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/boards.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/brief.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/chapter.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/dossier.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/fields.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/gitmoves.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/hello.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/integrate.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/orders.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/render.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/schema.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/server.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/thread.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/mcp/tools.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/py.typed +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/session.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/cache.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/creds.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/handover.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/log.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/pubkeys.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/reviews.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/server.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/store/stores.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/ui/index.html +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/ui/style.css +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_args.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_cards.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_chapter.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_context.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_facts.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_mentions.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_rows.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_stories.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_waiting.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/_windows.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/activity.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/assign.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/card.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/events.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/filed.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/plan.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/project.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/record.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/report.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/review.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/take.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/src/taskops/verbs/update.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/__init__.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/conftest.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_architecture.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_claude.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_core.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_git.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_mcp.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_report.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_store.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_ui.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_verbs.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/test_window.py +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/tests/ui_harness.js +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/build.mjs +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/index.html +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/package-lock.json +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/package.json +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/fixture.json +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/main.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/run.mjs +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/actors-window-filter.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/actors.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/all-chapters-scope.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-columns.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-flow-view.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-motion-and-avatars.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/board-root-mount.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapter-completion-derivation.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapter-landed.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapter-story-grid.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/chapters-open.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/comment-box.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/comment-toasts-model.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/comment-toasts-stack.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/diff-page.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/dossier.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/event-stream.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/feed-reconnect.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/forge-links.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/forge-opens-the-board.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/hosted-window.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/monitor-panes.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/overlay-escape.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/report-markdown.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/report-sandbox.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/reviewing-skew.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/section.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/swarm-topology.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/thread-closing-note.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections/worktrees-index.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/smoke/sections.mjs +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/App.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/client.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/completed.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/actors/Daysheet.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/actors/DevPanel.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/CardTile.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/Column.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/ViewToggle.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/flip.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/board/useFlip.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/CommentBox.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Drawer.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/Thread.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/split.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/card/tokens.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/AvatarStack.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/Header.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/KpiRail.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/chrome/TabNav.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/flow/FlowView.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/flow/layout.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Chapter.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/DependencyChain.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/EditSurface.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/EventStream.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/LiveLeases.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Mentions.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Pane.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Swarm.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/Throughput.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/monitor/panels.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/reports/ReportFrame.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Avatar.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Markdown.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Overlay.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/Segmented.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/shared/overlayStack.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/story/ChapterStory.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/story/stats.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/toasts/ToastStack.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/toasts/model.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/components/toasts/useToasts.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/format.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/hoursWindow.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/main.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/markdown.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Actors.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Board.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Monitor.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Reports.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/pages/Worktrees.tsx +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/theme/theme.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/theme/tokens.css +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/types.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/useBoard.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/src/useEvents.ts +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/styles/index.css +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/ui/tsconfig.json +0 -0
- {taskops_cli-0.5.1 → taskops_cli-0.5.3}/uv.lock +0 -0
|
@@ -837,14 +837,15 @@ nobody has touched yet.
|
|
|
837
837
|
| a `recover` verb | doing is derived from the live lease; nothing is ever wrong to recover. Handing a card on is not that verb and never grew into one — it stayed inside `assign`, needs a named replacement, and is argued in §12 | no entry in `verbs/__init__.py::REGISTRY`; `tests/test_verbs.py::test_a_dead_workers_card_comes_back_by_itself` |
|
|
838
838
|
| a reviewer ROLE, a stored review STATUS, automatic reviewer assignment | v1's review system: `peer` deadlocks, 14 closing rules over 6 modules, reviewers eating the budget of the work | review EXISTS since 2026-08-07 but narrowed: optional per card, derived from history-only events + a second lease, judged by an ordinary agent that may never judge its own work. `CARD_STATUSES` stays three; there is no reviewer role and nothing auto-assigns |
|
|
839
839
|
| AUTOMATIC merges to the trunk | v1's `land` merged as a side effect of closing a card and ran checkout under working agents | a CARD cannot be merged to the trunk — `taskops_merge task=` takes no target. A finished MILESTONE lands via `taskops_merge milestone=` (2026-08-07): explicit, refused while any card is open or unintegrated, refused off-trunk, recorded as a `milestone landed` event. What stays impossible is the trunk moving as a side effect of anything |
|
|
840
|
-
| git replication between clones | split-brain, two machines "owning" the same card | `RemoteBoard` never falls back to a local store on write failure (`Unreachable` instead) |
|
|
840
|
+
| git replication between clones | split-brain, two machines "owning" the same card | `RemoteBoard` never falls back to a local store on write failure (`Unreachable` instead). Narrowed on 2026-08-31, not lifted: the host became the board's git REMOTE (§16, "The host becomes the remote") — a hub, one direction on each leg (worktree → host, host → forge). What stays banned is BETWEEN clones: no clone pushes to another clone, and the host never pulls git from the forge |
|
|
841
841
|
| Claude hooks **that decide or store** | latency, another thing to install and drift; v1's held state and gated actions | context travels in `initialize.instructions` + tool responses. The ONE exception, sanctioned 2026-08-06: `taskops hook claude`, delivery-only — it reads, injects a ✉ or ◆ line, and can be deleted with no loss but immediacy. `tests/test_claude.py` pins its safety properties, one test each — the module docstring lists them and `grep -c '^def test_' tests/test_claude.py` counts them |
|
|
842
842
|
| a mark-as-read / ack verb for mentions | a stored `read` flag is `recover` again: a write whose only job is to contradict an earlier one | `core/mentions.py::pending()` derives it from the thread; `tests/test_verbs.py::test_a_mention_clears_itself_the_moment_the_actor_touches_the_card` |
|
|
843
843
|
| PER-REQUEST SIGNING (every call carrying an SSHSIG envelope, no sessions) | it was the first design and sessions won on cost, not on taste: a signature per request means teaching a signature envelope to THREE transports — the stdlib `http.server`, the WebSocket handshake on `/feed`, and the MCP layer that opens a board once and holds it — and every one of them would have had to learn nonce replay, clock skew and canonicalisation separately, for exactly the result a bearer already has. It also needs the private key FILE on every call, in every worker, forever; a session needs it twice a day. And the fleet argument decides it: a signed envelope is a NEW wire format, so production's four boards would have had to be re-joined, which rule 3 forbids | `/login` is the only door that verifies a signature (`http/login.py`), and what it hands back is an ordinary `Credential` — the same row, the same table, the same `Authorization: Bearer` every legacy token uses (`store/creds.py`). The client half is `session.py` (the token's life: mint, refresh, remember) with `identity.py` beside it (WHO signs in and with WHICH key — `discover_key`, `establish`, the one door `--key` comes in through); each is well under the budget *because* each has one job. `tests/test_topology.py::test_a_legacy_only_board_still_works_on_rpc` and its three siblings are the fleet half: production's exact state — no principal, no key, an empty `allowed_signers` — driven through /rpc, /feed, the MCP handshake and the `taskops ui` window |
|
|
844
844
|
| HAND-ROLLED CRYPTO, and a pip crypto DEPENDENCY | two ways to lose the same argument. Writing ed25519 verification by hand is the classic own-goal; adding `cryptography` or `PyNaCl` to buy it back breaks "a wheel and a directory" — the property that makes `pip install taskops` on a bare box a deploy (§17) — and puts a compiled wheel in the path of every agent's install | the verifier is OpenSSH's own: `ssh-keygen -Y sign` / `-Y verify -f allowed_signers`, the same SSHSIG mechanism git uses to sign commits, invoked through the ONE subprocess module (`gitwork/sig.py`, under `gitwork/run.py`). `pyproject.toml` has no runtime dependency at all, and `tests/test_architecture.py` keeps `subprocess` out of every layer but that one |
|
|
845
|
-
| ANONYMOUS WRITES, in any form — including the invisible one | a public board is anonymous READ and nothing else. The subtle failure is not a card somebody could see: every read verb opens with `stores.live.renew(actor, now)`, an INSERT into `presence`, so a public board without a guard has every visitor writing to `live.sqlite` on every page load — no event, no card, nothing any ordinary test would notice. There is also no "anonymous-write grace" and no third visibility | `http/auth.py::anonymous` never hands out a credential with more than `{"read"}`, and refuses a write with the sentence that names how a key gets registered; `store/live.py::renew` is the ONE place that decides the presence row. `tests/test_topology.py::test_an_anonymous_crawl_of_a_public_board_moves_not_one_byte` asserts `events.jsonl` and `live.sqlite` (with `-wal` and `-shm`) hash-identical across a crawl of every read door there is, and `…::test_anonymous_may_not_claim_to_be_somebody` closes the `actor=` hole |
|
|
845
|
+
| ANONYMOUS WRITES, in any form — including the invisible one | a public board is anonymous READ and nothing else. The subtle failure is not a card somebody could see: every read verb opens with `stores.live.renew(actor, now)`, an INSERT into `presence`, so a public board without a guard has every visitor writing to `live.sqlite` on every page load — no event, no card, nothing any ordinary test would notice. There is also no "anonymous-write grace" and no third visibility. A `git push` to the host's `repo.git` is a write under this ban, exactly as an event is (§16, "The host becomes the remote") | `http/auth.py::anonymous` never hands out a credential with more than `{"read"}`, and refuses a write with the sentence that names how a key gets registered; `store/live.py::renew` is the ONE place that decides the presence row. `tests/test_topology.py::test_an_anonymous_crawl_of_a_public_board_moves_not_one_byte` asserts `events.jsonl` and `live.sqlite` (with `-wal` and `-shm`) hash-identical across a crawl of every read door there is, and `…::test_anonymous_may_not_claim_to_be_somebody` closes the `actor=` hole |
|
|
846
846
|
| a `--force` on `taskops board push`, and a SYNC channel behind `board.ingest` | a non-empty target means two histories, and giving them an order they never had fabricates a timeline the board never observed. The precondition ("no history but its own beginning") is true exactly once in a board's life, which is what keeps the door a promotion and not replication between clones — the thing banned two rows up | `http/ingest.py` refuses and says so; `tests/test_topology.py::test_a_target_that_is_not_empty_is_refused_and_no_force_is_offered`, and the argument is the module's own docstring |
|
|
847
847
|
| a `--force` on `taskops board rm` (and it is not an alias for `--discard-history`) | removing a board deletes the only copy of a history nobody can regenerate, and `--force` is a word every tool spends on something recoverable — it names no consequence, so it cannot warn. The flag that gets past the guardrail names the thing it destroys, and the guardrail itself is on the HOST, comparing the ids the caller says it holds against the ids the board really has: a wall the client enforces is a convention | `http/removal.py` refuses unless `core/holding.py` says the history is held elsewhere, and only a literal `true` opens `discard_history`; `tests/test_topology.py::test_the_flag_is_named_for_what_it_destroys_and_a_force_is_not_a_synonym` and `…::test_board_rm_refuses_a_history_this_checkout_does_not_hold_and_names_both_ways_out` |
|
|
848
|
+
| a `--force` push, a ref deletion, or a prune against the host's `repo.git` — and a HAND-ROLLED git protocol behind it | the host became the board's git remote (§16, "The host becomes the remote", 2026-08-31) precisely because a forge-side prune erased a landed card's diff from the board; a force or delete on the host would reintroduce the same erasure at the hub, and there is no flag that opens either, for `board rm`'s reason — `--force` names no consequence. And the pack protocol is git's own, spoken by git's plumbing through the one subprocess module, for the reason hand-rolled crypto is banned two rows up: a Python pkt-line is the same own-goal with a different file extension | `gitwork/bare.py` writes `receive.denyDeletes` and `receive.denyNonFastForwards` into the created repo's own config, so the refusing process IS the `git receive-pack` that would move the ref (`tests/test_topology.py::test_the_host_refuses_a_ref_deletion` and `…_a_non_fast_forward_even_forced` pin both, `--force` included); `subprocess` stays confined to `gitwork/run.py` by `tests/test_architecture.py` |
|
|
848
849
|
| a SILENT `taskops join` over a local board | the local history stayed on disk byte for byte and nothing ever looked at it again or said so — a command about connecting is what made it invisible | `cli/commands.py::_keep_or_archive` refuses naming `taskops board push` and `--discard-local` (which archives, never deletes); `tests/test_topology.py::test_join_refuses_to_orphan_a_local_board_and_names_both_ways_out` |
|
|
849
850
|
| GitHub as a CREDENTIAL — v1's GitHub login: a stored token, a call to GitHub on every sign-in | three costs, and §19 is the whole argument: a token that travels (worth stealing for reasons unrelated to the board), a network dependency at every login (GitHub down = nobody signs in), and a SECOND identity system beside the keys, with its own enrolment, expiry and revocation to keep in step. The convenience is kept and the costs are not: GitHub is asked ONCE, by the owner, and what persists is an ssh key | the only module that speaks to GitHub is `cli/github.py`, on the owner's machine; the host takes principals and key lines (`http/members.py`) and no token reaches it. `tests/test_topology.py::test_the_owners_token_is_spent_on_ONE_endpoint_and_written_nowhere` greps the host tree AND the checkout with a positive control, and `…::test_no_flag_on_join_takes_a_token_and_none_ever_will` holds the parser to "never a flag value". There is one `members.enroll` row in `core/scope.py` — a ROLE rule — and no second credential type anywhere |
|
|
850
851
|
| a GitHub door on the DEV's side — `POST /<board>/join/github`, a `--github` flag, a token discovered at join time (2026-08-11) | it made every dev's own credential travel to a host that has no business seeing one, to prove a fact the OWNER already holds; and it asked for an ssh key from people who, having push over ssh, had published one already. Deleted the day after it shipped — §19.1 | there is no route, no flag and no client function: `grep -r 'join/github\|by_github' src tests` is empty, and `test_no_flag_on_join_takes_a_token_and_none_ever_will` asserts the ABSENCE from `join --help`. The replacement is one command on the owner's side and `taskops join`, bare, on the dev's (`…::test_the_dev_whose_key_the_sync_published_joins_with_two_words`) |
|
|
@@ -1525,9 +1526,16 @@ below — and comes back only standing on a real history: the forge mirror, in
|
|
|
1525
1526
|
That cascade is ONE function — `ui/src/links.tsx::cascade`, beside the slug and
|
|
1526
1527
|
the link templates it already owned — and `ui/src/components/card/Patch.tsx`
|
|
1527
1528
|
only DRAWS the step it is handed, so no component holds a fallback order of its
|
|
1528
|
-
own. It feeds the dossier's **Files changed** (the card as a PR
|
|
1529
|
-
|
|
1530
|
-
|
|
1529
|
+
own. It feeds the dossier's **Files changed** (the card as a PR, over the range
|
|
1530
|
+
the BOARD recorded rather than two branch names — `links.tsx::cardRange`: one
|
|
1531
|
+
recorded commit goes through `commit/<sha>`, which the door diffs against its
|
|
1532
|
+
first parent, and several become `compare/<first>^...<last>`, whose base is the
|
|
1533
|
+
oldest commit's PARENT so that commit's own changes are inside the range. Both
|
|
1534
|
+
ends are durable, which is what makes the pane render for a card whose `tk-*`
|
|
1535
|
+
and `ms/*` branches were pruned before this host became the remote — the head
|
|
1536
|
+
alone was not enough, 0.5.2 shipped that and the pane stayed empty. Then the
|
|
1537
|
+
file list from `stat`, each file's patch on expand via `?path=`) and the fold on
|
|
1538
|
+
every commit row. Availability is
|
|
1531
1539
|
DISCOVERED, not configured: the first refusal whose words are
|
|
1532
1540
|
`gitdoor.py::NO_REPO` flips a module-level flag for the session and nothing asks
|
|
1533
1541
|
again — an unknown ref does not flip it, because that means "ask again for
|
|
@@ -1541,6 +1549,10 @@ effect firing, and that half is covered against a real server in
|
|
|
1541
1549
|
chapter): `pages/WorktreeDiff.tsx`, the full-width page a Worktrees row opens,
|
|
1542
1550
|
hands its whole range `<milestone branch>...tk-<id>` to the very same
|
|
1543
1551
|
`FilesChanged` — plus a summary bar, which is a prop on it and not a second ask.
|
|
1552
|
+
Two live BRANCHES there, deliberately, where the dossier asks by sha: that page's
|
|
1553
|
+
subject is a working tree that exists on disk right now, so where its two tips
|
|
1554
|
+
stand IS the question. `FilesChanged` therefore takes a whole `GitTarget` rather
|
|
1555
|
+
than a base/head pair — a pair could only ever express the second question.
|
|
1544
1556
|
Nothing else moved to do that: **no verb, no stored key, no change to the door**,
|
|
1545
1557
|
and no component fetches a patch outside `cascade()`. The one seam this cost is
|
|
1546
1558
|
`FileList` exported beside `FilesChanged` — the pure half, given a step — for
|
|
@@ -1722,6 +1734,14 @@ A repo joined to nothing behaves exactly as it did.
|
|
|
1722
1734
|
|
|
1723
1735
|
### The hosted window: the host may hold a MIRROR of the declared forge (decided 2026-08-30)
|
|
1724
1736
|
|
|
1737
|
+
*Reversed one day later — "The host becomes the remote", below. The direction
|
|
1738
|
+
of the git flow inverts: the host holds the board's OWN repo (`repo.git`),
|
|
1739
|
+
takes pushes from enrolled principals, and mirrors OUTWARD to the forge;
|
|
1740
|
+
`mirror.git` retires. This section stays as the argument it was, because what
|
|
1741
|
+
it got right (a hosted window must stand on a real history; the switch is
|
|
1742
|
+
decided at mount time; the credential is a deploy key, never a token)
|
|
1743
|
+
survives the reversal, and what it got wrong is named there.*
|
|
1744
|
+
|
|
1725
1745
|
The first amendment narrowed "no server reads a repo" to "a host that sits in a
|
|
1726
1746
|
repo may read it". This one narrows the other clause — *the server deliberately
|
|
1727
1747
|
has no clone* — and, as before, the change is recorded here rather than quietly
|
|
@@ -1918,6 +1938,251 @@ sh smoke.sh # taskops.bernardocastro.dev, task
|
|
|
1918
1938
|
sh smoke.sh <host> <board> <private-board>
|
|
1919
1939
|
```
|
|
1920
1940
|
|
|
1941
|
+
### The host becomes the remote (decided 2026-08-31 — reverses the mirror)
|
|
1942
|
+
|
|
1943
|
+
This section has been narrowed twice and each time the change was recorded
|
|
1944
|
+
rather than quietly made. This amendment is larger: it REVERSES a ban rather
|
|
1945
|
+
than narrowing one, and the words it reverses are one day old. "The mirror
|
|
1946
|
+
takes no client pushes — the host still exposes no receive door of any kind"
|
|
1947
|
+
stops being true. From this chapter on:
|
|
1948
|
+
|
|
1949
|
+
> **The board's host holds the board's git, as a real remote. A checkout's
|
|
1950
|
+
> `origin` IS the host: `git push` lands there, `git clone`/`fetch` reads from
|
|
1951
|
+
> there, and the declared forge becomes an OUTBOUND mirror the host pushes to,
|
|
1952
|
+
> best effort. The host never prunes a card branch.**
|
|
1953
|
+
|
|
1954
|
+
**Be fair to the ban before reversing it.** "Client pushes to the host" was
|
|
1955
|
+
banned when the host had no git at all — a directory of boards, `subprocess`
|
|
1956
|
+
nowhere near it — and the failure it guarded against was real: two clones
|
|
1957
|
+
replicating git state between themselves is split-brain with a merge driver,
|
|
1958
|
+
and a host that accepted pushes with no identity story would have been an
|
|
1959
|
+
anonymous write with a packfile attached. Both hazards are still hazards.
|
|
1960
|
+
What changed is not the hazards; it is that the host now HAS an identity
|
|
1961
|
+
story every door already checks (§19), and that the topology proposed here is
|
|
1962
|
+
not replication between clones at all — it is a hub, one direction on each
|
|
1963
|
+
leg. Writing the ban down was right. What the day-old design got wrong is
|
|
1964
|
+
narrower and worth naming exactly.
|
|
1965
|
+
|
|
1966
|
+
**What the pull-mirror got wrong.** The mirror's one source was the forge, so
|
|
1967
|
+
the board's view of the code became a FUNCTION OF SOMEBODY ELSE'S BRANCH
|
|
1968
|
+
HYGIENE — three failures, all observed live against the hosted window on
|
|
1969
|
+
2026-08-31, the day after it shipped:
|
|
1970
|
+
|
|
1971
|
+
- a worktree branch that was never pushed **does not exist** for the host, so
|
|
1972
|
+
a card being worked on had no diff at its URL until its close pushed it;
|
|
1973
|
+
- a `tk-*` branch pruned on the forge when its chapter landed **disappeared
|
|
1974
|
+
from the board**, even though the work is intact and reachable from master.
|
|
1975
|
+
Verified against production: `git ls-remote origin` had neither `tk-dfaff7`
|
|
1976
|
+
nor `ms/parallel-by-mechanism-the-board-`, yet both cards' work is in the
|
|
1977
|
+
trunk. The stale sentence called that "normal, not a fault" — true of the
|
|
1978
|
+
MIRROR, and exactly the fault of the DESIGN: a landed card's diff is the
|
|
1979
|
+
board's own record and it had been made erasable by a forge-side `git push
|
|
1980
|
+
--delete` the board never sees;
|
|
1981
|
+
- and structurally, the host's git was a copy of a copy: worktree → GitHub →
|
|
1982
|
+
mirror, with GitHub — the viewer — sitting in the middle of the board's own
|
|
1983
|
+
data path.
|
|
1984
|
+
|
|
1985
|
+
The original ask, restated by Berna the same day, was the other topology all
|
|
1986
|
+
along: the taskops server keeps the history and mirrors it ONWARD to GitHub.
|
|
1987
|
+
The cost of adopting it now is small by construction: `gitwork/bind.py`
|
|
1988
|
+
already pushes `origin <tk-…>` on post-commit, best effort, never a gate — so
|
|
1989
|
+
for a joined checkout the only thing that changes is WHICH remote `origin`
|
|
1990
|
+
is. The lifecycle pushes in `gitwork/remote.py::push` keep their contract and
|
|
1991
|
+
their ten seconds; they just stop travelling through a third party.
|
|
1992
|
+
|
|
1993
|
+
The decisions the rest of the chapter follows, each argued:
|
|
1994
|
+
|
|
1995
|
+
**Transport: git smart-HTTP over the existing door, not ssh.** The relay in
|
|
1996
|
+
front of this host (§17) exposes no port but HTTPS, so ssh transport would
|
|
1997
|
+
begin by re-plumbing the deployment. It would also mean a SECOND auth system
|
|
1998
|
+
— an sshd, an `authorized_keys`, a key-to-principal mapping — beside the one
|
|
1999
|
+
the board already has, for keys that are already enrolled on the host and
|
|
2000
|
+
already end in a bearer session every door checks (§19: one introduction, one
|
|
2001
|
+
credential). Git speaks smart-HTTP natively (`info/refs?service=…`, then
|
|
2002
|
+
`git-upload-pack` / `git-receive-pack` POSTs), authenticates it with HTTP
|
|
2003
|
+
Basic, and a session token rides in that password field with no client-side
|
|
2004
|
+
tooling at all. ssh becomes worth revisiting only if BOTH of these turn true:
|
|
2005
|
+
a deployment that fronts raw TCP (no relay), and clients that cannot hold a
|
|
2006
|
+
session token. Neither exists, and the second is unlikely ever to — `taskops
|
|
2007
|
+
join` mints the session that signs everything else.
|
|
2008
|
+
|
|
2009
|
+
**The address: `/<board>/repo.git`.** A git client is pointed at
|
|
2010
|
+
`https://<host>/<board>/repo.git` and everything git asks for lives under it:
|
|
2011
|
+
`/<board>/repo.git/info/refs?service=git-upload-pack`,
|
|
2012
|
+
`/<board>/repo.git/git-upload-pack`, `/<board>/repo.git/git-receive-pack`.
|
|
2013
|
+
It cannot collide with anything the router already serves, and each half of
|
|
2014
|
+
that is a fact about an existing closed set, not a hope: the JSON diff door
|
|
2015
|
+
is the tail `git/…` (and `api/git/…` — `http/routes.py::split` strips the
|
|
2016
|
+
prefix once, and `repo.git` does not start with `api/` so the strip never
|
|
2017
|
+
touches it), while `repo.git` is its own first segment; and the page's assets
|
|
2018
|
+
are `static.py::asset`'s closed set — flat filenames whose suffix is in
|
|
2019
|
+
`TYPES`, and `.git` is not in `TYPES`, so no bundle that ever ships can
|
|
2020
|
+
shadow the repo and the repo can never shadow an asset. The `.git` suffix is
|
|
2021
|
+
also the convention every forge trained every tool on, which is why the name
|
|
2022
|
+
is not `code` or `src`: an address a person guesses should be the one that
|
|
2023
|
+
works.
|
|
2024
|
+
|
|
2025
|
+
**On-disk: a NEW bare repo, `<root>/<board>/repo.git`, and `mirror.git`
|
|
2026
|
+
retires.** The mirror does not get promoted, for two reasons written into its
|
|
2027
|
+
own contract. It is DERIVED and disposable — "delete it and it re-clones" —
|
|
2028
|
+
and the board's real remote is the opposite thing: truth the forge does not
|
|
2029
|
+
necessarily hold, deletable by nobody. And its refspec is `--mirror`, whose
|
|
2030
|
+
fetch semantics are *make local match the forge, prunes included* — the exact
|
|
2031
|
+
behaviour this chapter exists to kill would be sitting in the repo's own
|
|
2032
|
+
config, one habitual `git fetch` away. A truth-holder configured to erase
|
|
2033
|
+
itself is not a truth-holder. So: a fresh `git init --bare repo.git` beside
|
|
2034
|
+
`events.jsonl`, and on a host that already has a populated `mirror.git`
|
|
2035
|
+
(production does, today), `repo.git` is SEEDED from it — one local
|
|
2036
|
+
`git clone --bare mirror.git repo.git`, on-disk and cheap — so no history the
|
|
2037
|
+
mirror held is lost, and the mirror-mode config dies with the old directory,
|
|
2038
|
+
which is then removed. Wired as `gitwork/bare.py::adopt` and called by
|
|
2039
|
+
`http/repos.py`, the first door that needs an answer: it is the ONE read path
|
|
2040
|
+
in the codebase allowed to write, argued there — a migration of a history the
|
|
2041
|
+
host already possesses is not a directory conjured by a stranger's question,
|
|
2042
|
+
and a board holding neither repo still leaves the disk untouched. `mirror.py`
|
|
2043
|
+
is DELETED in the same card rather than left as a second source; a fallback
|
|
2044
|
+
that could only ever answer for refs the FORGE has is wrong in exactly the case
|
|
2045
|
+
the reader asks about (a branch nobody pushed, a branch the forge pruned), and
|
|
2046
|
+
`http/repos.py` carries that argument. `http/repos.py` points `/git` and the
|
|
2047
|
+
hosted window at `repo.git` from then on: the window's diffs are read from the board's OWN
|
|
2048
|
+
repository, with no dependency on the forge — which is the acceptance the
|
|
2049
|
+
chapter is held to.
|
|
2050
|
+
|
|
2051
|
+
**Roles: a push is a write, and §11's anonymous-write ban covers it.** Only
|
|
2052
|
+
an enrolled principal may push — `dev:<name>` and `agent:<dev>/<name>` alike,
|
|
2053
|
+
because workers are exactly who `bind.py` pushes as, on every commit. An
|
|
2054
|
+
anonymous caller and a read-only credential are refused at the receive door
|
|
2055
|
+
with the sentence that names `taskops join`. A read — clone, fetch,
|
|
2056
|
+
`ls-remote` — follows the board's VISIBILITY exactly as `/rpc` does: a public
|
|
2057
|
+
board clones anonymously (and, per §11's invisible-write ban, that read
|
|
2058
|
+
leaves no `presence` row), a private board clones with the credential the
|
|
2059
|
+
doors already check. One rule, already written, now asked by one more door.
|
|
2060
|
+
|
|
2061
|
+
**The host never prunes, and never takes a force.** A card branch pushed to
|
|
2062
|
+
the host is readable at its URL forever — that permanence is the point of the
|
|
2063
|
+
reversal, so it is enforced, not hoped: the receive door refuses a ref
|
|
2064
|
+
deletion and a non-fast-forward push outright. There is no flag that opens
|
|
2065
|
+
either, for `board rm`'s reason (§11): `--force` is a word that names no
|
|
2066
|
+
consequence, and what it would destroy here is the diff of a landed card —
|
|
2067
|
+
the board's own record. History rewriting, when it is ever needed, is the
|
|
2068
|
+
owner's deliberate act against the host's filesystem, not a verb.
|
|
2069
|
+
|
|
2070
|
+
**The outbound mirror: best effort, never a gate, failure visible.** The
|
|
2071
|
+
forge relationship inverts, it does not end: a board that declared one
|
|
2072
|
+
(`taskops board forge <owner>/<repo>`) gets its history pushed ONWARD, from
|
|
2073
|
+
`repo.git` to the forge, at the same lifecycle moments `remote.py::push`
|
|
2074
|
+
already owns — and a forge that is down MUST NOT fail the client's push,
|
|
2075
|
+
which lands on the host and is done. Best effort never means silent: a mirror
|
|
2076
|
+
push that fails is a FACT a reader can see — surfaced where the board already
|
|
2077
|
+
surfaces derived facts, on the payload, never swallowed into a log line —
|
|
2078
|
+
because an invisible best-effort is how the forge quietly drifts a month
|
|
2079
|
+
behind. The credential is an ssh deploy key on the host's filesystem with
|
|
2080
|
+
WRITE on the forge — and that is a real escalation over the mirror's
|
|
2081
|
+
read-only key, so it is named as one: minted and installed by the OWNER, an
|
|
2082
|
+
explicit act, revocable on the forge like any deploy key, and scoped to that
|
|
2083
|
+
one repo. §19.2 carries the credential argument. What it is NOT: a GitHub
|
|
2084
|
+
token (the key opens one repo, not an account), and never anything a dev
|
|
2085
|
+
supplies.
|
|
2086
|
+
|
|
2087
|
+
Wired as `gitwork/onward.py`, started by the receive door once git's own exit
|
|
2088
|
+
code says the push landed (`http/gitpack.py`) and running on a background
|
|
2089
|
+
thread — best effort in TIME as well as in outcome, since inline the client
|
|
2090
|
+
would feel the mirror as a ten-second gate on a push that already succeeded.
|
|
2091
|
+
Two pushes racing need no lock: the refspec is the whole `refs/heads/*`, so a
|
|
2092
|
+
later push subsumes an earlier one; only the REPORT is ordered, by timestamp
|
|
2093
|
+
(`store/mirroring.py`). The report is a row in `live.sqlite` — alive, never an
|
|
2094
|
+
event, because "could this host reach GitHub" is false the moment the log
|
|
2095
|
+
travels — and it rides on the board payload as `mirror` (`verbs/pulse.py`,
|
|
2096
|
+
under `forge`'s own contract: nothing to say sends no key), drawn as one
|
|
2097
|
+
MIRROR line by `mcp/boardview.py`, success included, so "silence" can never
|
|
2098
|
+
mean both "up to date" and "the key expired a month ago". The outbound remote
|
|
2099
|
+
is one named `forge` inside `repo.git`, added by the owner by hand — the ssh
|
|
2100
|
+
address IS the credential story, and a forge declared with no remote reads as
|
|
2101
|
+
a failure naming that exact command.
|
|
2102
|
+
|
|
2103
|
+
**A board with NO declared forge gets everything except the outbound
|
|
2104
|
+
mirror — and that is not a fault.** Push, clone, the hosted window's diffs,
|
|
2105
|
+
the permanence: all of it stands on `repo.git` alone. The forge was the
|
|
2106
|
+
SOURCE under the pull-mirror, so a board without one had no hosted window at
|
|
2107
|
+
all; under this topology the forge is a projection, and a board that wants no
|
|
2108
|
+
projection is simply complete. `NO_FORGE`'s refusal retires with the mirror
|
|
2109
|
+
it described — into `gitdoor.NO_REPO`, which now says the true board-level
|
|
2110
|
+
fact ("nobody has pushed this board's code here yet") and names the two moves
|
|
2111
|
+
that end it. The HOSTED WINDOW's gate moves the same way: `repos.backed` asks
|
|
2112
|
+
for `repo.git`, not for a forge, so `static.NO_UI` stops promising a page in
|
|
2113
|
+
exchange for `taskops board forge` — and the missing-ref sentence's AUDIENCE
|
|
2114
|
+
split (tk-9cde88) outlives the mirror that prompted it, the host's half
|
|
2115
|
+
rewritten as `stale.HOSTED`: this host prunes nothing, so a ref absent here was
|
|
2116
|
+
never pushed here. The window's half is unchanged, byte for byte.
|
|
2117
|
+
|
|
2118
|
+
**Beside §20, not inside it.** `board push`/`board pull` move the BOARD — the
|
|
2119
|
+
event log, the one history nobody can regenerate — and this chapter moves the
|
|
2120
|
+
CODE. The two lifecycles share a host and nothing else: `board push` still
|
|
2121
|
+
promotes a history exactly once onto an empty target, `board pull` is still a
|
|
2122
|
+
snapshot, `board rm` still guards on possession — and `board rm` now names
|
|
2123
|
+
one more thing it destroys, the board's `repo.git`, judged by the same
|
|
2124
|
+
possession logic: a history this checkout holds (the clone IS possession of
|
|
2125
|
+
the git half) is deletable, one it does not hold is refused.
|
|
2126
|
+
|
|
2127
|
+
**What stays banned, spelled out, because a reversal invites the question:**
|
|
2128
|
+
|
|
2129
|
+
- **replication BETWEEN clones.** This is a hub — worktree → host, host →
|
|
2130
|
+
forge, one direction on each leg. No clone pushes to another clone, no
|
|
2131
|
+
clone pulls the board's state from anywhere but the host, and the host
|
|
2132
|
+
never pulls git from the forge again.
|
|
2133
|
+
- **a stored GitHub token, and GitHub as a second credential type.** §19,
|
|
2134
|
+
unmoved. The outbound key is a deploy key: one repo, revocable, the
|
|
2135
|
+
owner's.
|
|
2136
|
+
- **anonymous writes, in any form — a git push included.** The receive door
|
|
2137
|
+
is behind the same credential wall as every write verb.
|
|
2138
|
+
- **a hand-rolled git protocol.** The pack protocol is git's, spoken by
|
|
2139
|
+
git's own plumbing through the ONE subprocess module (`gitwork/run.py`),
|
|
2140
|
+
the way SSHSIG verification is OpenSSH's (§11). A Python reimplementation
|
|
2141
|
+
of pkt-line is the crypto own-goal with a different file extension.
|
|
2142
|
+
- **a `--force` or a prune that could erase a card branch on the host.**
|
|
2143
|
+
Argued above; it is the reversal's own reason turned into a rule.
|
|
2144
|
+
|
|
2145
|
+
### The dev's side: `taskops remote git`, and no `origin` (2026-08-31)
|
|
2146
|
+
|
|
2147
|
+
The reversal gave the host a git door and left the client unable to NAME it:
|
|
2148
|
+
`https://<host>/<board>/repo.git` existed only inside `http/gitpack.py`, so
|
|
2149
|
+
pointing a checkout at it meant reading the source. `taskops remote git` is
|
|
2150
|
+
that sentence — printed to paste, or `--add` to write it here — and it is a
|
|
2151
|
+
third action on `taskops remote` rather than a twelfth top-level command,
|
|
2152
|
+
because recording where the board's git lives is the same act as recording
|
|
2153
|
+
where the board lives (`cli/gitremote.py`).
|
|
2154
|
+
|
|
2155
|
+
**`origin` is never written, and `--name origin` is refused outright.** Not
|
|
2156
|
+
"left alone if it exists": refused. A checkout's `origin` is somebody's own
|
|
2157
|
+
setup — very often their GitHub — and no state of it is this command's to
|
|
2158
|
+
decide, its absence included. The remote is `taskops`; a name already in use
|
|
2159
|
+
is a refusal that names `--name <other>`, never a `set-url`. Adding a remote
|
|
2160
|
+
is CONNECTING (the CLI's promise); repointing one is managing.
|
|
2161
|
+
|
|
2162
|
+
**The credential is a HELPER, never a URL.** `https://x:<token>@host/…` is the
|
|
2163
|
+
obvious spelling and it is banned here for two reasons, either sufficient:
|
|
2164
|
+
`git remote add` persists its URL in `.git/config`, plaintext, so a live
|
|
2165
|
+
session token would sit in the one file nobody thinks to look at; and a
|
|
2166
|
+
taskops session EXPIRES, so a token baked into a config is wrong within the
|
|
2167
|
+
hour and cannot renew itself. What gets configured is
|
|
2168
|
+
`credential.<host>.helper`, pointing at `taskops hook credential` — git asks
|
|
2169
|
+
for a password at push time, the helper mints or renews a session from the ssh
|
|
2170
|
+
key already on disk (`identity.establish`, the same entry every board verb
|
|
2171
|
+
uses) and hands it over on a pipe. `store` and `erase` are answered with
|
|
2172
|
+
silence: a helper that caches is a helper that persists a token, and minting is
|
|
2173
|
+
free. One credential story, §19's, reaching git through git's own door — and
|
|
2174
|
+
`allowed_signers` remains the only thing the host keeps.
|
|
2175
|
+
|
|
2176
|
+
**The address is NOT a board-payload field, and that is the argued half.** The
|
|
2177
|
+
payload carries the forge and the mirror fact, so `repo_url` looks like it
|
|
2178
|
+
belongs beside them. It does not: the server cannot know its own public
|
|
2179
|
+
address — a proxy, a port-forward, the local `taskops ui` window, and every
|
|
2180
|
+
reader already reached it at an address the server never saw — so a stored one
|
|
2181
|
+
is the first field to rot and the first to send a dev to the wrong host. The
|
|
2182
|
+
address is (the host you asked) + `/<board>/repo.git`, a derivation the CLIENT
|
|
2183
|
+
can always do correctly and the server never can. `board ls` prints that shape
|
|
2184
|
+
once, and names the command that spells it exactly.
|
|
2185
|
+
|
|
1921
2186
|
---
|
|
1922
2187
|
|
|
1923
2188
|
## 17. Deployed — taskops.bernardocastro.dev is v2 (2026-08-08)
|
|
@@ -2473,6 +2738,36 @@ which `as_object` flattens to `{}`. A page of people silently reading as nobody
|
|
|
2473
2738
|
is the bug that shape prevents, so the decoding belongs to whoever knows what it
|
|
2474
2739
|
asked for and only the status and `Unreachable` are shared.
|
|
2475
2740
|
|
|
2741
|
+
### 19.2 The mirror's key gains WRITE — the owner's explicit act (2026-08-31)
|
|
2742
|
+
|
|
2743
|
+
The hosted-window chapter (§16, 2026-08-30) added exactly one credential to
|
|
2744
|
+
this story: an ssh deploy key on the host's filesystem, READ-ONLY, so the
|
|
2745
|
+
host could pull a private repo's mirror. The reversal (§16, "The host becomes
|
|
2746
|
+
the remote") inverts the flow, and the key follows: the host now PUSHES to
|
|
2747
|
+
the declared forge, so the deploy key needs write on that one repo. That is a
|
|
2748
|
+
real escalation and it is treated as one, not smuggled in as a config detail:
|
|
2749
|
+
|
|
2750
|
+
- **It is the owner's explicit act, twice over.** Declaring the forge was
|
|
2751
|
+
already the owner's move (`taskops board forge`); minting a WRITE deploy
|
|
2752
|
+
key and installing it on the host is a second, separate decision the owner
|
|
2753
|
+
makes with eyes open, because it hands the host the power to move branches
|
|
2754
|
+
on the forge. Nothing mints it, discovers it, or asks a dev for it.
|
|
2755
|
+
- **It is still not a token, and the difference still matters.** A deploy key
|
|
2756
|
+
opens ONE repo; a GitHub token opens an account. If the host is ever
|
|
2757
|
+
compromised, the blast radius is the mirror of a repo whose real history
|
|
2758
|
+
the host itself holds — revoke the key on the forge and the leg is dead,
|
|
2759
|
+
the board untouched. Everything §19 bans stays banned: no stored GitHub
|
|
2760
|
+
token, no dev credential travelling, no second identity system.
|
|
2761
|
+
- **Write on the forge, never force.** The outbound push is the same
|
|
2762
|
+
best-effort, fast-forward-only discipline the host demands of its own
|
|
2763
|
+
receive door: the mirror leg cannot delete or rewrite on the forge either,
|
|
2764
|
+
so a key with write is not a key that erases.
|
|
2765
|
+
|
|
2766
|
+
A board whose owner declines the escalation keeps the read side whole: the
|
|
2767
|
+
host serves clones, pushes, and the hosted window from `repo.git`, and the
|
|
2768
|
+
forge simply goes unmirrored — visible as the mirror-push fact the payload
|
|
2769
|
+
carries, never as a fault.
|
|
2770
|
+
|
|
2476
2771
|
## 20. A board's whole life — create → push → (live) → pull → rm (2026-08-10)
|
|
2477
2772
|
|
|
2478
2773
|
Until this chapter a board's life ran one way. `board create` made one, `board
|
|
@@ -3,6 +3,83 @@
|
|
|
3
3
|
The source of truth for release notes — GitHub Releases are extracted from
|
|
4
4
|
here, never written twice.
|
|
5
5
|
|
|
6
|
+
## 0.5.3 — the Files-changed pane renders for a landed card
|
|
7
|
+
|
|
8
|
+
- **The pane asked for two branch names, and a landed card has neither.**
|
|
9
|
+
0.5.2 made the HEAD durable — the card's newest recorded sha instead of its
|
|
10
|
+
branch — and stopped there. The BASE stayed the chapter's branch name, and
|
|
11
|
+
for every card that landed before this host became the board's remote that
|
|
12
|
+
branch was pruned on the forge, so the compare refused and the pane was
|
|
13
|
+
empty while the commits sat in the trunk. Reported live on tk-bffa26.
|
|
14
|
+
- **The range now comes from the card's own commits, both ends**
|
|
15
|
+
(`ui/src/links.tsx::cardRange`): one recorded commit goes through the commit
|
|
16
|
+
door, which already diffs a commit against its first parent; several become
|
|
17
|
+
`compare/<oldest>^...<newest>`. The base is the oldest commit's PARENT, not
|
|
18
|
+
the oldest commit — `git diff` is exclusive of its base, so `<oldest>...<newest>`
|
|
19
|
+
answers 200 with that commit's own changes silently missing, which is a pane
|
|
20
|
+
that looks complete and is not. No server change was needed: `<sha>^` already
|
|
21
|
+
passed the ref shape and resolves through `^{commit}`.
|
|
22
|
+
- **Pinned where it broke.** The pane renders the route it asked for, and a
|
|
23
|
+
smoke section reads that route off the rendered component — the previous
|
|
24
|
+
attempt was pinned one layer below the call site, so a call-site mutation
|
|
25
|
+
went green while the pane stayed broken. `smoke.sh` now derives the same
|
|
26
|
+
route from the LIVE card payload and asserts the first commit's files are in
|
|
27
|
+
the answer.
|
|
28
|
+
- **"so them was never pushed here"** — the host's missing-ref refusal had a
|
|
29
|
+
plural substitution in a subject slot. The number now runs through the whole
|
|
30
|
+
sentence, and both forms are pinned as bytes.
|
|
31
|
+
|
|
32
|
+
## 0.5.2 — the host holds the git; GitHub is the copy
|
|
33
|
+
|
|
34
|
+
0.5.0 and 0.5.1 built the hosted window on a mirror the host PULLED from the
|
|
35
|
+
declared forge. That was the wrong direction, and the reader felt it: a
|
|
36
|
+
worktree branch nobody had pushed did not exist for the host, and a `tk-*`
|
|
37
|
+
branch pruned when its chapter landed erased a finished card's diff even
|
|
38
|
+
though the commits were sitting in the trunk. The board's view of the code was
|
|
39
|
+
a function of somebody else's branch hygiene. This release inverts it.
|
|
40
|
+
|
|
41
|
+
- **The host IS the board's git remote.** `<root>/<board>/repo.git`, served
|
|
42
|
+
over smart-HTTP at `https://<host>/<board>/repo.git`: clone, fetch and push.
|
|
43
|
+
A worktree's card branch reaches it at the moment of the commit — the
|
|
44
|
+
post-commit hook already pushed `origin <tk-…>`, so for a joined checkout the
|
|
45
|
+
only thing that changes is which remote that is.
|
|
46
|
+
- **It never prunes.** `receive.denyDeletes` and `receive.denyNonFastForwards`
|
|
47
|
+
live in the repo's own config, so the refusal comes from the very
|
|
48
|
+
`receive-pack` process that would move the ref: no window between an
|
|
49
|
+
application check and the update, no hook script a later `git init` can drop,
|
|
50
|
+
and a client's `--force` changes nothing. A landed card's diff is readable at
|
|
51
|
+
its URL forever.
|
|
52
|
+
- **GitHub becomes the outbound copy.** After a push lands, the host pushes
|
|
53
|
+
onward to the declared forge on a background thread — best effort in outcome
|
|
54
|
+
AND in time, since inline a hanging forge would make the mirror felt as a
|
|
55
|
+
ten-second gate on a push that already succeeded. Fast-forward only: no
|
|
56
|
+
`--mirror` (it deletes), no `--force`, no `--prune`. The host does not do to
|
|
57
|
+
the forge what it refuses for itself.
|
|
58
|
+
- **A mirror failure is never silent.** Success and failure both land on the
|
|
59
|
+
board payload as `mirror` and on the orchestrator's board view — "up to date,
|
|
60
|
+
pushed 2m ago" or "FAILED just now: <git's own words>" — kept in
|
|
61
|
+
`live.sqlite` rather than the event log, because "this host reached GitHub ten
|
|
62
|
+
seconds ago" replayed on another machine is a lie.
|
|
63
|
+
- **One credential, still.** A push authenticates as the enrolled principal the
|
|
64
|
+
board already knows: git's HTTP Basic carries a session in the password
|
|
65
|
+
field, minted at push time from the dev's ssh key by a credential helper
|
|
66
|
+
(`taskops hook credential`), so no token is ever written into `.git/config`.
|
|
67
|
+
The receive door answers 401 with `WWW-Authenticate: Basic`, because git only
|
|
68
|
+
volunteers a credential after a challenge. Reads follow the board's
|
|
69
|
+
visibility exactly as `/rpc` does.
|
|
70
|
+
- **`taskops remote git [--add]`** wires a checkout to the board's git and
|
|
71
|
+
prints what it did. It refuses `--name origin` outright: repointing somebody's
|
|
72
|
+
own remote is not connecting, it is rewriting. `taskops join` does not do it
|
|
73
|
+
for you — it is the one act that could silently change where a push goes.
|
|
74
|
+
- **The pull mirror is deleted**, not kept as a fallback: it could only ever
|
|
75
|
+
answer for refs the forge has, which is wrong in exactly the case a reader
|
|
76
|
+
asks about. A host that already holds a populated `mirror.git` migrates it
|
|
77
|
+
once — `git clone --bare` into `repo.git`, then the mirror is removed.
|
|
78
|
+
- **The owner's two acts**, if they want GitHub kept in sync: a WRITE deploy key
|
|
79
|
+
on the host, and `git -C <root>/<board>/repo.git remote add forge
|
|
80
|
+
git@github.com:<owner>/<repo>.git`. Until then every board read says the
|
|
81
|
+
mirror is not configured. Still no stored GitHub token, anywhere.
|
|
82
|
+
|
|
6
83
|
## 0.5.1 — the hosted window, actually usable
|
|
7
84
|
|
|
8
85
|
0.5.0 shipped a window nobody could use. Both faults were reported the next
|
|
@@ -110,6 +110,46 @@ payload, derived per read exactly as `visibility` is, so a reader finds the door
|
|
|
110
110
|
instead of bumping into it. **A board with no forge sends no key at all** —
|
|
111
111
|
never `null`.
|
|
112
112
|
|
|
113
|
+
**The HOST holds the git; GitHub is the copy** (2026-08-31, ARCHITECTURE §16
|
|
114
|
+
"The host becomes the remote", which REVERSES the one-day-old pull mirror — read
|
|
115
|
+
that section before touching any of it). One hub, one direction per leg:
|
|
116
|
+
worktree → host → forge.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
your worktree ──push──▶ <root>/<board>/repo.git ──best effort──▶ the declared forge
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- The door is git smart-HTTP at `/<board>/repo.git/…` (`http/gitpack.py`), spoken
|
|
123
|
+
by git's own plumbing through the ONE subprocess module. A session token rides
|
|
124
|
+
in HTTP Basic's password field; a refusal there is a **401 with
|
|
125
|
+
`WWW-Authenticate`**, not `/rpc`'s 409, because git volunteers Basic only after
|
|
126
|
+
one.
|
|
127
|
+
- A READ follows the board's visibility (public clones anonymously, and leaves no
|
|
128
|
+
`presence` row); a PUSH is a write, so only an enrolled principal may make one.
|
|
129
|
+
- **The host never prunes and takes no force**: `receive.denyDeletes` /
|
|
130
|
+
`denyNonFastForwards` in the repo's own config, and no flag opens either. A
|
|
131
|
+
card branch is readable at its URL after the forge pruned it — that permanence
|
|
132
|
+
IS the chapter.
|
|
133
|
+
- The bare repo is created on the first push and a legacy `mirror.git` is
|
|
134
|
+
MIGRATED, not promoted (`gitwork/bare.py::adopt`; `gitwork/mirror.py` is
|
|
135
|
+
deleted). `http/repos.py` is the one read path allowed to write, argued there.
|
|
136
|
+
- The outbound leg is `gitwork/onward.py`, started by the receive door once git's
|
|
137
|
+
exit code says the push landed, on a background thread: **best effort, never a
|
|
138
|
+
gate, never silent** — the outcome is a `live.sqlite` row (`store/mirroring.py`)
|
|
139
|
+
riding on the board payload as `mirror`, drawn as one MIRROR line. Its
|
|
140
|
+
credential is a WRITE deploy key the OWNER installs on the host plus a remote
|
|
141
|
+
named `forge` inside `repo.git`, added by hand (§19.2). Still not a token.
|
|
142
|
+
- **A board with no forge gets everything except the outbound leg** — push,
|
|
143
|
+
clone, the hosted window's diffs, the permanence all stand on `repo.git`
|
|
144
|
+
alone — so `NO_FORGE` retired into `gitdoor.NO_REPO` / `static.NO_UI`, which
|
|
145
|
+
name a board nobody has pushed to yet, never a missing forge.
|
|
146
|
+
- The DEV's side is `taskops remote git [--add]` (`cli/gitremote.py`): it writes
|
|
147
|
+
a remote named `taskops` and **refuses `--name origin` outright**, and the
|
|
148
|
+
credential is a HELPER (`taskops hook credential`), never a URL — `git remote
|
|
149
|
+
add` persists its URL in `.git/config` and a session expires within the hour.
|
|
150
|
+
The address is not a board-payload field: the server cannot know its own public
|
|
151
|
+
address, so it is (the host you asked) + `/<board>/repo.git`, derived client-side.
|
|
152
|
+
|
|
113
153
|
## Layers — imports only point DOWN
|
|
114
154
|
|
|
115
155
|
```
|
|
@@ -150,6 +190,8 @@ a *library* users must not import, and taskops has no such half.
|
|
|
150
190
|
<board>/events.jsonl THE TRUTH — append + fsync BEFORE the cache
|
|
151
191
|
<board>/cache.sqlite derived, disposable (delete it, it rebuilds)
|
|
152
192
|
<board>/live.sqlite leases + presence — separate file ON PURPOSE
|
|
193
|
+
<board>/repo.git the board's OWN git, on the HOST — created by the first
|
|
194
|
+
push, never pruned, never force-pushed. Truth, not a cache
|
|
153
195
|
<root>/server.sqlite the HOST: principals + pubkeys
|
|
154
196
|
<root>/allowed_signers DERIVED from it, whole, on every change
|
|
155
197
|
```
|
|
@@ -160,16 +202,19 @@ releases. A board is created by an explicit act and never by being asked for.
|
|
|
160
202
|
|
|
161
203
|
## The CLI surface — it connects, it never manages
|
|
162
204
|
|
|
163
|
-
Eleven top-level commands
|
|
164
|
-
Moving a card from the terminal does not exist: that is MCP.
|
|
165
|
-
|
|
205
|
+
Eleven top-level commands; two of them — `board` and `remote` — have actions of
|
|
206
|
+
their own. Moving a card from the terminal does not exist: that is MCP.
|
|
207
|
+
Re-derive every list rather than trusting this paragraph — `--help` is the source:
|
|
166
208
|
|
|
167
209
|
```sh
|
|
168
210
|
uv run python -m taskops.cli --help | sed -n '/^usage/,/^$/p' # the eleven
|
|
169
211
|
uv run python -m taskops.cli board --help # its actions
|
|
212
|
+
uv run python -m taskops.cli remote --help # add · git
|
|
170
213
|
```
|
|
171
214
|
|
|
172
|
-
`board` today: `create · ls · push · pull · rm · visibility · forge
|
|
215
|
+
`board` today: `create · ls · push · pull · rm · visibility · forge`; `remote`:
|
|
216
|
+
`add` (the host this checkout operates) and `git` (the board's own `repo.git`,
|
|
217
|
+
printed or `--add`ed as a remote named `taskops`). The four that move
|
|
173
218
|
a whole history are one lifecycle, and each says what it destroys:
|
|
174
219
|
|
|
175
220
|
```
|
|
@@ -230,20 +275,25 @@ what it exists to report, not a fault.
|
|
|
230
275
|
|
|
231
276
|
**The hosted page is at the board's OWN address** — `https://<host>/<board>/`
|
|
232
277
|
(and `/<board>`), its assets beside it, the machine doors under
|
|
233
|
-
`/<board>/api/{rpc,git,feed}
|
|
278
|
+
`/<board>/api/{rpc,git,feed}` — and the board's git is its own first segment,
|
|
279
|
+
`/<board>/repo.git/…`, which can collide with neither (the `api/` strip never
|
|
280
|
+
touches it, and `.git` is not in `static.py`'s asset `TYPES`). Its diffs are read
|
|
281
|
+
from that repo, never from a forge. 0.5.0's spellings (`/<board>/ui/`,
|
|
234
282
|
`/<board>/rpc`, `/<board>/git/…`, `/<board>/feed`) still answer and are a
|
|
235
283
|
contract, not a legacy — ARCHITECTURE §16, "The board's own address IS the
|
|
236
284
|
page". The page derives its base from its own location
|
|
237
285
|
(`ui/src/client.ts::baseOf`) and hardcodes neither. `sh smoke.sh` re-derives all
|
|
238
286
|
of it against the live host, plus the credential rule a browser exposed:
|
|
239
287
|
**an `Authorization` header with no value is the ABSENCE of a credential, a
|
|
240
|
-
header with a wrong value still refuses loudly** (`http/auth.py::token_in`)
|
|
288
|
+
header with a wrong value still refuses loudly** (`http/auth.py::token_in`) —
|
|
289
|
+
and the git doors, whose anonymous push must come back `401` +
|
|
290
|
+
`WWW-Authenticate` rather than a 409.
|
|
241
291
|
|
|
242
292
|
## Never re-introduce
|
|
243
293
|
|
|
244
294
|
Each has its line in ARCHITECTURE.md §11 saying what it cost and where it is
|
|
245
295
|
enforced: a reviewer ROLE, a stored review STATUS, or automatic reviewer
|
|
246
|
-
assignment · `land` or automatic merges to the trunk · a SECOND trunk (2026-08-10: `main` and `master` both existed, `trees.base_ref` cut every chapter from `origin/main`, and a one-sided push refspec was quietly landing card merges there — three facts that only became a bug together) · git replication
|
|
296
|
+
assignment · `land` or automatic merges to the trunk · a SECOND trunk (2026-08-10: `main` and `master` both existed, `trees.base_ref` cut every chapter from `origin/main`, and a one-sided push refspec was quietly landing card merges there — three facts that only became a bug together) · git replication BETWEEN CLONES (the host-as-remote hub is not that: one direction per leg, and the host never pulls git from the forge)
|
|
247
297
|
· Claude hooks **that decide or store** · a stored `doing` · a slug in a branch
|
|
248
298
|
name · a `recover` · a mark-as-read/ack verb · per-request SIGNING · hand-rolled
|
|
249
299
|
crypto or a pip crypto dependency · a `--force` on `board push` **or on `board
|