taskops-cli 0.3.2__tar.gz → 0.3.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.3.2 → taskops_cli-0.3.3}/.github/workflows/ci.yml +1 -1
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/cache.sqlite-shm +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/cache.sqlite-wal +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/events.jsonl +12 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/live.sqlite-shm +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/live.sqlite-wal +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ARCHITECTURE.md +1 -1
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/CHANGELOG.md +10 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/CLAUDE.md +2 -2
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/PKG-INFO +63 -8
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/README.md +61 -6
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_version.py +1 -1
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/commands.py +21 -10
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_topology.py +28 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.claude/settings.json +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.gitattributes +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.github/workflows/workflow.yml +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.gitignore +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.mcp.json +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/cache.sqlite +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board/live.sqlite +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/board.json +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/reports/lifecycle-chapter.html +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/reports/reports-chapter.html +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/.taskops/ui.lock +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/pyproject.toml +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_clock.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_errors.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_ids.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_json.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_locate.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/_wire.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/board.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/__main__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/admin.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/claude.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/enrol.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/grants.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/hooks.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/main.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/operate.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/paging.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/parser.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/pull.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/push.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/remote.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/rm.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/serving.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/watch.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/window.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/cli/wording.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/actors.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/challenge.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/chapters.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/event.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/forge.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/graph.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/holding.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/hours.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/machine.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/mentions.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/replay.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/reports.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/review.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/scope.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/seams.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/core/types.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/bind.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/catchup.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/claudefiles.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/diff.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/install.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/landing.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/patch.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/remote.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/run.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/sig.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/trailer.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/gitwork/trees.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/admin.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/auth.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/feed.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/gitdoor.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/github.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/grants.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/handler.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/ingest.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/login.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/mounts.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/removal.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/rpc.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/scoped.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/server.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/static.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/upstream.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/http/watcher.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/identity.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/__main__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/activity.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/before.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/boards.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/boardview.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/brief.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/dossier.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/fields.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/gitmoves.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/hello.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/integrate.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/render.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/schema.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/server.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/thread.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/mcp/tools.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/py.typed +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/session.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/cache.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/creds.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/live.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/log.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/pubkeys.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/reviews.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/server.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/store/stores.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/ui/app.js +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/ui/index.html +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/ui/style.css +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_args.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_cards.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_context.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_facts.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_mentions.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_rows.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_stories.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/_waiting.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/activity.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/assign.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/card.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/events.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/filed.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/plan.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/project.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/pulse.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/record.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/report.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/review.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/take.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/src/taskops/verbs/update.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/__init__.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/conftest.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_architecture.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_claude.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_core.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_git.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_mcp.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_report.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_store.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_ui.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_verbs.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/test_window.py +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/tests/ui_harness.js +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/build.mjs +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/index.html +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/package-lock.json +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/package.json +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/fixture.json +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/main.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/run.mjs +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/actors.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/board-columns.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/chapter-landed.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/chapters-open.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/comment-box.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/diff-page.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/dossier.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/event-stream.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/forge-links.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/git-diff.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/monitor-panes.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/overlay-escape.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/report-markdown.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/report-sandbox.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/reviewing-skew.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/section.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/swarm-topology.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/thread-closing-note.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections/worktrees-index.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/smoke/sections.mjs +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/App.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/client.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/actors/Daysheet.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/actors/DevPanel.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/board/CardTile.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/board/Column.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/CommentBox.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Drawer.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Patch.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Sections.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/Thread.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/split.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/card/tokens.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/AvatarStack.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/Header.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/KpiRail.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/MilestonePicker.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/chrome/TabNav.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Chapter.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/DependencyChain.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/EditSurface.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/EventStream.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/LeaseHealth.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/LiveLeases.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Mentions.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Pane.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Swarm.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/Throughput.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/monitor/panels.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/reports/ReportFrame.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/shared/Markdown.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/shared/Overlay.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/components/shared/overlayStack.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/format.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/links.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/main.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/markdown.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Actors.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Board.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Monitor.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Reports.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/WorktreeDiff.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/pages/Worktrees.tsx +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/theme/theme.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/theme/tokens.css +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/types.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/useBoard.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/src/useEvents.ts +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/styles/index.css +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/ui/tsconfig.json +0 -0
- {taskops_cli-0.3.2 → taskops_cli-0.3.3}/uv.lock +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -780,3 +780,15 @@
|
|
|
780
780
|
{"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/jp1"},"id":"d74b22b355d8797f6fc451e81aa105bc","kind":"edited","task":"tk-d1ff9b","ts":1786393796.008371}
|
|
781
781
|
{"actor":"dev:berna","body":{"text":"STOP — the ground moved under this card. I killed jp1 mid-work on purpose, because it was about to write an onboarding README naming a host the axion board no longer lives on.\n\nThe axion board MOVED, tonight, at Berna's request (the home box was too slow):\n\n from https://taskops.bernardocastro.dev/axion (nginx on the GCP relay → proxy_pass 10.8.0.2 → LXD container on the home box)\n to https://boards.t.bernardocastro.dev/axion (taskops 0.3.1 SERVED BY the GCP relay itself, systemd unit `taskops`, 127.0.0.1:8787 behind nginx)\n\nMeasured, which is why it moved: /healthz is 0.36s on the new host against 0.77s on the old one — the relay→box hop was 178ms each way.\n\nHow it moved, and what survives:\n- `board pull axion` brought 962 events down (\"all 962 event(s) are held here\"), then `board create` + `board push axion` on the new host: \"verified: 962 landed, seq 963\".\n- The OLD host still holds its copy — a pull destroys nothing. Nobody has removed it, and that is Berna's call, not this card's.\n- jp1's work was NOT wasted: it declared the forge before it was stopped (event 925540b6…, ts 1786393854, `{op: forge, repo: cloudacio/Axion, need: push}`), and that event travelled with the 962. The GitHub door is live on the new host — `POST /axion/join/github` answers 400 with the body shape it wants, no longer 409 \"opened by invite\".\n\nSo for whoever picks this up: every URL in the README is `boards.t.bernardocastro.dev`, the forge step is already done and must not be re-declared, and the milestone's rule still holds — every command in the README gets run at least once, and you say plainly which ones you could not run because you are the owner and not JP.\n\nOne papercut found while doing this, worth its own card and NOT this one: bare `taskops board pull` in a joined checkout defaults the board name to the DIRECTORY (`axion-v3`) instead of the board `board.json` names (`axion`), so it refused with \"no board named 'axion-v3'\". `board pull axion` works."},"id":"41b9109c6dd2c3feba08a6c0dd04eeeb","kind":"comment","task":"tk-d1ff9b","ts":1786394715.91416}
|
|
782
782
|
{"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","ARCHITECTURE.md","src/taskops/cli/serving.py","src/taskops/cli/window.py","src/taskops/http/handler.py","src/taskops/http/static.py","src/taskops/http/upstream.py","src/taskops/ui/app.js","tests/test_window.py","ui/src/client.ts","ui/src/main.tsx"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[6,0],".taskops/board/live.sqlite-wal":null,"ARCHITECTURE.md":[1,1],"src/taskops/cli/serving.py":[1,1],"src/taskops/cli/window.py":[1,1],"src/taskops/http/handler.py":[2,0],"src/taskops/http/static.py":[30,0],"src/taskops/http/upstream.py":[1,1],"src/taskops/ui/app.js":null,"tests/test_window.py":[58,1],"ui/src/client.ts":[5,1],"ui/src/main.tsx":[13,3]},"sha":"ab2da80683a6fb3f4358f01364f5f9b2c2aa0fe0","subject":"the window's address is its port — `taskops ui` opens `/`, not `/board/ui/`"},"id":"bdd27bc33f6137028dfab5546ebfcdfa","kind":"commit","task":"project","ts":1786395781.0}
|
|
783
|
+
{"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","CHANGELOG.md","src/taskops/_version.py","src/taskops/http/gitdoor.py","src/taskops/ui/app.js","tests/test_topology.py","tests/test_ui.py","ui/smoke/fixture.json","ui/smoke/sections/forge-links.tsx","ui/smoke/sections/report-markdown.tsx","ui/smoke/sections/report-sandbox.tsx","ui/src/App.tsx","ui/src/components/card/Drawer.tsx","ui/src/components/card/Sections.tsx","ui/src/components/card/tokens.ts","ui/src/components/monitor/Chapter.tsx","ui/src/components/reports/ReportFrame.tsx","ui/src/types.ts"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[1,0],".taskops/board/live.sqlite-wal":null,"CHANGELOG.md":[25,0],"src/taskops/_version.py":[1,1],"src/taskops/http/gitdoor.py":[14,4],"src/taskops/ui/app.js":null,"tests/test_topology.py":[28,2],"tests/test_ui.py":[14,2],"ui/smoke/fixture.json":[109,109],"ui/smoke/sections/forge-links.tsx":[18,5],"ui/smoke/sections/report-markdown.tsx":[86,0],"ui/smoke/sections/report-sandbox.tsx":[28,7],"ui/src/App.tsx":[8,0],"ui/src/components/card/Drawer.tsx":[9,2],"ui/src/components/card/Sections.tsx":[32,21],"ui/src/components/card/tokens.ts":[10,0],"ui/src/components/monitor/Chapter.tsx":[1,1],"ui/src/components/reports/ReportFrame.tsx":[36,1],"ui/src/types.ts":[5,2]},"sha":"e9fff1749b079e11bd8b2840457f0bca54cb3420","subject":"0.3.2 — prose reads as prose, and the card's worktree link stays inside"},"id":"a032a873f75785936dbfce569ab9b07d","kind":"commit","task":"project","ts":1786397681.0}
|
|
784
|
+
{"actor":"dev:berna","body":{"branch":"master","files":[".github/workflows/ci.yml",".taskops/board/cache.sqlite-shm",".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-shm",".taskops/board/live.sqlite-wal","ARCHITECTURE.md","CLAUDE.md"],"numstat":{".github/workflows/ci.yml":[1,1],".taskops/board/cache.sqlite-shm":null,".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[1,0],".taskops/board/live.sqlite-shm":null,".taskops/board/live.sqlite-wal":null,"ARCHITECTURE.md":[1,1],"CLAUDE.md":[2,2]},"sha":"01f165b6a0cf7c578039b8fb05e3542ba4f06a01","subject":"one trunk, named by what it actually is"},"id":"570e34f74dcc2a70f1e7b5fc52bf516f","kind":"commit","task":"project","ts":1786433708.0}
|
|
785
|
+
{"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/jp2"},"id":"3a8a0205d4946d6eee567de2a405cd77","kind":"edited","task":"tk-d1ff9b","ts":1786433782.743027}
|
|
786
|
+
{"actor":"agent:berna/jp2","body":{"no_code":true,"reason":"The work landed in ~/axion-v3 (commit 48f116a, Bernardo Castro <me@bernardocastro.dev>, no trailers), not in this worktree — .taskops/trees/tk-d1ff9b has nothing to commit.\n\nWHAT CHANGED\n- NEW docs/13-taskops.md (Spanish, matches the 00-12 series tone): onboarding for ANY dev joining the repo. Board URL, the move off taskops.bernardocastro.dev with the measured reason, the two-command GitHub join, the invite as the alternative, what `join` installs, the ELEVEN MCP tools, the local dashboard, the four refusals with their verbatim text, and a §8 \"Verificado\" table.\n- README.md: the personal onboarding section is GONE. 16 lines now: what taskops is here, the board URL, the two commands, and a link to docs/13. Added the doc to the Documentación table.\n- CLAUDE.md §5 was also lying (old host, \"GitHub no longer grants board access\", \"the nine MCP tools\", a CLI list missing board pull/rm/forge). Fixed all four and pointed at docs/13.\n\nVERIFIED BY RUNNING (2026-08-11)\n- /healthz: new host 0.40s, old host 0.75s. Both answer 200.\n- POST /axion/join/github with a well-formed body -> 409 \"GitHub rejected that token (401) ... this board opens to whoever has push on cloudacio/Axion\". That IS the forge read back; I did not re-declare it.\n- /axion/ui/ -> 410.\n- uv tool install taskops-cli -> +taskops-cli==0.3.2 (in an isolated UV_TOOL_DIR; berna's own install untouched).\n- taskops remote add https://boards.t.bernardocastro.dev -> ok. taskops join axion (bare) -> \"joined ... as dev:berna. Hooks installed; the board is in MCP.\"\n- taskops invite probe-tk-d1ff9b --board axion -> minted (id 11568a62b8dbd711); taskops revoke --invite -> \"revoked\". Host is back to boards:1, no leftovers.\n- taskops board ls, taskops --help, taskops board --help -> the eleven commands and board's seven actions, both quoted in the doc from the real output.\n- taskops ui -> served 127.0.0.1:8791, GET /?token= -> 200 text/html, /healthz -> version 0.3.2. Process killed, ui.json removed.\n- ssh-keygen -t ed25519, gh auth token -> ok.\n- Tool count derived from mcp/tools.py TOOLS in BOTH 0.3.1 and 0.3.2: ELEVEN (board, card, activity, filed, plan, assign, merge, take, review, update, comment). The README's \"nueve\" was stale.\n\nCOULD NOT RUN, and why (both stated plainly in docs/13 §8)\n1. A real first `taskops join axion --github` by a non-owner. I ran it and the host refused: \"this host already has a principal named 'berna', and GitHub membership does not extend it\". That proves the door exists, not that it enrols. Only JP (push on cloudacio/Axion, no board credential) can close milestone criterion 3.\n2. `taskops board forge cloudacio/Axion --need push` — already declared by jp1; re-running is re-declaring, not verifying. Read back through the 409 above instead.\n\nCORRECTION TO THE BRIEF: the host does NOT run 0.3.2. `systemctl show taskops` on gpu-relay -> /home/berna/taskops/.venv/bin/taskops serve --root /home/berna/taskops-boards --host 127.0.0.1 --port 8787, and importlib.metadata reports 0.3.1. It does not matter for this card (0.3.1 already carries the activity/filed verbs and the forge door), but \"the relay serves 0.3.2\" is wrong. Berna's laptop is also on 0.3.1 — that is why `taskops ui` there still opens /board/ui/ while a fresh install opens /. The doc tells the reader to use the URL the command prints, so it is right on both.","to":"done"},"id":"6a1c0ebf50326cfb2fa1afc7881dc7b3","kind":"status","task":"tk-d1ff9b","ts":1786434450.829515}
|
|
787
|
+
{"actor":"dev:berna","body":{"into":"ms/production-catches-up-the-host-r","sha":"e8e71eb91f89c02286b29b7b6dce424653b9e87b"},"id":"447096a1f3da6140780b0dfd10f2fc6d","kind":"merged","task":"tk-d1ff9b","ts":1786434517.435541}
|
|
788
|
+
{"actor":"dev:berna","body":{"branch":"master","files":[".taskops/board/cache.sqlite-wal",".taskops/board/events.jsonl",".taskops/board/live.sqlite-wal","README.md","src/taskops/cli/commands.py","tests/test_topology.py"],"numstat":{".taskops/board/cache.sqlite-wal":null,".taskops/board/events.jsonl":[4,0],".taskops/board/live.sqlite-wal":null,"README.md":[61,6],"src/taskops/cli/commands.py":[20,9],"tests/test_topology.py":[28,0]},"sha":"33ad8b0433586e6abf8a8f50c17ff119f82b1c10","subject":"join reads the address the clone already carries — v1's two words, restored"},"id":"7db41a1223bd083e3d4f3dd949d9fc68","kind":"commit","task":"project","ts":1786439728.0}
|
|
789
|
+
{"actor":"dev:berna","body":{"branch":"ms/el-forge-enrola-al-equipo-github","created":1786441801.790267,"criteria":["El dueño corre `taskops board forge <owner>/<repo>` y cada colaborador con el acceso declarado queda enrolado: principal=login, llaves de github.com/<login>.keys — probado end-to-end contra un stub del API y en vivo contra un repo real.","Un dev en un clon entra con `taskops join` a secas: sin token, sin invite, sin flag — su llave ya estaba en allowed_signers.","Re-correr el forge sincroniza: enrola a los nuevos, reporta el drift de los que perdieron acceso (sin revocar solo), y jamás toca al owner.","POST /<board>/join/github, --github y el descubrimiento de token en el join NO EXISTEN más — ni código, ni tests que los nombren, ni docs que los enseñen.","grep por token persistido: cero, en el host y en los checkouts. Suite completa verde, docs de los dos repos diciendo la verdad."],"goal":"Hoy el forge abre una puerta que cada dev tiene que cruzar cargando cosas: un token de GitHub (descubierto, pero suyo), una ssh key que quizás fabricó para esto, y un POST al host en el momento del join. Berna lo dijo exacto: pedir push en el repo Y una llave aparte es redundante — quien pushea por ssh YA tiene una llave, publicada por GitHub en https://github.com/<login>.keys, sin token.\n\nEste capítulo mueve TODA la lógica de GitHub al lado del dueño, donde es trivial: `taskops board forge <owner>/<repo>` pasa de declarar un hecho a SINCRONIZAR el equipo — lista los colaboradores con el acceso declarado (una llamada con el token del dueño, que no se guarda), trae las pubkeys de cada uno del endpoint público, y las enrola en el host. Re-correrlo re-sincroniza. El dev, en su clon, escribe `taskops join` y nada más: su llave ya estaba enrolada, firma y entra. Ni token, ni invite, ni --github, ni ssh-keygen.\n\nCon eso la puerta del dev — POST /<board>/join/github, el flag --github, el descubrimiento de token en join — se BORRA, no se deja comentada. Lo que queda de GitHub: el vocabulario del forge (core/forge.py, qué repo y qué acceso) y el sync del dueño. La regla de la casa no se toca: ningún token de GitHub se guarda nunca, en ningún lado — ahora ni siquiera viaja al host.\n\nLo que este capítulo NO hace: pruning automático. Un principal enrolado por invite (ana, que no es login de GitHub) sería revocado por un sync que nunca la conoció — así que el sync AGREGA y REPORTA el drift (quiénes ya no tienen push, con el comando revoke listo para pegar), y revocar sigue siendo un acto del dueño. Si duele, es el capítulo siguiente.","id":"ms-05f325","op":"create","reviews":false,"rules":["Mutation-check cada fix: romperlo a propósito, UN sitio a la vez, ver caer el test correcto, restaurar — con PYTHONDONTWRITEBYTECODE=1.","Ningún token de GitHub se guarda ni se loguea, nunca: viaja en una llamada del CLI del dueño a api.github.com y muere con ella. Un grep por ghp_/github_token sobre lo persistido es parte de cerrar cada card.","Presupuesto de 200 líneas por módulo — si hay que partir, partir por cohesión, jamás relajar la regla. Los tests de test_architecture.py son la pared.","Borrar es borrar: nada de código comentado, flags muertos ni doors que responden 410. Los tests del código borrado se reemplazan por los del nuevo, y se justifica en el commit qué pinneaban.","Los boards legacy con bearer token siguen siendo una flota: nada de esto puede romper el join por ?token= ni por invite — los test_a_legacy_* son la prueba.","Cada refusal nombra la salida. El sync que encuentra un colaborador sin llaves en GitHub lo REPORTA con el camino (invite), no lo omite en silencio."],"status":"open","title":"El forge enrola al equipo — GitHub es del dueño, y de nadie más"},"id":"20383745733325afad71ca037c1a24da","kind":"milestone","task":"project","ts":1786441801.790267}
|
|
790
|
+
{"actor":"dev:berna","body":{"card":{"after":[],"assignee":"","created":1786441801.787267,"created_by":"dev:berna","criteria":["Mismo lote dos veces deja el mismo estado, probado.","El owner re-enrolado sigue owner; sus llaves se acumulan.","Un no-owner es rechazado con la salida en el mensaje.","allowed_signers rederivado entero tras el lote."],"files":["src/taskops/core/scope.py","src/taskops/http/login.py","src/taskops/store/pubkeys.py","src/taskops/store/server.py","tests/test_topology.py"],"id":"tk-bcf293","labels":[],"milestone":"ms-05f325","parent":null,"priority":0,"review":false,"spec":"La costura de todo el capítulo: una operación de host que recibe [{principal, keys: [líneas ssh-...]}, …] y enrola cada par, para que el CLI del dueño tenga a quién hablarle.\n\nDÓNDE MIRAR primero: `core/scope.py` (board.forge es la pared OWNER exacta a imitar), `http/login.py`::register y `store/pubkeys.py` (el enroll que ya existe — extender, no duplicar: el invite y esto tienen que terminar en el MISMO sitio), `store/server.py` (principals/roles), y el test `test_a_re_join_never_demotes_the_owner_it_only_adds_a_key` — esa propiedad tiene que sobrevivir al lote: enrolar un principal que ya existe AGREGA llaves, jamás reescribe el rol.\n\nSemántica: idempotente (mismo lote dos veces = mismo estado); un principal nuevo entra como member; el owner jamás cambia; allowed_signers se rederiva ENTERO al final, como ya hace todo enroll. La respuesta dice qué hizo: enrolados nuevos, llaves agregadas, sin cambios — números y nombres, porque el CLI los va a imprimir tal cual.\n\nQué NO hace: no revoca a nadie (el porqué está en el goal del milestone — un principal de invite sería víctima de un sync que no lo conoció), no habla con GitHub (no sabe qué es GitHub: recibe principals y llaves), no toca boards (es del HOST, como server init).\n\nTests: la pared de rol (un member que lo intenta es rechazado nombrando la salida), idempotencia, owner intocable, llaves acumulándose, y los legacy intactos.","status":"open","title":"El host enrola en lote — la operación members, del OWNER y de nadie más","updated":1786441801.787267}},"id":"759363f6cae91d1121454bb396118bc9","kind":"created","task":"tk-bcf293","ts":1786441801.787267}
|
|
791
|
+
{"actor":"dev:berna","body":{"card":{"after":["tk-bcf293"],"assignee":"","created":1786441801.788267,"created_by":"dev:berna","criteria":["forge contra el stub enrola a todos los colaboradores con push, paginación incluida.","Un colaborador sin llaves en GitHub queda nombrado con su salida, no omitido.","El drift se reporta con el comando revoke exacto; nada se revoca solo.","El token no aparece en nada persistido ni impreso — grep como parte del cierre."],"files":["src/taskops/cli/operate.py","src/taskops/cli/enrol.py","src/taskops/_wire.py","tests/test_topology.py"],"id":"tk-194ff2","labels":[],"milestone":"ms-05f325","parent":null,"priority":0,"review":false,"spec":"`taskops board forge <owner>/<repo> [--need push|admin]` pasa de declarar a DECLARAR+SINCRONIZAR, y re-correrlo re-sincroniza.\n\nEl flujo del lado del dueño, todo en el CLI: (1) el token del dueño — MOVER `enrol.github_token()` a donde corresponda ahora, no reescribirlo: gh auth token → $GITHUB_TOKEN → prompt oculto, jamás un valor de flag; (2) GET /repos/<o>/<r>/collaborators con el permiso declarado (paginado — un equipo no entra en una página; api.github.com via _wire); (3) por cada login, GET https://github.com/<login>.keys — público, sin token; (4) la operación members de la card anterior, un solo lote; (5) imprimir el reporte: enrolados, llaves agregadas, SIN LLAVES EN GITHUB (nombrados, con `taskops invite <login>` como salida), y el DRIFT — principals del host que ya no están en la lista de colaboradores, con el `taskops revoke --key SHA256:…` listo para pegar y la aclaración de que un principal de invite puede aparecer ahí legítimamente.\n\nEl token muere con la llamada (2). No se guarda, no se loguea, no viaja al host — el host recibe principals y llaves, nunca sabe de GitHub.\n\nDÓNDE MIRAR: `cli/operate.py` (forge hoy), `cli/enrol.py` (github_token — se muda con su docstring, que es el post-mortem del flag), `core/forge.py` (el vocabulario, no debería cambiar), `_wire.py`. Tests con un stub del API de GitHub sobre socket real — el patrón ya existe en test_topology para el stub forge; los tests del stub viejo que prueban MEMBRESÍA del lado del host van a morir en la card siguiente, los tuyos prueban el SYNC del lado del CLI.\n\nOjo con el presupuesto de líneas en operate.py — si el sync no entra, módulo propio (cli/sync.py o similar), partido por cohesión.","status":"open","title":"El forge sincroniza — collaborators con push → .keys → enroll, y el drift reportado","updated":1786441801.788267}},"id":"1ce93c8f500604691a443f56a3dc9f54","kind":"created","task":"tk-194ff2","ts":1786441801.788267}
|
|
792
|
+
{"actor":"dev:berna","body":{"card":{"after":["tk-194ff2"],"assignee":"","created":1786441801.789267,"created_by":"dev:berna","criteria":["grep -r 'join/github\\|--github\\|by_github' sobre src/ y tests/: cero.","El e2e nuevo: sync enrola → clon fresco → `taskops join` a secas → adentro.","Los legacy (?token=, invite) siguen verdes sin tocar sus tests.","handler.py y parser.py más chicos que antes del capítulo."],"files":["src/taskops/http/github.py","src/taskops/http/handler.py","src/taskops/cli/parser.py","src/taskops/cli/commands.py","src/taskops/cli/enrol.py","tests/test_topology.py"],"id":"tk-4c9761","labels":[],"milestone":"ms-05f325","parent":null,"priority":0,"review":false,"spec":"Con el sync vivo, la puerta del dev es peso muerto y se BORRA entera: `http/github.py` (el módulo), su ruta en `http/handler.py` (POST /<board>/join/github), el flag `--github` de `cli/parser.py`, la rama github de `commands.join`, `enrol.by_github` + NO_KEY + NO_TOKEN (github_token ya se mudó en la card anterior — verificar que nada del lado dev lo importe).\n\nEl join queda: bare (dirección del clon o remote grabado) con llave · --invite · ?token= legacy · público read-only. Un dev cuya llave enroló el sync entra con `taskops join` a secas — probarlo end-to-end: stub-sync enrola, clon fresco con board.json, join bare, firma, entra. `--as <login>` para el unix user que no coincide: ya existe, solo verificar que el camino funciona.\n\nLOS TESTS DEL CÓDIGO BORRADO: los que pineaban la puerta (el stub forge de test_topology que responde membresía, los test del door 400/409, BOTH invite+github) se borran CON justificación en el commit — pineaban un contrato que este capítulo retira a propósito. Los que pinean cosas que sobreviven (github_token nunca en un flag, el token muere con la llamada) se MUDAN al lado del sync si la card anterior no lo hizo ya. El refusal de un board sin forge (409 invite-only) también muere con la ruta.\n\nCuidado quirúrgico con handler.py y parser.py: presupuestos 191/149, borrar debería BAJARLOS. mcp/schema o tools no nombran --github (verificar con grep). El hecho op=forge y core/forge.py QUEDAN — son el vocabulario del sync ahora.","status":"open","title":"La puerta del dev se borra — join sin GitHub, de verdad","updated":1786441801.789267}},"id":"5ccccc43f25f0e7c696f926524b00574","kind":"created","task":"tk-4c9761","ts":1786441801.789267}
|
|
793
|
+
{"actor":"dev:berna","body":{"card":{"after":["tk-4c9761"],"assignee":"","created":1786441801.790267,"created_by":"dev:berna","criteria":["grep --github/join/github en docs de ambos repos: cero, salvo historia (CHANGELOG viejo).","ARCHITECTURE §19 cuenta el diseño nuevo con la fecha; §11 endurecido.","El board de axion sincronizado, con el reporte pegado en la card — o el bloqueo dicho con el comando listo.","Cada comando de los docs, corrido; los que no, nombrados."],"files":["README.md","ARCHITECTURE.md","CHANGELOG.md","CLAUDE.md"],"id":"tk-eb8453","labels":[],"milestone":"ms-05f325","parent":null,"priority":1,"review":false,"spec":"Todo lo que enseña el flujo viejo, corregido en el MISMO capítulo que lo cambió — un doc que enseña --github un día más es un onboarding roto para el próximo que clone.\n\n(1) taskops-v2: README (la escalera del join, la sección '### The whole flow' con su diagrama — el POST del dev desaparece, el sync del dueño entra; la lista de board actions; el bloque del forge), ARCHITECTURE.md §19 se reescribe: el título 'GitHub is the INTRODUCTION, never the credential' evoluciona a su forma final — GitHub es el ROSTER, preguntado por el dueño; el dev no carga nada. La entrada de §11 (STORED GitHub token) queda MÁS fuerte: ahora ni viaja. CLAUDE.md del repo si nombra el flujo. CHANGELOG: sección nueva SIN número de versión — el número lo elige Berna.\n\n(2) axion-v3: docs/13-taskops.md (el onboarding entero se simplifica: `taskops join` y reiniciar Claude — la sección --github muere), README (el bloque de dos líneas queda en una), CLAUDE.md §5 si lo nombra. Identidad de commit: Bernardo Castro <me@bernardocastro.dev>, sin trailers.\n\n(3) El board REAL de axion: correr el forge sync contra cloudacio/Axion en boards.t.bernardocastro.dev y pegar el reporte en la card — quiénes quedaron enrolados. Si el token de gh activo no alcanza para listar colaboradores de cloudacio, DECIRLO y dejar el comando exacto para que Berna lo corra — no inventar acceso.\n\n(4) Si algún .tsx cambió en el capítulo (no debería — el header del forge sigue igual), UNA rebuild del bundle acá. Verificar con git diff antes de asumir.\n\nRegla de siempre: cada comando que aparezca en un doc, corrido al menos una vez, y decir cuáles no pudiste.","status":"open","title":"Cierre — los docs dejan de enseñar la puerta que no existe, y axion se re-forja","updated":1786441801.790267}},"id":"d9c06ea6386110cf2efa70c61a9be318","kind":"created","task":"tk-eb8453","ts":1786441801.790267}
|
|
794
|
+
{"actor":"dev:berna","body":{"field":"assignee","to":"agent:berna/lote1"},"id":"7d26319e1285adef7fe667287cf0ac7a","kind":"edited","task":"tk-bcf293","ts":1786441826.92571}
|
|
Binary file
|
|
Binary file
|
|
@@ -836,7 +836,7 @@ nobody has touched yet.
|
|
|
836
836
|
|---|---|---|
|
|
837
837
|
| a `recover` verb | doing is derived from the live lease; nothing is ever wrong to recover | 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
|
-
| AUTOMATIC merges to
|
|
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
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) |
|
|
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` |
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
The source of truth for release notes — GitHub Releases are extracted from
|
|
4
4
|
here, never written twice.
|
|
5
5
|
|
|
6
|
+
## 0.3.3 — a clone joins with two words
|
|
7
|
+
|
|
8
|
+
- **`taskops join` reads the address the clone already carries.** v2 committed
|
|
9
|
+
`.taskops/board.json` exactly as v1 did — and never read it back: a bare join
|
|
10
|
+
resolved through the per-machine recorded remote, so a fresh clone died on
|
|
11
|
+
"which host?" while holding the answer in its tree. Restored: inside a clone,
|
|
12
|
+
`taskops join` (or `taskops join --github`, first time) is the whole
|
|
13
|
+
onboarding. `taskops remote add` remains for a checkout that carries no
|
|
14
|
+
address, or a join onto a different board.
|
|
15
|
+
|
|
6
16
|
## 0.3.2 — the window's address, prose that reads as prose, and a link that stays inside
|
|
7
17
|
|
|
8
18
|
- **`taskops ui` opens `http://127.0.0.1:<port>/`.** It used to hand out
|
|
@@ -37,7 +37,7 @@ releases its card by definition. A stalled card is handed over with
|
|
|
37
37
|
**2. Branches are inhabited, not switched.** `git switch` appears nowhere.
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
|
|
40
|
+
master ──────────────────────────▶ the HUMAN decides: a PR, or taskops_merge milestone=
|
|
41
41
|
└─ ms/<slug> ──┬──────┬───────▶ the ORCHESTRATOR integrates, card by card
|
|
42
42
|
tk-a11 tk-b22 ← one WORKER each, one worktree each
|
|
43
43
|
```
|
|
@@ -202,7 +202,7 @@ what it exists to report, not a fault.
|
|
|
202
202
|
|
|
203
203
|
Each has its line in ARCHITECTURE.md §11 saying what it cost and where it is
|
|
204
204
|
enforced: a reviewer ROLE, a stored review STATUS, or automatic reviewer
|
|
205
|
-
assignment · `land` or automatic merges to main · git replication between clones
|
|
205
|
+
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
|
|
206
206
|
· Claude hooks **that decide or store** · a stored `doing` · a slug in a branch
|
|
207
207
|
name · a `recover` · a mark-as-read/ack verb · per-request SIGNING · hand-rolled
|
|
208
208
|
crypto or a pip crypto dependency · a `--force` on `board push` **or on `board
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: taskops-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -47,8 +47,9 @@ can be misread:
|
|
|
47
47
|
into it exactly once, to install it and register your key.
|
|
48
48
|
* **A board**: one directory on that host. Created from your laptop with
|
|
49
49
|
`taskops board create`.
|
|
50
|
-
* **A repo**: joined to one board.
|
|
51
|
-
|
|
50
|
+
* **A repo**: joined to one board. A clone carries the board's address
|
|
51
|
+
(`.taskops/board.json`, committed), so `taskops join` is the whole step;
|
|
52
|
+
`taskops remote add` covers a checkout that carries none.
|
|
52
53
|
* **Your agents**: they never touch any of this — they talk to the board through
|
|
53
54
|
the eleven MCP tools.
|
|
54
55
|
|
|
@@ -90,10 +91,11 @@ Joining a hosted one is bare, like every other verb — the host is recorded onc
|
|
|
90
91
|
and the key is discovered the way ssh discovers one:
|
|
91
92
|
|
|
92
93
|
```sh
|
|
93
|
-
taskops
|
|
94
|
-
taskops join
|
|
95
|
-
taskops join my-project --invite <id> # first time
|
|
96
|
-
taskops
|
|
94
|
+
taskops join # a clone: board.json carries the address, done
|
|
95
|
+
taskops join --github # same clone, first time: GitHub vouches for you
|
|
96
|
+
taskops join my-project --invite <id> # first time by invite: it enrols your key too
|
|
97
|
+
taskops remote add https://host:8787 # no carried address? record the host once…
|
|
98
|
+
taskops join my-project # …and name the board
|
|
97
99
|
taskops join my-project # no key + public board: read-only window
|
|
98
100
|
```
|
|
99
101
|
|
|
@@ -222,6 +224,59 @@ until an owner records it and cleared again with `--clear`. Only a board that
|
|
|
222
224
|
has been opted in answers `--github` at all; every other one is invite-only,
|
|
223
225
|
exactly as before.
|
|
224
226
|
|
|
227
|
+
### The whole flow, and why cloning is not enough
|
|
228
|
+
|
|
229
|
+
Cloning the repo gives you the board's ADDRESS — `.taskops/board.json` is
|
|
230
|
+
committed and travels with the code — but the host has never seen you: it is a
|
|
231
|
+
different server from GitHub, sharing no session and no cookie with it. So one
|
|
232
|
+
command — `taskops join --github`, no URL: the clone already carries it —
|
|
233
|
+
introduces you, once per checkout, and after that GitHub is not in the picture
|
|
234
|
+
at all.
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
GitHub the board HOST
|
|
238
|
+
<owner>/<repo>, private principals + allowed_signers
|
|
239
|
+
| |
|
|
240
|
+
1. git clone -> the code, and .taskops/board.json (the address).
|
|
241
|
+
remote.json is 0600 and gitignored: no credential travels.
|
|
242
|
+
| |
|
|
243
|
+
2. taskops join --github (the carried address) |
|
|
244
|
+
| |
|
|
245
|
+
|-- the CLI finds YOUR token: `gh auth token`, else $GITHUB_TOKEN,
|
|
246
|
+
| else a hidden prompt. Never a flag value — the shell writes those
|
|
247
|
+
| into ~/.zsh_history before the process starts.
|
|
248
|
+
| |
|
|
249
|
+
|-- POST /<board>/join/github ------------------>|
|
|
250
|
+
| { github_token, principal, pubkey } |
|
|
251
|
+
| |
|
|
252
|
+
| the host asks GitHub ONCE, with your token:
|
|
253
|
+
| "does this user have <need> on <owner>/<repo>?"
|
|
254
|
+
| |
|
|
255
|
+
| yes -> it writes exactly two rows: |
|
|
256
|
+
| principals: <you>, member
|
|
257
|
+
| allowed_signers: <you> ssh-ed25519 AAAA...
|
|
258
|
+
| no -> refused, without saying whether the repo exists
|
|
259
|
+
| |
|
|
260
|
+
3. the GitHub token dies with that request. It is on neither disk.
|
|
261
|
+
|
|
262
|
+
from here on GitHub never participates again:
|
|
263
|
+
|
|
264
|
+
every session: your key signs a challenge -> the host checks it
|
|
265
|
+
(~/.ssh/id_ed25519) against allowed_signers
|
|
266
|
+
-> a 12h session
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The alternative — re-checking GitHub on every request — would mean the host
|
|
270
|
+
holding somebody else's token, or being handed one on each call. That is the
|
|
271
|
+
whole category of problem this removes: there is no token to steal because
|
|
272
|
+
there is none stored.
|
|
273
|
+
|
|
274
|
+
**One consequence, stated plainly: access is granted automatically and taken
|
|
275
|
+
back by hand.** Losing push on the repo does not close the board, because the
|
|
276
|
+
credential is no longer GitHub — it is the enrolled key. Remove it with
|
|
277
|
+
`taskops revoke --key SHA256:…`, which is the same verb an invite-enrolled key
|
|
278
|
+
takes.
|
|
279
|
+
|
|
225
280
|
No URL and no `--key` after `remote add`: the host is recorded in the checkout,
|
|
226
281
|
`board create` records the name, and the key is **discovered** the way ssh
|
|
227
282
|
discovers one — `~/.ssh/id_ed25519`, `id_ecdsa`, `id_rsa`, in that order.
|
|
@@ -35,8 +35,9 @@ can be misread:
|
|
|
35
35
|
into it exactly once, to install it and register your key.
|
|
36
36
|
* **A board**: one directory on that host. Created from your laptop with
|
|
37
37
|
`taskops board create`.
|
|
38
|
-
* **A repo**: joined to one board.
|
|
39
|
-
|
|
38
|
+
* **A repo**: joined to one board. A clone carries the board's address
|
|
39
|
+
(`.taskops/board.json`, committed), so `taskops join` is the whole step;
|
|
40
|
+
`taskops remote add` covers a checkout that carries none.
|
|
40
41
|
* **Your agents**: they never touch any of this — they talk to the board through
|
|
41
42
|
the eleven MCP tools.
|
|
42
43
|
|
|
@@ -78,10 +79,11 @@ Joining a hosted one is bare, like every other verb — the host is recorded onc
|
|
|
78
79
|
and the key is discovered the way ssh discovers one:
|
|
79
80
|
|
|
80
81
|
```sh
|
|
81
|
-
taskops
|
|
82
|
-
taskops join
|
|
83
|
-
taskops join my-project --invite <id> # first time
|
|
84
|
-
taskops
|
|
82
|
+
taskops join # a clone: board.json carries the address, done
|
|
83
|
+
taskops join --github # same clone, first time: GitHub vouches for you
|
|
84
|
+
taskops join my-project --invite <id> # first time by invite: it enrols your key too
|
|
85
|
+
taskops remote add https://host:8787 # no carried address? record the host once…
|
|
86
|
+
taskops join my-project # …and name the board
|
|
85
87
|
taskops join my-project # no key + public board: read-only window
|
|
86
88
|
```
|
|
87
89
|
|
|
@@ -210,6 +212,59 @@ until an owner records it and cleared again with `--clear`. Only a board that
|
|
|
210
212
|
has been opted in answers `--github` at all; every other one is invite-only,
|
|
211
213
|
exactly as before.
|
|
212
214
|
|
|
215
|
+
### The whole flow, and why cloning is not enough
|
|
216
|
+
|
|
217
|
+
Cloning the repo gives you the board's ADDRESS — `.taskops/board.json` is
|
|
218
|
+
committed and travels with the code — but the host has never seen you: it is a
|
|
219
|
+
different server from GitHub, sharing no session and no cookie with it. So one
|
|
220
|
+
command — `taskops join --github`, no URL: the clone already carries it —
|
|
221
|
+
introduces you, once per checkout, and after that GitHub is not in the picture
|
|
222
|
+
at all.
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
GitHub the board HOST
|
|
226
|
+
<owner>/<repo>, private principals + allowed_signers
|
|
227
|
+
| |
|
|
228
|
+
1. git clone -> the code, and .taskops/board.json (the address).
|
|
229
|
+
remote.json is 0600 and gitignored: no credential travels.
|
|
230
|
+
| |
|
|
231
|
+
2. taskops join --github (the carried address) |
|
|
232
|
+
| |
|
|
233
|
+
|-- the CLI finds YOUR token: `gh auth token`, else $GITHUB_TOKEN,
|
|
234
|
+
| else a hidden prompt. Never a flag value — the shell writes those
|
|
235
|
+
| into ~/.zsh_history before the process starts.
|
|
236
|
+
| |
|
|
237
|
+
|-- POST /<board>/join/github ------------------>|
|
|
238
|
+
| { github_token, principal, pubkey } |
|
|
239
|
+
| |
|
|
240
|
+
| the host asks GitHub ONCE, with your token:
|
|
241
|
+
| "does this user have <need> on <owner>/<repo>?"
|
|
242
|
+
| |
|
|
243
|
+
| yes -> it writes exactly two rows: |
|
|
244
|
+
| principals: <you>, member
|
|
245
|
+
| allowed_signers: <you> ssh-ed25519 AAAA...
|
|
246
|
+
| no -> refused, without saying whether the repo exists
|
|
247
|
+
| |
|
|
248
|
+
3. the GitHub token dies with that request. It is on neither disk.
|
|
249
|
+
|
|
250
|
+
from here on GitHub never participates again:
|
|
251
|
+
|
|
252
|
+
every session: your key signs a challenge -> the host checks it
|
|
253
|
+
(~/.ssh/id_ed25519) against allowed_signers
|
|
254
|
+
-> a 12h session
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The alternative — re-checking GitHub on every request — would mean the host
|
|
258
|
+
holding somebody else's token, or being handed one on each call. That is the
|
|
259
|
+
whole category of problem this removes: there is no token to steal because
|
|
260
|
+
there is none stored.
|
|
261
|
+
|
|
262
|
+
**One consequence, stated plainly: access is granted automatically and taken
|
|
263
|
+
back by hand.** Losing push on the repo does not close the board, because the
|
|
264
|
+
credential is no longer GitHub — it is the enrolled key. Remove it with
|
|
265
|
+
`taskops revoke --key SHA256:…`, which is the same verb an invite-enrolled key
|
|
266
|
+
takes.
|
|
267
|
+
|
|
213
268
|
No URL and no `--key` after `remote add`: the host is recorded in the checkout,
|
|
214
269
|
`board create` records the name, and the key is **discovered** the way ssh
|
|
215
270
|
discovers one — `~/.ssh/id_ed25519`, `id_ecdsa`, `id_rsa`, in that order.
|
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from . import enrol, watch
|
|
16
16
|
from .. import session, identity
|
|
17
17
|
from .._json import query
|
|
18
|
-
from ..board import DIR, find_root, open_board
|
|
18
|
+
from ..board import DIR, find_root, open_board, read_config
|
|
19
19
|
from ..store import log
|
|
20
20
|
from .._errors import TaskopsError
|
|
21
21
|
from ..gitwork import remote, install, claudefiles
|
|
@@ -58,13 +58,15 @@ def join( # noqa: PLR0913 — one command, one config; each flag is a way IN
|
|
|
58
58
|
invite: str = "",
|
|
59
59
|
github: bool = False,
|
|
60
60
|
) -> int:
|
|
61
|
-
"""Connect this repo to a board. Bare like every other verb
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
taskops join
|
|
66
|
-
taskops join
|
|
67
|
-
taskops join my-project --
|
|
61
|
+
"""Connect this repo to a board. Bare like every other verb: the address is
|
|
62
|
+
CARRIED by the clone, the key is discovered, and the recorded remote is the
|
|
63
|
+
fallback for a checkout that carries nothing:
|
|
64
|
+
|
|
65
|
+
taskops join a clone: board.json travels, done
|
|
66
|
+
taskops join --github same clone, first time: GitHub vouches
|
|
67
|
+
taskops join my-project --invite <id> first time by invite: enrols the key
|
|
68
|
+
taskops remote add https://host:8787 no carried address? record the host…
|
|
69
|
+
taskops join my-project …and name the board
|
|
68
70
|
taskops join my-project no key anywhere + public board: read-only
|
|
69
71
|
|
|
70
72
|
The name defaults exactly as `board create`'s does (recorded name, else the
|
|
@@ -93,8 +95,17 @@ def join( # noqa: PLR0913 — one command, one config; each flag is a way IN
|
|
|
93
95
|
root = find_root(here)
|
|
94
96
|
bare = "://" not in target
|
|
95
97
|
if bare:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
# v1's whole ambition, restored (its join.py said it in one line: "a
|
|
99
|
+
# clone carries `.taskops/board.json`, so the second developer types
|
|
100
|
+
# two words"). The committed address is read FIRST, so a fresh clone
|
|
101
|
+
# joins with no URL and no `remote add` — the recorded remote is for
|
|
102
|
+
# the checkout that has no carried address, or a DIFFERENT board.
|
|
103
|
+
carried = str(read_config(root).get("url", ""))
|
|
104
|
+
if carried and target in ("", carried.rsplit("/", 1)[-1]):
|
|
105
|
+
target = carried
|
|
106
|
+
else:
|
|
107
|
+
host, name = remote_cli.named(target)
|
|
108
|
+
target = f"{host}/{name}"
|
|
98
109
|
base = target.partition("?")[0]
|
|
99
110
|
params = query(target)
|
|
100
111
|
invite = invite or params.get("invite", "")
|
|
@@ -665,6 +665,34 @@ def test_an_invite_registers_the_joiners_key_and_still_answers_a_token(
|
|
|
665
665
|
assert RemoteBoard(url_of(server), minted["token"], ANA).call("board", {})["seq"] >= 0
|
|
666
666
|
|
|
667
667
|
|
|
668
|
+
def test_a_clone_that_carries_its_boards_address_joins_bare_no_url_no_remote(
|
|
669
|
+
server: BoardServer, keyed: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
670
|
+
) -> None:
|
|
671
|
+
"""v1's two-word join, restored (2026-08-11). `.taskops/board.json` is
|
|
672
|
+
COMMITTED and travels with the clone, so the second developer types
|
|
673
|
+
`taskops join` and nothing else — no URL pasted from a chat, no
|
|
674
|
+
`remote add` first. The regression: v2 resolved a bare join through the
|
|
675
|
+
RECORDED remote (`remote.json`, per-machine, never travels) and ignored the
|
|
676
|
+
address the clone already carried. The recorded remote stays the fallback
|
|
677
|
+
for a checkout with no carried address, or a join onto a DIFFERENT board.
|
|
678
|
+
|
|
679
|
+
Both bare spellings, because both read the carried address: no target at
|
|
680
|
+
all, and the board's own name (which must not fall through to `named()`
|
|
681
|
+
and die on the missing remote)."""
|
|
682
|
+
from taskops.cli import commands
|
|
683
|
+
|
|
684
|
+
for target in ("", BOARD):
|
|
685
|
+
project = tmp_path / f"clone-{target or 'bare'}"
|
|
686
|
+
(project / ".git").mkdir(parents=True)
|
|
687
|
+
(project / ".taskops").mkdir()
|
|
688
|
+
(project / ".taskops" / "board.json").write_text(json.dumps({"url": url_of(server)}))
|
|
689
|
+
monkeypatch.setenv("TASKOPS_ACTOR", BERNA)
|
|
690
|
+
commands.join(project, target, BERNA, str(keyed))
|
|
691
|
+
saved = json.loads((project / ".taskops" / "remote.json").read_text())
|
|
692
|
+
assert saved["login"]["host"] == host_of(server)
|
|
693
|
+
assert RemoteBoard(url_of(server), saved["token"], BERNA).call("board", {})["seq"] >= 0
|
|
694
|
+
|
|
695
|
+
|
|
668
696
|
def test_a_re_join_never_demotes_the_owner_it_only_adds_a_key(
|
|
669
697
|
server: BoardServer, keyed: Path, tmp_path: Path
|
|
670
698
|
) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|