mcp-gee-sweet 0.7.0.dev87__tar.gz → 0.7.0.dev93__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.
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.claude/commands/cleanup-worktrees.md +1 -1
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.claude/commands/merge-pr.md +2 -0
- mcp_gee_sweet-0.7.0.dev93/.claude/commands/orchestrator.md +21 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.claude/commands/prep-for-pr.md +1 -1
- mcp_gee_sweet-0.7.0.dev93/.claude/commands/team-member.md +88 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.claude/commands/verify-pr.md +3 -1
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/workflows/ci.yml +1 -1
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/workflows/docs.yml +1 -1
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/workflows/publish-dev.yml +2 -2
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/workflows/release.yml +2 -2
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.gitignore +4 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/CLAUDE.md +2 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/Makefile +8 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/PKG-INFO +1 -1
- mcp_gee_sweet-0.7.0.dev93/docs/decisions/decision-async-tool-execution.md +111 -0
- mcp_gee_sweet-0.7.0.dev93/docs/decisions/decision-dev-team.md +38 -0
- mcp_gee_sweet-0.7.0.dev93/docs/decisions/decision-release-docs-roles.md +39 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/index.md +3 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/drive.md +75 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/infra.md +15 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/sheets_read.md +43 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/roadmap.md +1 -1
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/tools.md +1 -1
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/pyproject.toml +2 -0
- mcp_gee_sweet-0.7.0.dev93/scripts/setup_team.sh +107 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/auth.py +4 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/cache.py +63 -16
- mcp_gee_sweet-0.7.0.dev93/src/mcp_gee_sweet/http_transport.py +42 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/server.py +8 -5
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/cache.py +3 -3
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/calendar.py +74 -31
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/content.py +85 -48
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/emitter.py +85 -40
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/layout.py +50 -34
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/style.py +37 -17
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/tables.py +109 -79
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/drive/activity.py +6 -2
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/drive/files.py +190 -75
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/drive/sharing.py +137 -77
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/drive/transfer.py +197 -123
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/response_limits.py +14 -5
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/sheets/data.py +131 -60
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/sheets/helpers.py +11 -7
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/sheets/structure.py +111 -64
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/drive/test_activity.py +36 -36
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/drive/test_files.py +119 -74
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/drive/test_sharing.py +69 -30
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/drive/test_transfer.py +17 -17
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/sheets/test_data.py +111 -60
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/sheets/test_structure.py +135 -125
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_cache.py +51 -15
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_calendar.py +98 -92
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_docs_content.py +110 -110
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_docs_layout.py +42 -42
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_docs_style.py +40 -40
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_docs_tables.py +86 -82
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_response_limits.py +10 -10
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_server.py +42 -21
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/uv.lock +25 -0
- mcp_gee_sweet-0.7.0.dev87/.claude/commands/orchestrator.md +0 -20
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.claude/commands/start-worktree.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.dockerignore +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.python-version +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/Dockerfile +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/LICENSE +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-cache-invalidation.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/design.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/docs/qa-checklist.md +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/tests/test_gen_tool_docs.py +0 -0
{mcp_gee_sweet-0.7.0.dev87 → mcp_gee_sweet-0.7.0.dev93}/.claude/commands/cleanup-worktrees.md
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Remove worktrees under `.claude/worktrees/` whose branch has a merged PR, so completed tickets don't pile up as stale directories. Safe by default: never deletes anything with uncommitted or unpushed changes — those are reported, not touched.
|
|
2
2
|
|
|
3
|
-
1. List current worktrees: `git worktree list --porcelain`. Skip the main checkout (the entry with no `.claude/worktrees/` path).
|
|
3
|
+
1. List current worktrees: `git worktree list --porcelain`. Skip the main checkout (the entry with no `.claude/worktrees/` path). Also skip the six dev-team slot worktrees (`.claude/worktrees/ash`, `sky`, `jay`, `kit`, `aziz`, `amy`) unconditionally — they're persistent, not one-off ticket worktrees, and are never removed by this command regardless of branch/PR state. `/merge-pr` resets them to idle (`team/<name>`) in place after each ticket merges; this command has no role there.
|
|
4
4
|
2. For each remaining worktree, get its branch name and check whether it has a merged PR:
|
|
5
5
|
```
|
|
6
6
|
gh pr list --repo khuisman/mcp-gee-sweet --state merged --head <branch> --json number,url,mergedAt
|
|
@@ -5,9 +5,11 @@ Merge the current branch's PR and clean up its worktree. Uses `--admin` only to
|
|
|
5
5
|
- `state` must be `OPEN`.
|
|
6
6
|
- Every check in `statusCheckRollup` (in particular "Lint and test") must show success. If anything is failing or still pending, stop and report which — do not merge, and do not wait/poll.
|
|
7
7
|
- `mergeable` must not be `CONFLICTING`.
|
|
8
|
+
- If the branch's second `/`-separated segment is one of the four dev-lane slot names (`ash`, `sky`, `jay`, `kit` — the type prefix in front varies, `feat`/`fix`/`chore`/etc., don't assume `feat`), it must carry the `qa-approved` label (check `gh pr view <number> --json labels`). If it doesn't, stop — that means the paired QA agent (`/team-member`) hasn't signed off yet; don't merge and don't review it yourself here. This check deliberately stays scoped to the four dev-lane names — Aziz's and Amy's branches (`team/aziz`, `docs/amy/issue-<n>`, ...) aren't part of the Dev/QA pairing and go through normal review instead.
|
|
8
9
|
3. Show the user a summary: PR number, title, URL, check status, and review status (expected to be blocked only on the missing-approval requirement). State plainly that merging will use `--admin` to bypass that review gate. Ask for explicit confirmation before proceeding.
|
|
9
10
|
4. On confirmation, squash-merge (matching this repo's existing history — commits land on `develop` as a single `title (#number)` commit): `gh pr merge <number> --admin --squash`. The remote branch is auto-deleted on merge (repo setting), so don't pass `--delete-branch`.
|
|
10
11
|
5. Clean up:
|
|
12
|
+
- If the merged branch's second segment was one of the six dev-team slot names (`ash`, `sky`, `jay`, `kit`, `aziz`, `amy`) — again, regardless of its type prefix — **do not remove the worktree**: it's a persistent slot, not a one-off. From that worktree, reset it to idle instead: `git fetch origin develop`, `git checkout team/<name>`, `git reset --hard origin/develop`, then delete the just-merged branch by its actual name (`git branch --show-current` before switching, or read it off the PR's `headRefName` — don't assume it starts with `feat/`). Report that the slot is back to idle, ready for its next ticket.
|
|
11
13
|
- If the current branch is a `review/<headRefName>` branch created by `/verify-pr` (not a `.claude/worktrees/*` worktree — just a local branch in the main checkout), there's no worktree to remove: switch to `develop` and `git branch -D review/<headRefName>`.
|
|
12
14
|
- If this session created the current worktree (i.e. it got here via `/start-worktree` or `/next-issue` earlier in this same session), use `ExitWorktree` with `action: "remove"`.
|
|
13
15
|
- Otherwise (worktree predates this session), fall back to manual cleanup from the main checkout: `git worktree remove <path>`, then `git branch -d <branch>`, then `git worktree prune`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Establish this session as **Kai**, the orchestrator — the one session responsible for ticket triage, lane assignment, roadmap grooming, live testing, and merging PRs, as opposed to a worker session doing ticket implementation in a worktree. Code review and live QA belong to the Sky/Kit QA agents now (see `/team-member`) — Kai's merging step just checks their work landed (`qa-approved` label) rather than redoing it.
|
|
2
|
+
|
|
3
|
+
Why this role split exists: any locally-configured MCP server that runs this project via `uv run --directory <path>` (check your global MCP config for one pointing at this repo), plus the `mcp-gee-sweet` docker-compose service (bind-mounts `./src`), always serve whatever is checked out at that fixed path — the main checkout, not whichever worktree a session's shell happens to be in. A worktree's code is invisible to every live MCP tool call until it's checked out in the main checkout or merged to the default branch. So only one place can ever do meaningful live testing — the main checkout — and only one session should treat it as home base at a time.
|
|
4
|
+
|
|
5
|
+
1. Confirm this session is running from the main checkout, not a `.claude/worktrees/*` path. If it's in a worktree, tell the user the orchestrator role requires the main checkout and ask whether to `ExitWorktree` (`keep`) first.
|
|
6
|
+
2. Check `git branch --show-current` and `git status --short` on the main checkout:
|
|
7
|
+
- On `develop` and clean: ready.
|
|
8
|
+
- On a feature branch, or dirty: flag it. Ticket implementation shouldn't live in the main checkout under this model — it blocks freely checking out other branches to live-test them. Ask the user whether to move that work into its own worktree (`start-worktree`) or continue as-is for now.
|
|
9
|
+
3. Report current state so the user can steer:
|
|
10
|
+
- `git worktree list` — active worker worktrees
|
|
11
|
+
- `gh pr list --state open` — PRs awaiting live-test/review/merge (run from inside the repo so `gh` infers it from the git remote — don't hardcode a repo slug, forks won't match)
|
|
12
|
+
- `gh issue list --label ready-for-development --state open` — queued ticket(s)
|
|
13
|
+
4. State the orchestrator's responsibilities plainly:
|
|
14
|
+
- Ticket triage, labeling, and **lane assignment**. If the dev-team is active (`.claude/worktrees/ash` and `.claude/worktrees/jay` exist), also check which lane is idle before labeling the next ticket: a Dev slot is idle if its worktree is on `team/<name>` rather than a ticket branch, or equivalently if no open PR's branch has `<name>` as its second `/`-separated segment (`gh pr list --state open --json headRefName --jq '[.[] | select((.headRefName | split("/"))[1] == "<name>")]'` — the type prefix in front varies, don't assume `feat`). Only label a new `ready-for-development` ticket once a lane is actually free to pick it up — Ash and Jay each work one ticket at a time. When deciding what to label next: check the active roadmap-planning memory (memory index, not a hardcoded filename) for phase order and any noted dependencies, then cross-reference `git worktree list` / `gh pr list --state open` to see which phase-fronts are already claimed. Before labeling a ticket to run *in parallel* with other in-flight work, check whether it (or the roadmap memory) flags it as cross-cutting or conflict-risk (e.g. a rewrite touching most/all tool files) — that's what the `solo-only` GitHub label means: apply it to the issue, and while it's the only thing labeled `ready-for-development`, don't add any other ticket's `ready-for-development` label alongside it, no matter how many lanes are idle. The label pairing (not lane-count discipline) is the actual control — `/next-issue` only ever sees what's labeled, so an unlabeled ticket can't be grabbed regardless of how many lanes ask. Re-label the held-back tickets `ready-for-development` once the `solo-only` one ships.
|
|
15
|
+
- Roadmap grooming (`docs/roadmap.md` + whichever roadmap-planning memory is currently active — check the memory index rather than assuming a specific version-numbered file)
|
|
16
|
+
- Live/manual tool testing — only possible here (or in a QA agent's own worktree, once it's checked out a branch — see `/team-member`)
|
|
17
|
+
- Merging PRs that carry the `qa-approved` label (`merge-pr`) — do not merge one that's missing it, and do not perform the review/QA yourself; send it back to the relevant QA agent instead
|
|
18
|
+
- Worktree cleanup after merge (`cleanup-worktrees`) — this must never remove the six dev-team slot worktrees, only ad hoc worker worktrees
|
|
19
|
+
- Release readiness and documentation quality are Aziz's and Amy's territory, not Kai's — when cutting a release, hand off to Aziz (`/team-member Aziz`) for the review/QA-tier/sign-off pass rather than doing it here; route doc gaps found along the way to Amy (`documentation` label) rather than fixing them inline.
|
|
20
|
+
|
|
21
|
+
Do NOT pick up ticket implementation directly in this session — hand it to a worker session via `start-worktree` (or `next-issue`, which does this automatically), or to a dev-team lane via `/team-member` if the team is active.
|
|
@@ -9,7 +9,7 @@ Review the current branch against this checklist and report the status of each i
|
|
|
9
9
|
- [ ] **Regression coverage** — were tests that touch the modified files (not just new tests) also run?
|
|
10
10
|
- [ ] **QA test cases written** — are there AI-driven test cases in `docs/qa/tests/` covering the changed behavior? This applies beyond new tools: a change to shared internal code under `src/mcp_gee_sweet/tools/` (e.g. `docs/emitter.py`, `sheets/helpers.py`) that alters what a live tool call produces needs a test case too — a unit test alone confirms the function's internal contract, not the real API output. If this is a bug fix, the test case should reproduce the regression scenario itself, not just spot-check the tool's happy path.
|
|
11
11
|
|
|
12
|
-
**Live QA execution is out of scope for this checklist.** If this session is a worker in a `.claude/worktrees/*` checkout, running live QA tools here would exercise the main checkout's code, not this branch's changes — the result would look real but prove nothing. Do not run live tests and do not write `**Result**` entries from a worktree. That happens in the orchestrator's `/verify-pr` pass
|
|
12
|
+
**Live QA execution is out of scope for this checklist.** If this session is a worker in a `.claude/worktrees/*` checkout, running live QA tools here would exercise the main checkout's code, not this branch's changes — the result would look real but prove nothing. Do not run live tests and do not write `**Result**` entries from a worktree. That happens after the PR is open, in a place where the branch's actual code is reachable by live MCP tools: the orchestrator's `/verify-pr` pass in the main checkout for a plain worker PR, or the paired QA agent's pass (`/team-member Sky`/`/team-member Kit`) in its own dedicated worktree for a dev-team lane PR. Leave the `**Result**` line off new/changed test cases entirely — don't stub it as "pending," just omit it so that pass adds the first real one.
|
|
13
13
|
|
|
14
14
|
## 2. QA test case tags
|
|
15
15
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Bootstrap this session as a named member of the dev team. Takes the name as an argument (e.g. `/team-member Ash`). Only ever used by an agent spawned from within the `make claude-team` session — the top-level session itself is always Kai (Orchestrator) and uses `/orchestrator` instead.
|
|
2
|
+
|
|
3
|
+
## Roles
|
|
4
|
+
|
|
5
|
+
| Name | Role | Lane | Worktree | Partner |
|
|
6
|
+
|---|---|---|---|---|
|
|
7
|
+
| Ash | Dev | A | `.claude/worktrees/ash` | Sky |
|
|
8
|
+
| Sky | QA | A | `.claude/worktrees/sky` | Ash |
|
|
9
|
+
| Jay | Dev | B | `.claude/worktrees/jay` | Kit |
|
|
10
|
+
| Kit | QA | B | `.claude/worktrees/kit` | Jay |
|
|
11
|
+
| Aziz | Release QA lead | — | `.claude/worktrees/aziz` | — |
|
|
12
|
+
| Amy | Tech writer | — | `.claude/worktrees/amy` | — |
|
|
13
|
+
|
|
14
|
+
Aziz and Amy aren't lane-paired — they operate at release cadence, not per-ticket, so they have no `Partner` and no dedicated MCP server (see §2).
|
|
15
|
+
|
|
16
|
+
If the argument doesn't match one of these six (case-insensitively), stop and say so — `Kai` is the top-level session itself and has no `/team-member` entry.
|
|
17
|
+
|
|
18
|
+
## 1. Isolate into the slot
|
|
19
|
+
|
|
20
|
+
`EnterWorktree` with `path` set to that name's worktree from the table (it already exists — `make claude-team` provisions all six before launch via `scripts/setup_team.sh` — so this always re-enters, never creates). If `EnterWorktree` reports the path doesn't exist, stop and tell the user to run `make claude-team` first.
|
|
21
|
+
|
|
22
|
+
## 2. State the tool boundary
|
|
23
|
+
|
|
24
|
+
Every role's MCP servers are connected in this session because `scripts/setup_team.sh` copies `.claude/mcp-configs/team.mcp.json` to a `.mcp.json` in the repo root and in each worktree — Claude Code auto-discovers a project-level `.mcp.json` per working directory. (Agent-view sessions do **not** inherit the `--mcp-config`/`--strict-mcp-config` CLI flags passed to whatever process launched `make claude-team` — confirmed live 2026-07-11, a spawned session's `/mcp` showed only the user's global `~/.claude.json` servers until the per-worktree `.mcp.json` was added. If a role's `/mcp` is ever missing the team servers, re-run `scripts/setup_team.sh` and have that session run `/mcp` again — restarting the session may be required to pick up a newly-added project `.mcp.json`.) Say plainly: this session must only call tools prefixed `mcp__mcp-gee-sweet-<name>__` (lowercase name) — and `mcp__playwright__` if QA, respecting the filesystem-mutex protocol in `docs/qa/run.md` §"Coordinating Playwright across parallel shards" since the other QA lane may be using it too. Never call another role's `mcp__mcp-gee-sweet-<other>__` tools even though they're visible in this session's tool list.
|
|
25
|
+
|
|
26
|
+
**Aziz and Amy are the exception to "own prefix only."** Neither gets a dedicated `mcp-gee-sweet-aziz`/`mcp-gee-sweet-amy` server — `scripts/setup_team.sh` still copies them the full `.mcp.json`, so every server is visible in their tool list, but they have no server of their own to call directly. Amy generally shouldn't need one (docs work is filesystem-only). Aziz's whole job requires calling other roles' tools during a release QA pass — see §5 for when that's allowed and how it stays safe (borrowing only while the dev team is idle, delegating the actual calls to subagents rather than driving them directly).
|
|
27
|
+
|
|
28
|
+
## 3. Dev role (Ash / Jay)
|
|
29
|
+
|
|
30
|
+
Check the current branch in this worktree (`git branch --show-current`):
|
|
31
|
+
|
|
32
|
+
Branch naming for this slot always puts `<name>` as the **second** `/`-separated segment — `<type>/<name>/issue-<n>` — where `<type>` is whatever this repo's existing convention calls for given the ticket's nature (`feat`, `fix`, `chore`, `docs`, ...; see recent branch names for precedent). It is *not* always `feat` — don't assume that prefix anywhere, including when matching branches below; match on segment position, not a literal `feat/` string.
|
|
33
|
+
|
|
34
|
+
- **On `team/<name>` (idle):** this slot is free. Get the queue: `gh issue list --repo khuisman/mcp-gee-sweet --label ready-for-development --state open --json number,title,body,labels --jq 'sort_by([(.labels | map(.name | select(startswith("v"))) | sort | first // "v9.9"), .number])'`. Filter out any issue number already claimed by an *open* PR anywhere in the repo (so Ash and Jay never grab the same ticket): `gh pr list --state open --json headRefName --jq '[.[].headRefName]'` and drop any issue whose number appears in one of those branch names. Take the lowest-numbered remaining issue, confirm it with the user, then:
|
|
35
|
+
1. Pick `<type>` to match the ticket (feature vs. fix vs. chore/docs), then `git fetch origin develop && git checkout -b <type>/<name>/issue-<n> origin/develop`.
|
|
36
|
+
2. Work through the issue fully — code, tests, doc changes.
|
|
37
|
+
3. Run the test suite.
|
|
38
|
+
4. Run `/prep-for-pr`.
|
|
39
|
+
5. Commit and push (with confirmation, per this repo's normal rule), open a PR referencing the issue (`Closes #<n>`) targeting `develop`.
|
|
40
|
+
6. Remove the `ready-for-development` label once the PR is open.
|
|
41
|
+
7. Report the PR URL. Leave the branch checked out — don't return to `team/<name>` until the ticket is fully merged (see `/merge-pr`'s team-slot reset step).
|
|
42
|
+
- **On any other branch (mid-ticket):** report status — whether a PR is already open, what CI/QA said if anything, and whether it's waiting on QA or on further work — rather than claiming a new ticket.
|
|
43
|
+
|
|
44
|
+
## 4. QA role (Sky / Kit)
|
|
45
|
+
|
|
46
|
+
Find the partner Dev's open PR by matching the branch's second `/`-separated segment against the partner's lowercase name — not a `feat/`-specific prefix, since the Dev's branch type varies: `gh pr list --state open --json number,headRefName,url --jq '[.[] | select((.headRefName | split("/"))[1] == "<partner>")]'`. At most one is expected, since a lane only has one ticket in flight at a time.
|
|
47
|
+
|
|
48
|
+
- **None found:** report that Lane {A/B} has nothing to verify right now.
|
|
49
|
+
- **One found:** this is now essentially `/verify-pr`'s steps 4 onward, but simpler — this worktree already IS a dedicated review space, so skip `/verify-pr`'s steps 1–3 (the main-checkout precondition and the `review/<branch>` fetch trick don't apply here):
|
|
50
|
+
1. `git fetch origin <headRefName> && git reset --hard origin/<headRefName>` in this worktree, staying on `team/<name>` (never create a branch named after the PR here — this slot's identity is `team/<name>`, permanently).
|
|
51
|
+
2. Tell the user to run `/mcp reconnect` (standalone, on its own line) so this agent's own MCP connection picks up the reset code. Wait for confirmation.
|
|
52
|
+
3. Run `/code-review` at `high` effort against `origin/develop...HEAD`.
|
|
53
|
+
4. Scope and run the live QA steps from `/verify-pr` (its steps 6–8: find touched `docs/qa/tests/` cases, cross-check tool coverage, run them live using this worktree's own `mcp-gee-sweet-<name>`-prefixed tools).
|
|
54
|
+
5. Record `**Result**` entries for every case actually run.
|
|
55
|
+
6. If anything failed: push the recorded results (`git push origin HEAD:<headRefName>`, with confirmation), comment on the PR summarizing what failed, and stop — tell the user it goes back to the Dev agent.
|
|
56
|
+
7. If everything passed: push the results, then `gh pr edit <number> --add-label qa-approved`, and report to the user that it's ready for Kai to run `/merge-pr`.
|
|
57
|
+
|
|
58
|
+
## 5. Release QA Lead (Aziz)
|
|
59
|
+
|
|
60
|
+
Aziz doesn't work a ticket queue — he runs at release cadence: review everything going into a release, decide how much live testing it needs, run that testing, and sign off.
|
|
61
|
+
|
|
62
|
+
**Precondition — the dev team must be idle.** Aziz borrows Sky's and Kit's worktrees and MCP tool prefixes to execute live QA (see below), so Ash/Sky/Jay/Kit must not be mid-ticket when a release pass starts. Check both lanes: `gh pr list --state open --json headRefName --jq '[.[].headRefName]'` — if any branch's second `/`-segment is `ash`, `sky`, `jay`, or `kit`, that lane is active. Also check `git -C .claude/worktrees/ash branch --show-current` and the same for `jay` — either off `team/<name>` means mid-ticket even before a PR exists. If anything is active, stop and tell the user which lane, rather than borrowing a worktree out from under live Dev/QA work.
|
|
63
|
+
|
|
64
|
+
1. **Review the release.** Enumerate everything since the last stable tag: `git log v<last-stable>..origin/develop --oneline` and the merged PRs (`gh pr list --state merged --search "merged:>=<last-stable-date>"`). For each: confirm the ticket's acceptance criteria were actually met, skim the diff for anything that reads unfinished, and check whether touched features have matching doc updates (README, `docs/qa/tests/*.md` coverage for new tools, CHANGELOG if this repo keeps one).
|
|
65
|
+
2. **Decide the QA tier.** Follow the scoped-gating process in `docs/qa/runs/README.md` §"Scoped gating" exactly — enumerate commits, classify behavior-change vs. pure-refactor, map to domains, decide structural vs. non-structural per domain. Default to Full Regression; only substitute Smoke + targeted Domain runs if the audit is clean. Document the audit itself in the eventual `docs/qa/runs/vX.Y.Z.md` — this is Aziz's call to make, not something to defer to the user.
|
|
66
|
+
3. **Prep the borrowed worktrees.** In `.claude/worktrees/sky` and `.claude/worktrees/kit`: `git fetch origin develop && git reset --hard origin/develop` (they may be stale from the last PR either verified). Tell the user to have each of those sessions run `/mcp reconnect` if they're live, or note that a fresh `mcp-gee-sweet-sky`/`mcp-gee-sweet-kit` connection will pick up the reset code on next call.
|
|
67
|
+
4. **Shard and spawn.** Split the required suites across `Agent`-tool subagents (not Agent-View spawns — those don't inherit this session's already-connected MCP servers; true subagents do), one per domain, following the v0.8.1 precedent of parallel domain-sharded execution. Each subagent's prompt must specify: which `docs/qa/tests/<domain>.md` file and which TCs, which slot prefix to call tools through (`mcp-gee-sweet-sky` or `mcp-gee-sweet-kit` — split so no two subagents share a prefix concurrently), the fixture scope it owns if sharing live data with another shard, and — critically — that it must **not** edit any tracked file itself. A subagent's job is to run the live calls and report back a structured PASS/FAIL/SKIP list with what it actually observed; only Aziz writes to the repo, so results from worktrees on two different branches never need reconciling as competing diffs.
|
|
68
|
+
5. **Playwright shards** follow the existing mutex protocol in `docs/qa/run.md` §"Coordinating Playwright across parallel shards" (mkdir-based lock at `/tmp/mcp-gee-sweet-playwright.lock`, backoff-retry, ~120s staleness) — tell subagents that use `mcp__playwright__*` tools to respect it since Sky's and Kit's shards may both need it.
|
|
69
|
+
6. **No fabricated results.** Only record a `**Result**` for a case a subagent actually ran and reported observing; anything blocked gets `**Result** pending — <reason>`, never a guessed outcome.
|
|
70
|
+
7. **Repeat live verification** for any subagent-reported bug or fix-recheck that involves ordering, batching, or inheritance semantics rather than pure deterministic logic — one green pass isn't sufficient evidence for that class of behavior; have the subagent (or a follow-up one) repeat the cycle 2-3 times varying conditions before trusting it.
|
|
71
|
+
8. **Compile.** Once all shards report back, Aziz — working in his own worktree (`.claude/worktrees/aziz`), never Sky's or Kit's — writes the inline `**Result (date) ✅/❌**` entries into `docs/qa/tests/*.md`, the aggregate `docs/qa/results/<date>.md`, and the release sign-off `docs/qa/runs/vX.Y.Z.md` (including the scoped-gating audit from step 2). Any failures found get filed as issues or routed back to the responsible Dev lane; don't sign off a release with an open failure.
|
|
72
|
+
9. **Sign off.** Once every required suite is checked off and documentation review (step 1) is clean, commit the QA docs, push, open a PR, and report to the user that the release is ready to tag.
|
|
73
|
+
|
|
74
|
+
For a **minor/dev release** that only needs Smoke: same flow, but step 2's audit will usually conclude Smoke alone suffices — Aziz still runs it live and records real results, just against a smaller case set.
|
|
75
|
+
|
|
76
|
+
## 6. Tech Writer (Amy)
|
|
77
|
+
|
|
78
|
+
Amy keeps documentation accurate, friendly, comprehensive, and fun — README, `docs/`, tool docstrings surfaced to users, and QA test docs' clarity (not their results).
|
|
79
|
+
|
|
80
|
+
- **On `team/amy` (idle):** claim work the same way Ash/Jay do, but from the `documentation` label instead of `ready-for-development`: `gh issue list --repo khuisman/mcp-gee-sweet --label documentation --state open --json number,title,body --jq 'sort_by(.number)'`. Filter out issues already claimed by an open PR the same way Dev roles do. If the queue is empty, that's fine — ask the user whether there's a specific doc gap to work from instead (e.g. a just-merged feature whose docs lag behind, spotted during Aziz's release review).
|
|
81
|
+
1. `git fetch origin develop && git checkout -b docs/amy/issue-<n> origin/develop`.
|
|
82
|
+
2. Do the writing: fix inaccuracies, tighten voice, fill gaps, add examples. Read the actual code/tool behavior rather than trusting an existing doc's claims — a doc bug is exactly the kind of drift Amy exists to catch.
|
|
83
|
+
3. If a change touches user-facing tool behavior described in `docs/qa/tests/`, flag it for Aziz rather than editing test *results* herself — Amy owns doc accuracy, not QA sign-off.
|
|
84
|
+
4. Run `/prep-for-pr`, commit and push (with confirmation), open a PR referencing the issue (`Closes #<n>`) targeting `develop`.
|
|
85
|
+
5. Remove the `documentation` label once the PR is open.
|
|
86
|
+
- **On any other branch (mid-ticket):** report status rather than claiming new work.
|
|
87
|
+
|
|
88
|
+
Amy has no dedicated MCP server and doesn't need QA-approved sign-off to merge (docs-only PRs aren't gated by `qa-approved`) — normal `/merge-pr` review still applies.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
Verify an open worker PR before merging: code review plus live QA testing against real Google APIs. This only works from the orchestrator session in the main checkout — see `/orchestrator` for why (live MCP tools always serve the main checkout's code, never a worktree's).
|
|
2
2
|
|
|
3
|
+
**If the dev-team is active** (a PR whose branch has `ash` or `jay` as its second `/`-separated segment — the type prefix in front varies, don't assume `feat`), use `/team-member Sky` or `/team-member Kit` instead — those roles have their own dedicated worktree, so the main-checkout precondition in step 1 and the `review/<branch>` fetch trick in step 3 don't apply there; QA is steps 4 onward of this file, run directly from that worktree. This file remains the path for a plain worker PR with no dev-team lane behind it.
|
|
4
|
+
|
|
3
5
|
CI (the "Lint and test" check `/merge-pr` already gates on) covers unit tests. It cannot cover code review judgment or live calls against real Google APIs, since those need credentials and fixtures CI doesn't have. That's what this skill does instead — it's the step between a worker's PR going up and `/merge-pr`.
|
|
4
6
|
|
|
5
7
|
1. **Preconditions.** Confirm this session is in the main checkout, not a worktree (same check as `/orchestrator` step 1). If the main checkout has uncommitted changes, stop and ask the user how to proceed — don't stash or discard anything.
|
|
@@ -42,6 +44,6 @@ CI (the "Lint and test" check `/merge-pr` already gates on) covers unit tests. I
|
|
|
42
44
|
```
|
|
43
45
|
Ask before pushing, per this repo's normal commit/push confirmation rule. Leave the scratch worktree in place afterward (`ExitWorktree` with `action: "keep"`) for next time — don't remove it between PRs, only if the user asks for a cleanup pass.
|
|
44
46
|
|
|
45
|
-
10. **Report.** Summarize: code review findings (if any), which test cases passed/failed/pending, any coverage gaps found in step 6, and whether the branch looks ready to merge. If everything passed, say the next step is `/merge-pr`. If something failed, say the PR needs to go back to its worker session — don't attempt the fix yourself here; this session's job is verification, not implementation.
|
|
47
|
+
10. **Report.** Summarize: code review findings (if any), which test cases passed/failed/pending, any coverage gaps found in step 6, and whether the branch looks ready to merge. If everything passed, apply the sign-off label (`gh pr edit <number> --add-label qa-approved`) and say the next step is `/merge-pr`. If something failed, say the PR needs to go back to its worker session — don't attempt the fix yourself here; this session's job is verification, not implementation.
|
|
46
48
|
|
|
47
49
|
Leave the branch checked out either way — `/merge-pr` operates on the current branch's PR, and if it needs rework, the worker's own worktree still has the branch open there too.
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
fetch-depth: 0 # full history so uv-dynamic-versioning can read tags
|
|
24
24
|
|
|
25
25
|
- name: Install uv
|
|
26
|
-
uses: astral-sh/setup-uv@v8.3.
|
|
26
|
+
uses: astral-sh/setup-uv@v8.3.2
|
|
27
27
|
|
|
28
28
|
- name: Set up Python 3.12
|
|
29
29
|
run: uv python install 3.12
|
|
@@ -45,7 +45,7 @@ jobs:
|
|
|
45
45
|
fetch-depth: 0 # full history so uv-dynamic-versioning can read tags
|
|
46
46
|
|
|
47
47
|
- name: Install uv
|
|
48
|
-
uses: astral-sh/setup-uv@v8.3.
|
|
48
|
+
uses: astral-sh/setup-uv@v8.3.2
|
|
49
49
|
|
|
50
50
|
- name: Set up Python 3.12
|
|
51
51
|
run: uv python install 3.12
|
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
- uses: actions/checkout@v7
|
|
16
16
|
|
|
17
17
|
- name: Install uv
|
|
18
|
-
uses: astral-sh/setup-uv@v8.3.
|
|
18
|
+
uses: astral-sh/setup-uv@v8.3.2
|
|
19
19
|
|
|
20
20
|
- name: Set up Python 3.12
|
|
21
21
|
run: uv python install 3.12
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
fetch-depth: 0
|
|
37
37
|
|
|
38
38
|
- name: Install uv
|
|
39
|
-
uses: astral-sh/setup-uv@v8.3.
|
|
39
|
+
uses: astral-sh/setup-uv@v8.3.2
|
|
40
40
|
|
|
41
41
|
- name: Set up Python 3.12
|
|
42
42
|
run: uv python install 3.12
|
|
@@ -70,6 +70,8 @@ Set `AUTH_METHOD` to pin a specific method with no fallback — removes ambiguit
|
|
|
70
70
|
|
|
71
71
|
**Tool registration** (`tool` decorator wrapper): A custom `@tool()` decorator wraps `@mcp.tool()`. It checks `ENABLED_TOOLS` (set via `ENABLED_TOOLS` env var or `--include-tools` CLI arg) and skips registration for any tool not in the allowlist. This is how tool filtering works — tools simply aren't registered with FastMCP rather than being conditionally hidden.
|
|
72
72
|
|
|
73
|
+
**Async execution** (`execute_in_thread` + `thread_http`, in `http_transport.py`): every `@tool()`-registered function is `async def`, and `_timed`'s wrapper is unconditionally `async def wrapper(...): return await func(...)` — so **any new tool must be `async def`**, even one with no Google API calls (see `tools/cache.py`'s `get_cache_ttl`/`set_cache_ttl`/`refresh_cache` for an example with an empty `await`-free body; a plain `def` there raises `TypeError` at call time since `await`ing a sync return value fails — `test_server.py::TestAllToolsAreAsync` guards against this recurring). Every `.execute()` call is wrapped `await execute_in_thread(chain.execute, service)` (`http_transport.py`, re-exported from `auth.py` for existing imports) — **do not** write `asyncio.to_thread(chain.execute, http=thread_http(service))` directly: `thread_http(service)` as an eagerly-evaluated kwarg resolves on the event-loop thread before the worker thread starts, so concurrent calls end up sharing one transport across threads (a real, live-reproduced bug, fixed in #183's follow-up round — see the decision doc). `execute_in_thread` defers the `thread_http()` call into the thread itself via a lambda. `thread_http()` hands each worker thread its own `AuthorizedHttp` transport built from the shared credentials, since the lifespan-scoped `sheets_service`/`drive_service`/etc. objects each carry one `httplib2` transport that isn't safe for concurrent use across threads. Six multi-call tools (`sync_folder`, `get_multiple_sheet_data`, `get_multiple_spreadsheet_summary`, `import_csv_to_sheet`, `share_spreadsheet`, `share_file`) parallelize their per-item work via `asyncio.gather(..., return_exceptions=True)` instead of looping serially. `cache.py`'s SQLite I/O deliberately stays synchronous, not `to_thread`-wrapped — see the comment in `cache.py::_open()`; the module's two Google-API-calling helpers (`get_modified_time`, `fetch_sheets`) are `async def` and do use `execute_in_thread` for their own `.execute()` calls. `_BaseCache.snapshot_epoch()`/`store(..., epoch=...)` guard against a `refresh_cache()` call landing mid-fetch and having its invalidation silently overwritten — only wired up for `SheetStructureCache`/`SheetDataCache` so far. Full rationale: `docs/decisions/decision-async-tool-execution.md`.
|
|
74
|
+
|
|
73
75
|
**Two Google API clients**: The server builds both a Sheets client (`sheets/v4`) and a Drive client (`drive/v3`). Most tools use only the Sheets client; `list_spreadsheets`, `create_spreadsheet`, `share_spreadsheet`, and `list_folders` use Drive. `create_spreadsheet` uses Drive to create the file (so it can place it in a folder), not the Sheets API.
|
|
74
76
|
|
|
75
77
|
**A1 notation helpers**: `_parse_a1_notation`, `_column_index_to_letter`, and `_letter_to_column_index` convert between A1 ranges and the 0-based row/column indices the Sheets batchUpdate API requires. The Sheets values API uses A1 notation directly; batchUpdate requires numeric indices — keep that distinction in mind when adding new tools.
|
|
@@ -68,6 +68,14 @@ docs: ## Serve docs locally at http://127.0.0.1:8000 (live reload).
|
|
|
68
68
|
install-hooks: ## Install pre-commit hooks into the local git repo.
|
|
69
69
|
uv run pre-commit install
|
|
70
70
|
|
|
71
|
+
.PHONY: setup-team
|
|
72
|
+
setup-team: ## Idempotently provision/refresh dev-team worktree slots and MCP config, without launching Claude.
|
|
73
|
+
scripts/setup_team.sh
|
|
74
|
+
|
|
75
|
+
.PHONY: claude-team
|
|
76
|
+
claude-team: setup-team ## Launch Claude Code with all dev-team MCP servers connected (Kai/Ash/Sky/Jay/Kit/Aziz/Amy) for Agent View.
|
|
77
|
+
claude --mcp-config .claude/mcp-configs/team.mcp.json --strict-mcp-config
|
|
78
|
+
|
|
71
79
|
.PHONY: test
|
|
72
80
|
test: ## Run unit tests.
|
|
73
81
|
uv run python -m pytest
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-gee-sweet
|
|
3
|
-
Version: 0.7.0.
|
|
3
|
+
Version: 0.7.0.dev93
|
|
4
4
|
Summary: MCP server for Google Workspace — Sheets, Drive, Docs, and Calendar. 60+ tools for AI clients.
|
|
5
5
|
Project-URL: Homepage, https://github.com/khuisman/mcp-gee-sweet
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/khuisman/mcp-gee-sweet/issues
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Decision: Async Tool Execution (issue #183)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-12
|
|
4
|
+
**Snapshot commit:** branch `feat/ash/issue-183`
|
|
5
|
+
|
|
6
|
+
## Background
|
|
7
|
+
|
|
8
|
+
Every tool in this server made Google API calls synchronously and sequentially, even when a single tool logically does several independent calls — syncing N files, fetching N sheets, sharing with N recipients. FastMCP supports async tool handlers natively, so the fix is to convert the tool layer to `async def`, push each blocking `.execute()` call onto a worker thread via `asyncio.to_thread()`, and use `asyncio.gather()` to run genuinely independent calls within one tool concurrently instead of one at a time.
|
|
9
|
+
|
|
10
|
+
This touches all 13 tool files (`~144` `.execute()` call sites) plus two non-`@tool` helper chains that tool functions call into: `sheets/helpers.py` (`_get_sheet_id`, `_get_sheet_index`) and `docs/emitter.py` (`fill_tables` and its recursive fill chain).
|
|
11
|
+
|
|
12
|
+
## Decisions
|
|
13
|
+
|
|
14
|
+
### 1. Shared-transport thread-safety fix (new scope, found during design review)
|
|
15
|
+
|
|
16
|
+
Direct inspection of `mcp`'s `func_metadata.call_fn_with_arg_validation` confirmed sync tool handlers previously ran straight on the event loop thread — the server was effectively single-threaded for tool execution, so there was no existing thread-safety exposure. Converting to `to_thread`/`gather` changes that: multiple `.execute()` calls can now genuinely run in parallel OS threads against the *same* `sheets_service`/`drive_service`/etc. objects that `auth.py` builds once at startup and reuses forever. `google-api-python-client`'s default `httplib2`-backed transport is not safe for concurrent use of one instance — a new risk introduced by this change, not a pre-existing one, and in scope to fix here.
|
|
17
|
+
|
|
18
|
+
**The mechanism:** `googleapiclient.http.HttpRequest.execute()` accepts an `http=` override — no need to rebuild whole service objects per thread, just swap the transport at the call site. `auth.py` gained a `thread_http(service)` helper: a `threading.local()`-cached `AuthorizedHttp` per (thread, service), built from the credentials already attached to that service's `Resource` object (`service._http.credentials`, set by `googleapiclient.discovery.build()`). Standard call sites became `await asyncio.to_thread(chain.execute, http=thread_http(service))`.
|
|
19
|
+
|
|
20
|
+
`MediaIoBaseDownload.next_chunk()` doesn't accept an `http=` override (it reads `self._request.http` internally), so the three/four chunked-download sites (`export_file`, `download_file`, `download_folder`, `sync_folder`) instead set `request.http = thread_http(drive_service)` before starting the chunked loop, then wrap the whole loop in one `to_thread` call.
|
|
21
|
+
|
|
22
|
+
### 2. `cache.py` (SQLite) stays fully synchronous
|
|
23
|
+
|
|
24
|
+
Deliberate, not an oversight: only the actual Google API `.execute()` call inside a cache-touching helper gets `to_thread`-wrapped; the sync `cache.get()`/`cache.store()` calls stay on the single event-loop thread, interleaved between `await`s. Since only one coroutine runs on the event loop at a time (`to_thread` only offloads the specific blocking call it wraps), concurrent SQLite access never actually occurs even under `gather()` — no lock needed. A comment at the top of `cache.py`'s `_open()` spells this out so a future edit doesn't "fix" it by adding `to_thread`/locks that would actually introduce the race they're meant to prevent.
|
|
25
|
+
|
|
26
|
+
### 3. `gather()` restructuring — six tools, uniform pattern
|
|
27
|
+
|
|
28
|
+
Named in the issue: `sync_folder`, `get_multiple_sheet_data`, `get_multiple_spreadsheet_summary`. Extended per the issue's own "any tool that currently loops over API calls serially" scope to three more found during design review: `import_csv_to_sheet` (chunked row writes to disjoint ranges), `share_spreadsheet`, `share_file` (per-recipient/per-permission loops).
|
|
29
|
+
|
|
30
|
+
**`batch_update_cells` scope correction:** the issue named it as a parallelization target, but it has no loop over API calls — it's a single already-batched `.execute()` call (the loop present is a pure Python list comprehension building the request body, no I/O). Gave it the standard async/`to_thread` conversion only; there was nothing to `gather()`.
|
|
31
|
+
|
|
32
|
+
Shared pattern across all six: extract per-item work into an inner `async def`, gather with `return_exceptions=True`, reduce in one pass afterward. `return_exceptions=True` matters even though each per-item helper already catches its own errors and returns a tagged result — it also lets every in-flight item finish before an unexpected exception (a genuine bug, not the expected per-item failure path) surfaces, instead of orphaning still-running tasks. `gather()` preserves the order of the awaitables list, so `get_multiple_sheet_data`/`get_multiple_spreadsheet_summary` need no reordering. `share_spreadsheet`/`share_file`/`sync_folder` bucket results by outcome (successes/failures, uploaded/downloaded/failed) — relative order *within* a bucket now reflects completion order, not input order, since each item still carries its own identity (email, filename) in its result. `sync_folder`'s reduce pass also sums `total_bytes` and populates five separate lists from one tagged-result stream, avoiding concurrent mutation of shared accumulators entirely (only the single-threaded reduce pass after `gather()` touches them).
|
|
33
|
+
|
|
34
|
+
`asyncio.to_thread`'s default executor caps at `min(32, os.cpu_count() + 4)` workers, shared process-wide — a batch larger than that queues rather than fully parallelizing. Timing stays accurate, just with diminishing returns past the cap; noted at each `gather()` site so it isn't mistaken for a bug later.
|
|
35
|
+
|
|
36
|
+
### 4. Test infrastructure: `pytest-asyncio`, `asyncio_mode = "auto"`
|
|
37
|
+
|
|
38
|
+
Confirmed via user decision over two alternatives (anyio pytest plugin, manual `asyncio.run()` wrapper per test). `asyncio_mode = "auto"` means converting a test is purely mechanical — `def test_x():` → `async def test_x():` plus `await` at each call site — no `@pytest.mark.asyncio` decorators needed anywhere. `MagicMock`/`Mock`-based service chains needed no changes; `asyncio.to_thread(mock_chain.execute, http=...)` runs the sync mock callable in a worker thread and awaits the result normally — the mocks don't know or care about the extra `http=` kwarg.
|
|
39
|
+
|
|
40
|
+
### 5. A bug the mocked test suite structurally couldn't catch
|
|
41
|
+
|
|
42
|
+
`_timed`'s wrapper (`server.py`) became unconditionally `async def ...: return await func(...)`. Any tool registered through `tool()` that was *not* itself converted to `async def` would break at call time — `await` on a plain sync return value raises `TypeError`. `tools/cache.py`'s three tools (`get_cache_ttl`, `set_cache_ttl`, `refresh_cache`) have zero `.execute()` calls, so they were correctly left out of the `.execute()`-driven conversion sweep — but that left them as plain `def` under an `async def`-only wrapper. Every existing unit test captures the raw inner function via a fake tool registry, bypassing `_timed` entirely, so `pytest -q` stayed green throughout even with this broken. Only caught by importing `server.py` directly and checking `inspect.iscoroutinefunction()` on every registered tool's unwrapped inner function — a check worth keeping in mind for any future addition of a tool with no I/O of its own. Fixed by converting all three to `async def` (trivial — no `await` needed inside, they just satisfy the wrapper's calling convention).
|
|
43
|
+
|
|
44
|
+
## Verification
|
|
45
|
+
|
|
46
|
+
- Full unit suite (665 tests) green; `ruff check` clean; zero unwrapped `.execute()` calls remaining (`grep -rn "\.execute()" src/mcp_gee_sweet/tools/`).
|
|
47
|
+
- Manual smoke test: 5 concurrent `sync_folder` uploads (each artificially delayed 0.2s in the mock) completed in ~0.22s total, not the ~1.0s a serial implementation would take — confirms real concurrency, not just non-blocking syntax.
|
|
48
|
+
- Live QA: see `docs/qa/tests/infra.md` TC-I24 and the six new per-tool cases (`docs/qa/tests/drive.md` TC-D176–179, `docs/qa/tests/sheets_read.md` TC-R36–37) — each forces enough distinct, identifiable concurrent items to catch cross-item attribution corruption under the real Drive/Sheets transport, which no mock can exercise.
|
|
49
|
+
|
|
50
|
+
## Round 2 — QA findings from PR #293 review (2026-07-13)
|
|
51
|
+
|
|
52
|
+
QA (`/code-review --effort high` + live testing) sent the PR back with one live-reproduced blocking bug (TC-R36) and 8 further code-review findings. All 10 are fixed here.
|
|
53
|
+
|
|
54
|
+
### 6. The actual `thread_http()` bug: eager kwarg evaluation, not per-thread isolation
|
|
55
|
+
|
|
56
|
+
The mechanism described in §1 was correct in intent but broken in every call site: `await asyncio.to_thread(chain.execute, http=thread_http(service))` evaluates `thread_http(service)` as a keyword argument *before* `to_thread` schedules the worker thread — Python evaluates all arguments to a call before making the call. So `thread_http()` always ran on the event-loop thread, and every concurrently-gathered call for a given service resolved the *same* cached transport, which then got used by N worker threads simultaneously — the exact hazard §1 was meant to prevent. Live-reproduced as intermittent SSL/connection errors under real concurrent load (`docs/qa/tests/sheets_read.md` TC-R36).
|
|
57
|
+
|
|
58
|
+
**Fix:** `http_transport.py` (new module, extracted from `auth.py` to avoid a circular import with `cache.py` — see #9 below) gained `execute_in_thread(execute_fn, service)`:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
async def execute_in_thread(execute_fn: Any, service: Any) -> Any:
|
|
62
|
+
return await asyncio.to_thread(lambda: execute_fn(http=thread_http(service)))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`execute_fn` is the *bound but not yet called* `.execute` method (evaluating it is just attribute access — harmless on the event-loop thread); `thread_http(service)` is now called *inside* the lambda `to_thread` runs on the worker thread. All 141 standard call sites converted from `asyncio.to_thread(chain.execute, http=thread_http(service))` to `execute_in_thread(chain.execute, service)` via a verified 1:1 mechanical transform (every site's preceding line ended in `.execute,` and following line was a bare `)`, confirmed by scripted audit before transforming — no ambiguous block boundaries, unlike the regex-corruption incidents from round 1). One non-standard site (`export_revision`'s raw `.request()` call) fixed by hand with the same lambda-deferral pattern.
|
|
66
|
+
|
|
67
|
+
### 7. `get_spreadsheet_info` MCP resource handler was still fully synchronous
|
|
68
|
+
|
|
69
|
+
`server.py`'s `@mcp.resource("spreadsheet://{spreadsheet_id}/info")` handler predates the tool-layer conversion and was out of scope per the original plan ("tool files," not `@mcp.resource` handlers) — but that left one synchronous `.execute()` call blocking the event loop during a live API request, undermining the point of the whole conversion for that one code path. Converted to `async def`, wrapped via `execute_in_thread`.
|
|
70
|
+
|
|
71
|
+
### 8. `get_modified_time()`/`fetch_sheets()` in `cache.py` issued unwrapped blocking Google API calls
|
|
72
|
+
|
|
73
|
+
Both are cache-touching helpers, not `@tool()`-registered functions, so they were missed by the tool-file-scoped conversion sweep despite being called from async tool code (`sheets/data.py`, `sheets/helpers.py`, `sheets/structure.py`) — every call blocked the event loop for the duration of the underlying Drive/Sheets API request. Converted both to `async def`, wrapped their `.execute()` calls via `execute_in_thread`, and awaited all call sites. This is *not* the same decision as "`cache.py`'s SQLite I/O stays synchronous" (§2) — that's about the sync `cache.get()`/`store()` calls, which correctly stay unwrapped; this is about the actual network call these two helpers make before touching the cache at all.
|
|
74
|
+
|
|
75
|
+
### 9. Extracted `thread_http`/`execute_in_thread` into `http_transport.py`
|
|
76
|
+
|
|
77
|
+
Fixing #8 requires `cache.py` to call `execute_in_thread`, but `execute_in_thread` lived in `auth.py`, which already imports from `cache.py` (`SheetStructureCache` et al.) — a circular import. Moved both functions into a new `http_transport.py` with no dependency on either module; `auth.py` re-exports them (`from .http_transport import execute_in_thread, thread_http`) so none of the 13 tool files' `from ...auth import execute_in_thread, thread_http` imports needed to change.
|
|
78
|
+
|
|
79
|
+
### 10. `share_spreadsheet`/`share_file` lost the identity of a malformed input entry
|
|
80
|
+
|
|
81
|
+
Both tools' per-item `_share_one` closures extracted fields via `.get()` *before* entering their own try/except — a non-dict entry (e.g. a bare string in the `recipients`/`permissions` list) raised `AttributeError` from that very first `.get()` call, escaping to the outer `gather(..., return_exceptions=True)` catch-all, which only records `{"email_address": None, "error": ...}` / `{"entry": None, "error": ...}` — indistinguishable from any other anonymous failure if multiple bad entries were in the same batch. Fixed by wrapping each closure's entire body (field extraction included) in its own try/except, echoing the raw input back as `entry` even when extraction itself is what failed.
|
|
82
|
+
|
|
83
|
+
### 11. `import_csv_to_sheet`'s concurrent chunk writer could leave holes mid-sheet on partial failure
|
|
84
|
+
|
|
85
|
+
Before #183, a chunk-write failure stopped the (then-sequential) loop immediately, leaving a clean truncated prefix — rows 1..N present, nothing after. Under `asyncio.gather()`, chunks write concurrently, so a later chunk can succeed while an earlier one is still failing, leaving an ambiguous *hole* rather than a clean boundary, with the old code surfacing only the first raw exception and no indication of which rows were affected. Fixed: `_write_chunk` now returns a tagged `{start_row, end_row, ok, error?}` result instead of raising; on any failure the tool returns (rather than raises) an error payload with `failed_ranges` and `written_ranges` (each with exact row boundaries) so the missing rows can be retried precisely, alongside the already-created `spreadsheetId`.
|
|
86
|
+
|
|
87
|
+
### 12. Redundant per-sheet `get_modified_time()` call in `get_multiple_spreadsheet_summary`
|
|
88
|
+
|
|
89
|
+
The per-sheet loop refetched `get_modified_time(drive_service, spreadsheet_id)` for every sheet, even though `modifiedTime` is a Drive *file-level* property — identical for every sheet tab in the same spreadsheet at a given instant. The original comment justified this as catching an edit that happened mid-request, but that race-window benefit is marginal compared to N redundant Drive API calls per spreadsheet (now each one a full `execute_in_thread` round trip). Deduplicated: `sheet_mtime` now reuses `current_mtime` captured once before the per-sheet loop.
|
|
90
|
+
|
|
91
|
+
### 13. `get_multiple_spreadsheet_summary` was missing response-cap/`local_path` parity with its sibling
|
|
92
|
+
|
|
93
|
+
`get_multiple_sheet_data` and `find_in_spreadsheet` both call `enforce_response_size_cap` and support a `local_path` bypass; `get_multiple_spreadsheet_summary` did neither, so a large enough batch could silently produce an oversized response with no safety net and no way to redirect it to disk. Added both, matching the sibling tools' contract exactly (`{local_path, spreadsheet_count, bytes_written}` manifest shape).
|
|
94
|
+
|
|
95
|
+
### 14. Local-filesystem I/O sites still ran unwrapped on the event loop
|
|
96
|
+
|
|
97
|
+
Three sites did blocking disk I/O directly in async tool bodies, with no `to_thread` wrap — harmless for small files but blocking the whole event loop for however long a large read/write takes, which specifically undermines the paths meant to handle *large* results: the CSV read in `import_csv_to_sheet` (`csv.reader` over the whole file), `write_capped_result_to_disk` (exists specifically to hold data too large for the response cap), and the single-shot `dest.write_bytes(content)` sites in `transfer.py`'s Workspace-file export paths (the chunked-download closures were already correctly wrapped). All three wrapped in `asyncio.to_thread`; `write_capped_result_to_disk` becoming `async def` required `await` at its 5 call sites (`sheets/data.py` ×4, `docs/content.py` ×1).
|
|
98
|
+
|
|
99
|
+
### 15. Cache-invalidation race: a concurrent `refresh_cache()` could be silently undone
|
|
100
|
+
|
|
101
|
+
`refresh_cache(spreadsheet_id=X)` calls `mark_dirty()`, which sets `dirty=1`. Every cache class's `store()` does `INSERT OR REPLACE ... dirty=0` unconditionally. Before #183, a tool call ran to completion on the event loop with no interleaving point, so a read-then-store sequence for a given key could never be interrupted by another call — this race genuinely could not happen. Now that reads have a real `await` (the `.execute()` call), a `refresh_cache()` call landing *during* another read's in-flight fetch for the same key has its `dirty=1` silently overwritten back to `dirty=0` once that read's `store()` runs afterward — erasing the invalidation's effect with no error or signal.
|
|
102
|
+
|
|
103
|
+
**Fix:** an in-memory (not persisted — resets are fine, a fresh process has a fresh cache anyway), per-cache-instance monotonic epoch counter, bumped on every `mark_dirty`/`mark_all_dirty`. `_BaseCache` gained `snapshot_epoch()` (call before starting a fetch) and `_store_if_fresh(sql, params, epoch)` (skip the write entirely if the epoch changed between snapshot and store — i.e. an invalidation happened mid-fetch). Deliberately coarse (one counter per cache *instance*, not per-key): a concurrent invalidation for any key blocks in-flight stores for all keys in that cache, causing occasional spurious cache-miss refetches for unrelated keys rather than a precise per-key fix — a correctness-safe tradeoff against a full per-key generation-counter schema migration, scoped to the two caches actually touched by this PR's newly-concurrent code paths (`SheetStructureCache` via `fetch_sheets`/`get_multiple_spreadsheet_summary`, `SheetDataCache` via the latter's per-sheet loop). `store()` gained an optional `epoch: int | None = None` kwarg on both classes — omitting it (as every pre-existing caller does) preserves the old unconditional-write behavior exactly, so this is purely additive.
|
|
104
|
+
|
|
105
|
+
DriveFolderCache/DocContentCache/CalendarCache's `store()` methods have the identical theoretical race (any tool call is now async, so *any* two calls can interleave, not just gather()'d ones) but weren't part of this fix — out of scope since their tools aren't part of this PR's newly-introduced concurrency and Sky's finding cited only the sheets-side call sites.
|
|
106
|
+
|
|
107
|
+
### Round 2 verification
|
|
108
|
+
|
|
109
|
+
- Full unit suite (674 tests, up from 665) green; `ruff check`/`ruff format --check` clean; zero unwrapped `.execute()` calls remain.
|
|
110
|
+
- New regression tests: `test_server.py::TestAllToolsAreAsync` (enumerates every registered tool via `mcp._tool_manager.list_tools()` and asserts `inspect.iscoroutinefunction(inspect.unwrap(tool.fn))` — the exact check that caught #5 in round 1, now automated instead of a one-off manual introspection); `test_sharing.py`'s two `test_malformed_non_dict_entry_stays_attributable` tests; `test_files.py::test_partial_chunk_failure_reports_failed_and_written_ranges`; `test_cache.py`'s `test_store_with_stale_epoch_is_skipped`/`test_store_with_current_epoch_succeeds` pairs for both `SheetStructureCache` and `SheetDataCache`.
|
|
111
|
+
- Live re-verification of TC-R36 (the concurrency bug's original reproduction) still needed from QA — not re-run live as part of this fix pass; see the "Dev note" appended to that test case in `docs/qa/tests/sheets_read.md`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Decision: Named Dev-Team Roles with Isolated Worktrees + MCP Slots (issue #290)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-11
|
|
4
|
+
**Snapshot commit:** branch `feat/dev-team`
|
|
5
|
+
|
|
6
|
+
## Background
|
|
7
|
+
|
|
8
|
+
The existing `/orchestrator` workflow puts all post-implementation work — code review, live QA against real Google APIs, and merging — on one session (the main checkout), because live MCP tool calls only ever reach whatever code is checked out at a fixed filesystem path. `/verify-pr` works around this by fetching a worker's PR branch into a locally-named `review/<branch>` in the main checkout, testing it there, then pushing results back via a refspec. That workaround exists solely because nothing else can reach the branch's code live.
|
|
9
|
+
|
|
10
|
+
The goal here is to let review/QA happen in its own dedicated, isolated worktree — in parallel, without occupying the main checkout — by giving each role its own persistent worktree and its own MCP server connection.
|
|
11
|
+
|
|
12
|
+
## Decisions
|
|
13
|
+
|
|
14
|
+
### 1. Five named roles, not generic "worker"/"reviewer" labels
|
|
15
|
+
|
|
16
|
+
Kai (Orchestrator, main checkout), and two lanes each pairing a Dev and a QA agent: Ash/Sky (Lane A), Jay/Kit (Lane B). Short (≤3 character), gender-neutral names — easier for a human to scan and track at a glance than "worker-1"/"reviewer-2", and room to add personality later for anyone who wants a friendlier feel from the team, not just a bare label. Orchestrator sheds code review and live QA to the QA role; QA absorbs review, live testing, re-review, and sign-off. A lane runs at most one ticket at a time, which is what makes PR-to-lane routing unambiguous (decision 4).
|
|
17
|
+
|
|
18
|
+
### 2. Persistent slots, not per-ticket worktrees
|
|
19
|
+
|
|
20
|
+
The existing `/start-worktree`/`/next-issue` pattern creates a fresh worktree per ticket, removed after merge. Rejected for the four team roles: recreating a worktree (and its MCP server's `--directory`) every ticket would mean regenerating the MCP config on every cycle, and the whole point of naming these roles is a small fixed set of slots a human can track by name. Instead Ash/Sky/Jay/Kit each get one worktree that lives for the project's lifetime, resting on its own `team/<name>` branch and cycling: idle (`team/<name>`, at `origin/develop`) → active (checked out to a ticket/PR branch) → idle again after merge. `develop` itself is never checked out in a team-slot worktree — it's always checked out in the main checkout, so no other worktree can share that branch name.
|
|
21
|
+
|
|
22
|
+
### 3. One combined MCP config, not one config per role
|
|
23
|
+
|
|
24
|
+
The original framing assumed launching each role as its own `claude --mcp-config <role>.json --strict-mcp-config` process. Reconsidered once the actual usage pattern was clarified: this is driven from Claude Code's Agent View — one top-level `claude` invocation, then agents spawned from within it. A spawned agent always inherits whatever MCP servers its parent connected to at *its own* start; nothing spawned later can add a new connection, and there is no way to scope an already-running session down to a subset of its servers per-subagent. So five separate strict-mode processes doesn't fit — instead `.claude/mcp-configs/team.mcp.json` defines all six server entries (four role servers, Kai's own oauth/service-account pair, one shared `playwright`) side by side, loaded once. Every spawned agent can see every role's tools; the boundary is `/team-member` instructing each agent which prefix is its own, not a process wall. Accepted as a known tradeoff — worth revisiting (e.g. per-agent permission allow-lists) only if that discipline proves leaky in practice.
|
|
25
|
+
|
|
26
|
+
### 4. Branch-name segment for lane routing, not a GitHub label or project board
|
|
27
|
+
|
|
28
|
+
Dev branches are named `<type>/<devname>/issue-<n>`, inserting the name as the second `/`-separated segment ahead of the existing `<type>/issue-<n>` convention. `<type>` deliberately stays whatever this repo's convention already calls for given the ticket (checked against recent merged branches: `feat`, `fix`, `chore`, and `docs` all appear regularly) — it is *not* always `feat`, so routing must never assume that literal prefix. A QA agent finds its paired Dev's open PR by matching branch segment position, not a fixed string: `gh pr list --state open --json headRefName --jq 'select((.headRefName | split("/"))[1] == "<partner>")'`. Considered a `lane:a`/`lane:b` GitHub label instead — rejected as an extra piece of state to keep in sync for no real benefit, since the branch name already carries the same information and a lane only ever has one ticket in flight at a time (so the lookup is never ambiguous). The new `qa-approved` label *is* still used, but for sign-off state, not routing — routing needs no state at all beyond the branch name.
|
|
29
|
+
|
|
30
|
+
### 5. `/cleanup-worktrees` must never touch the four team-slot worktrees
|
|
31
|
+
|
|
32
|
+
Its existing rule — remove any worktree whose branch has a merged PR and is clean — would otherwise delete a team-slot worktree the moment its first ticket's PR merges, since a just-merged dev-team branch (e.g. `fix/ash/issue-241`) matches exactly that rule. Two changes: `/merge-pr`'s cleanup step now resets a team-slot worktree back to `team/<name>` in place (rather than removing the worktree) as soon as its PR merges, and `/cleanup-worktrees` itself unconditionally skips the four team-slot paths regardless of branch/PR state, as a second line of defense.
|
|
33
|
+
|
|
34
|
+
## When to Re-evaluate
|
|
35
|
+
|
|
36
|
+
- If the shared-MCP-config tool boundary (decision 3) turns out to be leaky in practice (an agent calling another role's tools by mistake), look at per-agent permission allow-lists rather than reintroducing per-role processes — Agent View's constraints haven't changed, so separate processes still won't fit the workflow.
|
|
37
|
+
- If a third lane is ever added, the branch-prefix routing (decision 4) and the role table in `/team-member` both need a new row — there's no dynamic registry, this is a small hardcoded set by design.
|
|
38
|
+
- If dev-team ticket volume grows past what two lanes can absorb, revisit whether Kai's own ticket-triage lane-assignment logic (in `/orchestrator`) still scales as a manual check, or needs to become closer to a real scheduler.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Decision: Release QA Lead and Tech Writer Roles (issue #295)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-12
|
|
4
|
+
**Snapshot commit:** branch `feat/dev-team-2/issue-295`
|
|
5
|
+
|
|
6
|
+
## Background
|
|
7
|
+
|
|
8
|
+
[Dev-Team Roles](decision-dev-team.md) gave Ash/Sky (Lane A) and Jay/Kit (Lane B) persistent worktrees for per-ticket Dev/QA work. Two responsibilities don't fit that per-ticket shape: reviewing an entire release for correctness and documentation quality before it ships, and keeping documentation accurate/friendly/comprehensive on an ongoing basis. Both need their own isolated worktree to work independently of the two lanes, per the same rationale as decision-dev-team's decision 2.
|
|
9
|
+
|
|
10
|
+
## Decisions
|
|
11
|
+
|
|
12
|
+
### 1. Two more persistent slots, not a repurposed lane
|
|
13
|
+
|
|
14
|
+
Aziz (release QA lead) and Amy (tech writer) each get a `team/<name>` worktree slot exactly like Ash/Sky/Jay/Kit — added to `scripts/setup_team.sh`'s worktree-provisioning loop and to the persistent-slot exclusion lists in `/merge-pr` and `/cleanup-worktrees` (now six names, not four). They are not a third lane: neither is Dev/QA-paired, both work at release or on-demand cadence rather than one-ticket-at-a-time, and the role table in `/team-member` reflects that with `—` for Lane and Partner.
|
|
15
|
+
|
|
16
|
+
### 2. No dedicated MCP server for Aziz or Amy
|
|
17
|
+
|
|
18
|
+
`scripts/setup_team.sh`'s `role_server` generation loop stays at four entries (Ash/Sky/Jay/Kit); Aziz and Amy are added to worktree provisioning only, not to that loop — no new `mcp-gee-sweet-aziz`/`mcp-gee-sweet-amy` server process starts. Both still receive the same combined `.mcp.json` copy every slot gets (per decision-dev-team's decision 3), so every server is visible in their tool list even though neither owns one. Amy's work is filesystem-only and doesn't need one. Aziz's live-QA work needs to call Sky's and Kit's tools directly (see decision 3) — giving him a server of his own would just be a second, redundant path to the same underlying MCP process each already runs, and the borrowed connection has the same in-session `.mcp.json` guarantee, so it costs nothing to skip.
|
|
19
|
+
|
|
20
|
+
### 3. Aziz borrows Sky's/Kit's slots via `Agent`-tool subagents, not a new Playwright/API identity
|
|
21
|
+
|
|
22
|
+
Live release QA needs the same real Google API + Playwright access Sky/Kit already have. Considered provisioning Aziz a third authenticated identity — rejected as unnecessary duplication of credentials and Playwright browser state for something that only runs periodically, at release time, when the dev team is idle by design (the user's explicit precondition: dev-team work and release QA never run concurrently). Instead Aziz resets Sky's and Kit's worktrees to `origin/develop` before a pass, then spawns `Agent`-tool subagents — true in-session subagents, which inherit whatever MCP servers their parent (Aziz's own session) is already connected to, per the established distinction from decision-dev-team's decision 3 fallout: this is different from an Agent-View "switch session" spawn, which does *not* inherit anything (confirmed live 2026-07-11, see `/team-member` §2). Each subagent is told which slot prefix (`mcp-gee-sweet-sky` or `mcp-gee-sweet-kit`) to call through and which domain/test file to run, mirroring the v0.8.1 QA pass's four-parallel-domain-shard structure. Subagents report structured PASS/FAIL back rather than editing any tracked file themselves — only Aziz, in his own worktree, writes the inline `**Result**` annotations, the aggregate results file, and the release sign-off doc, so two subagents running against Sky's and Kit's separately-checked-out worktrees never produce competing diffs to reconcile.
|
|
23
|
+
|
|
24
|
+
### 4. QA-tier decision reuses the existing scoped-gating audit, made explicitly Aziz's call
|
|
25
|
+
|
|
26
|
+
`docs/qa/runs/README.md` already documents a source-diff audit (enumerate commits, classify behavior-change vs. pure-refactor, map to domains, decide structural vs. non-structural) for when Full Regression can be substituted with Smoke + targeted Domain runs. Rather than inventing a separate process for "what does Aziz decide for a minor release," `/team-member`'s Aziz section points directly at that existing process and treats it as Aziz's responsibility to run and document per release, not a per-release ad hoc judgment call by whoever happens to be doing the release.
|
|
27
|
+
|
|
28
|
+
### 5. Existing Playwright lockfile and no-fabricated-results conventions carry over unchanged
|
|
29
|
+
|
|
30
|
+
Both were already established (`docs/qa/run.md` §"Coordinating Playwright across parallel shards"; the no-fabricated-results and repeat-live-verification disciplines from prior QA passes) and needed no new design — Aziz's subagents follow them exactly as any QA session would. Formalized in `/team-member` §5 rather than restated in a new doc.
|
|
31
|
+
|
|
32
|
+
### 6. Amy claims work via a `documentation` label, mirroring `ready-for-development`
|
|
33
|
+
|
|
34
|
+
Reuses the pre-existing `documentation` GitHub label rather than introducing a new one. Amy's PRs are not gated by `qa-approved` (that gate stays scoped to the four dev-lane names per decision-dev-team's decision 4) — normal `/merge-pr` review applies, since docs-only changes don't carry the same live-API regression risk that motivated the QA-approval gate for Dev-lane code.
|
|
35
|
+
|
|
36
|
+
## When to Re-evaluate
|
|
37
|
+
|
|
38
|
+
- If Aziz's borrowed-worktree model proves leaky (e.g. a subagent edits a tracked file despite instructions, or two shards collide on the same Playwright lock more than the backoff-retry tolerates), reconsider giving Aziz a dedicated Playwright/API identity after all — decision 3's rejection was based on avoided duplication for an infrequent workflow, not a hard constraint.
|
|
39
|
+
- If Amy's doc-quality work starts needing live tool verification (not just static accuracy checks), reconsider whether she needs read-only access to a slot's MCP tools rather than staying filesystem-only (decision 2).
|
|
@@ -12,3 +12,6 @@ Point-in-time records of specific decisions — context, alternatives, and reaso
|
|
|
12
12
|
| [Chart Theming and Phase 3 Integration](decision-chart-theming.md) | 2026-06-17 | decided | No parsing layer for charts; no Vega-Lite adoption; unified brand config (docs + charts sections) for Phase 3 theme system |
|
|
13
13
|
| [Grid Data Size Cap](decision-grid-data-size-cap.md) | 2026-07-03 | decided | Post-fetch response-size check (not pre-fetch cell-count estimate) for `get_sheet_data(include_grid_data=True)`; configurable cap + `local_path` bypass; empirically falsified two earlier approaches |
|
|
14
14
|
| [Cache Invalidation](decision-cache-invalidation.md) | 2026-07-07 | decided | `set_cache_ttl` tool for runtime TTL; `modifiedTime`-based validation for sheet/doc caches only (not folders/calendar); opt-out via `CACHE_VALIDATE_MODIFIED_TIME` |
|
|
15
|
+
| [Dev-Team Roles](decision-dev-team.md) | 2026-07-11 | decided | Named Orchestrator/Dev/QA ×2-lane team, persistent worktree slots, one combined MCP config for Agent View, branch-prefix lane routing |
|
|
16
|
+
| [Async Tool Execution](decision-async-tool-execution.md) | 2026-07-12 | decided | Full tool layer converted to `async def` + `asyncio.to_thread()`; `asyncio.gather()` for 6 multi-call tools; per-thread HTTP transport fix for the newly-introduced shared-service concurrency risk; `cache.py` stays synchronous by design |
|
|
17
|
+
| [Release QA Lead and Tech Writer Roles](decision-release-docs-roles.md) | 2026-07-12 | decided | Aziz/Amy persistent slots, no dedicated MCP server, Aziz borrows Sky/Kit via `Agent`-tool subagents for sharded live release QA, Amy claims `documentation`-labeled issues |
|