mcp-gee-sweet 0.7.0.dev74__tar.gz → 0.7.0.dev80__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.dev80/.claude/commands/cleanup-worktrees.md +15 -0
- mcp_gee_sweet-0.7.0.dev80/.claude/commands/merge-pr.md +13 -0
- mcp_gee_sweet-0.7.0.dev80/.claude/commands/orchestrator.md +20 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.claude/commands/prep-for-pr.md +2 -2
- mcp_gee_sweet-0.7.0.dev80/.claude/commands/start-worktree.md +9 -0
- mcp_gee_sweet-0.7.0.dev80/.claude/commands/verify-pr.md +33 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/PKG-INFO +1 -1
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/docs.md +18 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/roadmap.md +2 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/cache.py +89 -60
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/emitter.py +16 -9
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_cache.py +60 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_docs_tables.py +40 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.dockerignore +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/dependabot.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/workflows/ci.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/workflows/docs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/workflows/publish-dev.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.github/workflows/release.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.gitignore +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.pre-commit-config.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/.python-version +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/CHANGELOG.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/CLAUDE.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/CODE_OF_CONDUCT.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/CONTRIBUTING.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/Dockerfile +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/LICENSE +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/Makefile +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/SECURITY.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docker-compose.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/auth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/client-setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/configuration.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-chart-theming.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-composite-tools.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-docs-formatting.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-fork.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-grid-data-size-cap.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-publishing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/decision-testing.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/decisions/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/design/docs-ast-pipeline.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/design/effectiveformat-spike.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/design/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/design/markdown-support.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/design/phase3-theme-system.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/design.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/index.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/known-limitations.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/notes-read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/fixtures.template.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/operations.yaml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/playwright_oauth.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/.gitkeep +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-06-02.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-06-14.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-06-17.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-06-18.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-06-28.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-07-04-docs.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-07-04-infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/results/2026-07-04.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/retro-v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/run.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/runs/README.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/runs/v0.8.0.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/runs/v0.8.1.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/setup.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/calendar.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/drive.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/infra.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/sheets_charts.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/sheets_mgmt.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/sheets_read.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa/tests/sheets_write.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/qa-checklist.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/docs/tools.md +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/mkdocs.yml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/pyproject.toml +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/pyrightconfig.json +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/scripts/gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/scripts/oauth_setup.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/.env.template +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/cache.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/html_parser.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/drive/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/drive/test_activity.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/drive/test_files.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/drive/test_sharing.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/drive/test_transfer.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/sheets/__init__.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/sheets/test_data.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/sheets/test_helpers.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/sheets/test_structure.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_auth.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_calendar.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_docs_content.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_docs_core.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_docs_layout.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_docs_style.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_gen_tool_docs.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_response_limits.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/tests/test_server.py +0 -0
- {mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/uv.lock +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
|
|
3
|
+
1. List current worktrees: `git worktree list --porcelain`. Skip the main checkout (the entry with no `.claude/worktrees/` path).
|
|
4
|
+
2. For each remaining worktree, get its branch name and check whether it has a merged PR:
|
|
5
|
+
```
|
|
6
|
+
gh pr list --repo khuisman/mcp-gee-sweet --state merged --head <branch> --json number,url,mergedAt
|
|
7
|
+
```
|
|
8
|
+
3. For each worktree whose branch has a merged PR, check it's safe to remove:
|
|
9
|
+
- No uncommitted changes: `git -C <path> status --porcelain` must be empty.
|
|
10
|
+
- No unpushed commits: `git -C <path> log @{upstream}.. --oneline` must be empty (if the branch has no upstream, e.g. already deleted on the remote post-merge, treat a clean `status --porcelain` as sufficient).
|
|
11
|
+
- If both checks pass, remove it: `git worktree remove <path>`, then `git branch -d <branch>`.
|
|
12
|
+
- If either check fails, skip it and note what's uncommitted or unpushed — never force-remove.
|
|
13
|
+
4. Leave alone any worktree whose branch has no merged PR (open PR, no PR yet, or PR closed without merging).
|
|
14
|
+
5. Run `git worktree prune` to clear any stale administrative entries left behind.
|
|
15
|
+
6. Report a summary: worktrees removed (with their PR link), worktrees skipped as dirty (with what's blocking removal), and worktrees still open (with PR/issue status).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Merge the current branch's PR and clean up its worktree. Uses `--admin` only to bypass the required-review gate (this is a solo-maintainer repo, so a second approver is never coming) — it must never be used to bypass a failing or pending status check.
|
|
2
|
+
|
|
3
|
+
1. Get the PR for the current branch: `gh pr view --json number,title,url,state,mergeable,statusCheckRollup,reviewDecision`. If there's no open PR for this branch, stop and tell the user.
|
|
4
|
+
2. Verify it's actually safe to merge:
|
|
5
|
+
- `state` must be `OPEN`.
|
|
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
|
+
- `mergeable` must not be `CONFLICTING`.
|
|
8
|
+
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
|
+
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
|
+
5. Clean up the local worktree:
|
|
11
|
+
- 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"`.
|
|
12
|
+
- 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`.
|
|
13
|
+
6. Report a summary: the merge commit, the now-merged PR URL, and confirmation the worktree was removed.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Establish this session as the **orchestrator** — the one session responsible for ticket triage, roadmap grooming, live testing, and reviewing/merging PRs, as opposed to a worker session doing ticket implementation in a worktree.
|
|
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 and labeling (`ready-for-development`, version labels)
|
|
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
|
|
17
|
+
- Reviewing worker PRs: `verify-pr` (code review + scoped live QA against real Google APIs) before `merge-pr`
|
|
18
|
+
- Worktree cleanup after merge (`cleanup-worktrees`)
|
|
19
|
+
|
|
20
|
+
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).
|
|
@@ -8,8 +8,8 @@ Review the current branch against this checklist and report the status of each i
|
|
|
8
8
|
- [ ] **Unit tests passing** — has `uv run python -m pytest tests/` been run and passed?
|
|
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/` for the new/changed tools?
|
|
11
|
-
|
|
12
|
-
|
|
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 after the PR is open, in the main checkout, where the branch's actual code is reachable by the live MCP tools. Leave the `**Result**` line off new/changed test cases entirely — don't stub it as "pending," just omit it so `/verify-pr` adds the first real one.
|
|
13
13
|
|
|
14
14
|
## 2. QA test case tags
|
|
15
15
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Create an isolated git worktree for parallel work in this repo, with the same local configuration as the main checkout (credentials, env, and a synced virtualenv), so it can run alongside other sessions without touching their branches or files.
|
|
2
|
+
|
|
3
|
+
1. Determine the worktree name: use the argument passed to this command if given (e.g. an issue number or short slug, sanitized to letters/digits/dots/underscores/dashes). If no argument was given, ask the user for a short name before proceeding.
|
|
4
|
+
2. Call `EnterWorktree` with that name. This creates the worktree under `.claude/worktrees/<name>/`, branches it from `origin/develop` (the repo's default branch), and switches the session into it.
|
|
5
|
+
3. Copy the untracked local config that `git worktree` does not check out, from the original repo root (the directory the session was in before `EnterWorktree`) into the new worktree root: `.env`, `credentials.json`, `service_account.json`, `token.json`, `.claude/settings.local.json`. Skip any file that doesn't exist in the source.
|
|
6
|
+
4. Run `uv sync` in the new worktree to build its own virtualenv — worktrees don't share `.venv`.
|
|
7
|
+
5. Confirm the worktree is ready: report its path and current branch to the user.
|
|
8
|
+
|
|
9
|
+
Do not modify the original checkout's files. If the user later asks to leave this worktree, use `ExitWorktree` with `keep` unless they explicitly say to discard the work.
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
2. **Identify the PR.** Take a PR number as an argument if given, otherwise use `gh pr list --state open` and ask the user which one if there's more than one. Get its details: `gh pr view <number> --json number,title,url,headRefName,baseRefName,statusCheckRollup`.
|
|
8
|
+
|
|
9
|
+
3. **Check out the branch.** `gh pr checkout <number>`. This fetches and switches the main checkout to the PR's branch directly (no worktree needed — the orchestrator owns the main checkout).
|
|
10
|
+
|
|
11
|
+
4. **Reconnect the MCP server.** The server hot-reloads on file change, but this Claude session's MCP connection stays on the old process. Tell the user to run:
|
|
12
|
+
```
|
|
13
|
+
/mcp reconnect
|
|
14
|
+
```
|
|
15
|
+
on its own line, standalone (not embedded in other text). Wait for confirmation before continuing — live tests run against stale code otherwise.
|
|
16
|
+
|
|
17
|
+
5. **Code review.** Run `/code-review` at `high` effort against the diff (`<baseRefName>...<headRefName>`, typically `develop...HEAD`). Report findings before moving on to live testing — if it surfaces a correctness bug, ask the user whether to still proceed with live QA or send the PR back to the worker first.
|
|
18
|
+
|
|
19
|
+
6. **Scope the live QA run.** Don't run the full suite. Find what this PR actually touches:
|
|
20
|
+
- `git diff origin/<baseRefName>...HEAD --name-only -- docs/qa/tests/` — new or modified test case files.
|
|
21
|
+
- Within those, identify test cases that are new or whose `Prompt`/`Setup`/`Checks` changed (not ones only touched by unrelated reflow). These are mandatory.
|
|
22
|
+
- Cross-check against `git diff origin/<baseRefName>...HEAD --name-only -- src/mcp_gee_sweet/tools/` — for each changed tool, confirm there's at least one QA test case covering it in scope; if a changed tool has no corresponding test case at all, flag that as a gap rather than silently skipping it.
|
|
23
|
+
- Existing passing test cases for the same tool (regression) are optional spot-checks, not mandatory re-runs — full regression is `docs/qa/run.md`'s job, run periodically, not per-PR.
|
|
24
|
+
|
|
25
|
+
7. **Run the scoped test cases live.** Follow each test case's `Prompt`/`Setup` against the fixtures in `docs/qa/.env` (see `docs/qa/setup.md` if fixtures aren't configured), using the now-reconnected MCP tools. For `**Playwright: required**` cases, use the connected Playwright MCP if available; if not connected, tell the user and ask whether to proceed without visual verification or connect it first.
|
|
26
|
+
|
|
27
|
+
8. **Record results honestly.** For each test case actually executed, add `**Result (<date>) ✅ PASS**` or `**Result (<date>) ❌ FAIL — <what happened>**` directly below its `Checks` block. Never write a Result for a case you didn't actually invoke — if something blocks a case (fixture missing, tool errors before completing), write `**Result (<date>)** pending — <reason>` and say so plainly rather than guessing the outcome.
|
|
28
|
+
|
|
29
|
+
9. **Commit and push the results.** If any Result entries were added, commit just those QA doc changes onto the PR branch (`qa: record live verification for PR #<number>`) and push, so the evidence lands in history before merge. Ask before pushing, per this repo's normal commit/push confirmation rule.
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
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.
|
|
@@ -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.dev80
|
|
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
|
|
@@ -1020,6 +1020,24 @@ These test the HTML→AST→Docs API pipeline introduced in Phase 2 (#87). All u
|
|
|
1020
1020
|
|
|
1021
1021
|
---
|
|
1022
1022
|
|
|
1023
|
+
### TC-DOC89: Degenerate table (row with no cells) followed by a real table doesn't desync content (issue #277)
|
|
1024
|
+
|
|
1025
|
+
**Background:** `ast_to_requests` skips emitting an `insertTable` request for a table with zero rows or zero columns (e.g. a `<tr>` with no `<td>`s), but was still counting that table in the list it hands to `fill_tables()`. Since `fill_tables()` pairs AST tables against live-doc tables positionally, a skipped table shifted every later table's fill/merge/style requests onto the wrong doc table — silently, with no error. Fixed by excluding degenerate tables from that list at the source.
|
|
1026
|
+
|
|
1027
|
+
**Prompt**
|
|
1028
|
+
> "Write this HTML to doc {DOC_ID}: `<table><tr></tr></table><table><tr><td>A</td><td>B</td></tr></table>`"
|
|
1029
|
+
|
|
1030
|
+
**Checks**
|
|
1031
|
+
- Call succeeds with no API error
|
|
1032
|
+
- `get_doc_structure` shows exactly **one** table in the doc (the empty-row table produces no table element at all)
|
|
1033
|
+
- That table is `columns: 2` with cell [0,0] text "A" and cell [0,1] text "B" — not empty, not misapplied, not offset onto the wrong table
|
|
1034
|
+
|
|
1035
|
+
**Cleanup:** write fixture content back
|
|
1036
|
+
|
|
1037
|
+
**Result (2026-07-09) ✅ PASS**
|
|
1038
|
+
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1023
1041
|
### TC-DOC52: `get_doc_theme` scans body paragraph styles
|
|
1024
1042
|
**Note:** `get_doc_theme` reads explicit per-paragraph and per-run styles from the document body. It returns data for AI-generated docs (where styles are set explicitly on runs); for standard docs whose styles are fully inherited from named style defaults it returns an empty dict.
|
|
1025
1043
|
|
|
@@ -96,6 +96,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
96
96
|
|
|
97
97
|
**Defects** _(found after v0.8.1 shipped; too late for that release, don't warrant a standalone patch)_
|
|
98
98
|
- [ ] Bare URLs in markdown content aren't autolinked — Python-Markdown's built-in autolink only fires on `<https://...>` or `[text](url)`, not a bare URL ([#248](https://github.com/khuisman/mcp-gee-sweet/issues/248))
|
|
99
|
+
- [ ] `zip(doc_tables, ast_tables)` in `emitter.py` silently cross-pairs tables when one is skipped (zero-row/zero-col table), misapplying fill/merge/style requests to the wrong table — surfaced during #276's review ([#277](https://github.com/khuisman/mcp-gee-sweet/issues/277))
|
|
99
100
|
|
|
100
101
|
**Sheets**
|
|
101
102
|
- [ ] `update_borders` — border style, width, color on a range ([#122](https://github.com/khuisman/mcp-gee-sweet/issues/122)) _(freema/mcp-gsheets)_
|
|
@@ -130,6 +131,7 @@ No new tools. Stabilize on what Tier 1 shipped before starting Tier 2 feature wo
|
|
|
130
131
|
- [ ] `create_shortcut` — create a Drive shortcut to a file ([#141](https://github.com/khuisman/mcp-gee-sweet/issues/141))
|
|
131
132
|
- [ ] `sync_folder` — option to convert markdown files to Google Docs on upload ([#211](https://github.com/khuisman/mcp-gee-sweet/issues/211))
|
|
132
133
|
- [ ] `upload_local_file` with Drive format conversion (CSV→Sheets, MD→Docs) ([#188](https://github.com/khuisman/mcp-gee-sweet/issues/188))
|
|
134
|
+
- [ ] Expose `md5Checksum` in `list_files`/`get_file_metadata` for real content-diffing, and let `sync_folder` diff by checksum instead of/in addition to `modifiedTime` — mtime is unreliable across in-place overwrites, `upload_local_file`'s "now" stamp, and content-preserving regenerations; related to #239 ([#274](https://github.com/khuisman/mcp-gee-sweet/issues/274))
|
|
133
135
|
- [x] Drive Activity API — file change history ([#72](https://github.com/khuisman/mcp-gee-sweet/issues/72))
|
|
134
136
|
|
|
135
137
|
**Infrastructure**
|
|
@@ -28,6 +28,9 @@ def _connect(path: str) -> sqlite3.Connection:
|
|
|
28
28
|
conn = sqlite3.connect(path, check_same_thread=False)
|
|
29
29
|
conn.row_factory = sqlite3.Row
|
|
30
30
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
31
|
+
# Concurrent Claude sessions share this DB file by default; without a busy_timeout
|
|
32
|
+
# a write from another session hits SQLITE_BUSY immediately instead of waiting.
|
|
33
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
31
34
|
conn.execute(_DDL)
|
|
32
35
|
conn.commit()
|
|
33
36
|
return conn
|
|
@@ -60,6 +63,33 @@ def _open(db_path: str) -> sqlite3.Connection:
|
|
|
60
63
|
return _connect(":memory:")
|
|
61
64
|
|
|
62
65
|
|
|
66
|
+
def _safe_fetchone(conn: sqlite3.Connection, sql: str, params: tuple) -> sqlite3.Row | None:
|
|
67
|
+
"""Run a SELECT and fetchone(), treating any sqlite error as a cache miss.
|
|
68
|
+
|
|
69
|
+
Another session's crash can leave a locked/corrupted WAL behind; a cache read
|
|
70
|
+
should never take down a tool call over it (fail open, same as the API-fetch
|
|
71
|
+
fallback in fetch_sheets()).
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
return conn.execute(sql, params).fetchone()
|
|
75
|
+
except sqlite3.Error as exc:
|
|
76
|
+
logger.warning("Cache read failed (%s), treating as cache miss", exc)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _safe_write(conn: sqlite3.Connection, sql: str, params: tuple) -> None:
|
|
81
|
+
"""Run a write statement + commit, swallowing sqlite errors (fail-open cache write).
|
|
82
|
+
|
|
83
|
+
A failed cache write must not fail the tool call that already succeeded against
|
|
84
|
+
the underlying Google API — it just means that result won't be cached this time.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
conn.execute(sql, params)
|
|
88
|
+
conn.commit()
|
|
89
|
+
except sqlite3.Error as exc:
|
|
90
|
+
logger.warning("Cache write failed (%s), continuing without caching", exc)
|
|
91
|
+
|
|
92
|
+
|
|
63
93
|
@dataclass
|
|
64
94
|
class SheetInfo:
|
|
65
95
|
title: str
|
|
@@ -75,19 +105,20 @@ class SheetStructureCache:
|
|
|
75
105
|
logger.debug("Sheet structure cache opened: %s", db_path)
|
|
76
106
|
|
|
77
107
|
def _get_valid(self, spreadsheet_id: str) -> sqlite3.Row | None:
|
|
78
|
-
row =
|
|
108
|
+
row = _safe_fetchone(
|
|
109
|
+
self._conn,
|
|
79
110
|
"SELECT value, fetched_at, dirty FROM cache WHERE namespace=? AND key=?",
|
|
80
111
|
(self._NS, spreadsheet_id),
|
|
81
|
-
)
|
|
112
|
+
)
|
|
82
113
|
if row is None or row["dirty"]:
|
|
83
114
|
return None
|
|
84
115
|
if time.time() - row["fetched_at"] > self._ttl:
|
|
85
116
|
logger.debug("Cache TTL expired for %s, marking dirty", spreadsheet_id)
|
|
86
|
-
|
|
117
|
+
_safe_write(
|
|
118
|
+
self._conn,
|
|
87
119
|
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?",
|
|
88
120
|
(self._NS, spreadsheet_id),
|
|
89
121
|
)
|
|
90
|
-
self._conn.commit()
|
|
91
122
|
return None
|
|
92
123
|
return row
|
|
93
124
|
|
|
@@ -109,33 +140,33 @@ class SheetStructureCache:
|
|
|
109
140
|
value: dict = {"sheets": [{"title": s.title, "sheet_id": s.sheet_id} for s in sheets]}
|
|
110
141
|
if title is not None:
|
|
111
142
|
value["title"] = title
|
|
112
|
-
|
|
143
|
+
_safe_write(
|
|
144
|
+
self._conn,
|
|
113
145
|
"INSERT OR REPLACE INTO cache (namespace, key, value, fetched_at, dirty)"
|
|
114
146
|
" VALUES (?,?,?,?,0)",
|
|
115
147
|
(self._NS, spreadsheet_id, json.dumps(value), time.time()),
|
|
116
148
|
)
|
|
117
|
-
self._conn.commit()
|
|
118
149
|
logger.debug("Cached %d sheets for %s", len(sheets), spreadsheet_id)
|
|
119
150
|
|
|
120
151
|
def mark_dirty(self, spreadsheet_id: str):
|
|
121
|
-
|
|
152
|
+
_safe_write(
|
|
153
|
+
self._conn,
|
|
122
154
|
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?",
|
|
123
155
|
(self._NS, spreadsheet_id),
|
|
124
156
|
)
|
|
125
|
-
self._conn.commit()
|
|
126
157
|
logger.debug("Marked cache dirty for %s", spreadsheet_id)
|
|
127
158
|
|
|
128
159
|
def mark_all_dirty(self):
|
|
129
|
-
self._conn
|
|
130
|
-
self._conn.commit()
|
|
160
|
+
_safe_write(self._conn, "UPDATE cache SET dirty=1 WHERE namespace=?", (self._NS,))
|
|
131
161
|
logger.debug("Invalidated all sheet structure cache entries")
|
|
132
162
|
|
|
133
163
|
def get_stale_sheets(self, spreadsheet_id: str) -> list[SheetInfo] | None:
|
|
134
164
|
"""Returns cached sheet list regardless of dirty/TTL state; None only on complete miss."""
|
|
135
|
-
row =
|
|
165
|
+
row = _safe_fetchone(
|
|
166
|
+
self._conn,
|
|
136
167
|
"SELECT value FROM cache WHERE namespace=? AND key=?",
|
|
137
168
|
(self._NS, spreadsheet_id),
|
|
138
|
-
)
|
|
169
|
+
)
|
|
139
170
|
if row is None:
|
|
140
171
|
return None
|
|
141
172
|
return [SheetInfo(**s) for s in json.loads(row["value"])["sheets"]]
|
|
@@ -161,17 +192,17 @@ class SheetDataCache:
|
|
|
161
192
|
self, spreadsheet_id: str, sheet_id: int, rows_to_fetch: int
|
|
162
193
|
) -> sqlite3.Row | None:
|
|
163
194
|
key = self._key(spreadsheet_id, sheet_id)
|
|
164
|
-
row =
|
|
195
|
+
row = _safe_fetchone(
|
|
196
|
+
self._conn,
|
|
165
197
|
"SELECT value, fetched_at, dirty, rows_fetched FROM cache WHERE namespace=? AND key=?",
|
|
166
198
|
(self._NS, key),
|
|
167
|
-
)
|
|
199
|
+
)
|
|
168
200
|
if row is None or row["dirty"]:
|
|
169
201
|
return None
|
|
170
202
|
if time.time() - row["fetched_at"] > self._ttl:
|
|
171
|
-
|
|
172
|
-
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
203
|
+
_safe_write(
|
|
204
|
+
self._conn, "UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
173
205
|
)
|
|
174
|
-
self._conn.commit()
|
|
175
206
|
return None
|
|
176
207
|
if (row["rows_fetched"] or 0) < rows_to_fetch:
|
|
177
208
|
return None
|
|
@@ -198,7 +229,8 @@ class SheetDataCache:
|
|
|
198
229
|
rows_to_fetch: int,
|
|
199
230
|
):
|
|
200
231
|
value = {"headers": headers, "first_rows": first_rows}
|
|
201
|
-
|
|
232
|
+
_safe_write(
|
|
233
|
+
self._conn,
|
|
202
234
|
"INSERT OR REPLACE INTO cache (namespace, key, value, fetched_at, dirty, rows_fetched)"
|
|
203
235
|
" VALUES (?,?,?,?,0,?)",
|
|
204
236
|
(
|
|
@@ -209,27 +241,26 @@ class SheetDataCache:
|
|
|
209
241
|
rows_to_fetch,
|
|
210
242
|
),
|
|
211
243
|
)
|
|
212
|
-
self._conn.commit()
|
|
213
244
|
logger.debug("Cached sheet data for %s/%s", spreadsheet_id, sheet_id)
|
|
214
245
|
|
|
215
246
|
def mark_dirty(self, spreadsheet_id: str, sheet_id: int | None = None):
|
|
216
247
|
if sheet_id is not None:
|
|
217
|
-
|
|
248
|
+
_safe_write(
|
|
249
|
+
self._conn,
|
|
218
250
|
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?",
|
|
219
251
|
(self._NS, self._key(spreadsheet_id, sheet_id)),
|
|
220
252
|
)
|
|
221
253
|
logger.debug("Marked sheet data dirty for %s/%s", spreadsheet_id, sheet_id)
|
|
222
254
|
else:
|
|
223
|
-
|
|
255
|
+
_safe_write(
|
|
256
|
+
self._conn,
|
|
224
257
|
"UPDATE cache SET dirty=1 WHERE namespace=? AND key LIKE ?",
|
|
225
258
|
(self._NS, f"{spreadsheet_id}:%"),
|
|
226
259
|
)
|
|
227
260
|
logger.debug("Marked all sheet data dirty for %s", spreadsheet_id)
|
|
228
|
-
self._conn.commit()
|
|
229
261
|
|
|
230
262
|
def mark_all_dirty(self):
|
|
231
|
-
self._conn
|
|
232
|
-
self._conn.commit()
|
|
263
|
+
_safe_write(self._conn, "UPDATE cache SET dirty=1 WHERE namespace=?", (self._NS,))
|
|
233
264
|
logger.debug("Invalidated all sheet data cache entries")
|
|
234
265
|
|
|
235
266
|
def close(self):
|
|
@@ -251,17 +282,17 @@ class DriveFolderCache:
|
|
|
251
282
|
|
|
252
283
|
def _get_valid(self, folder_id: str, mime_type: str | None) -> sqlite3.Row | None:
|
|
253
284
|
key = self._key(folder_id, mime_type)
|
|
254
|
-
row =
|
|
285
|
+
row = _safe_fetchone(
|
|
286
|
+
self._conn,
|
|
255
287
|
"SELECT value, fetched_at, dirty FROM cache WHERE namespace=? AND key=?",
|
|
256
288
|
(self._NS, key),
|
|
257
|
-
)
|
|
289
|
+
)
|
|
258
290
|
if row is None or row["dirty"]:
|
|
259
291
|
return None
|
|
260
292
|
if time.time() - row["fetched_at"] > self._ttl:
|
|
261
|
-
|
|
262
|
-
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
293
|
+
_safe_write(
|
|
294
|
+
self._conn, "UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
263
295
|
)
|
|
264
|
-
self._conn.commit()
|
|
265
296
|
return None
|
|
266
297
|
return row
|
|
267
298
|
|
|
@@ -274,25 +305,24 @@ class DriveFolderCache:
|
|
|
274
305
|
return json.loads(row["value"])
|
|
275
306
|
|
|
276
307
|
def store(self, folder_id: str, mime_type: str | None, files: list):
|
|
277
|
-
|
|
308
|
+
_safe_write(
|
|
309
|
+
self._conn,
|
|
278
310
|
"INSERT OR REPLACE INTO cache (namespace, key, value, fetched_at, dirty)"
|
|
279
311
|
" VALUES (?,?,?,?,0)",
|
|
280
312
|
(self._NS, self._key(folder_id, mime_type), json.dumps(files), time.time()),
|
|
281
313
|
)
|
|
282
|
-
self._conn.commit()
|
|
283
314
|
logger.debug("Cached %d files for folder %s (mime=%s)", len(files), folder_id, mime_type)
|
|
284
315
|
|
|
285
316
|
def mark_dirty(self, folder_id: str):
|
|
286
|
-
|
|
317
|
+
_safe_write(
|
|
318
|
+
self._conn,
|
|
287
319
|
"UPDATE cache SET dirty=1 WHERE namespace=? AND key LIKE ?",
|
|
288
320
|
(self._NS, f"{folder_id}:%"),
|
|
289
321
|
)
|
|
290
|
-
self._conn.commit()
|
|
291
322
|
logger.debug("Marked drive folder cache dirty for %s", folder_id)
|
|
292
323
|
|
|
293
324
|
def mark_all_dirty(self):
|
|
294
|
-
self._conn
|
|
295
|
-
self._conn.commit()
|
|
325
|
+
_safe_write(self._conn, "UPDATE cache SET dirty=1 WHERE namespace=?", (self._NS,))
|
|
296
326
|
logger.debug("Invalidated all drive folder cache entries")
|
|
297
327
|
|
|
298
328
|
def close(self):
|
|
@@ -310,17 +340,19 @@ class DocContentCache:
|
|
|
310
340
|
logger.debug("Doc cache opened: %s", db_path)
|
|
311
341
|
|
|
312
342
|
def _get_valid(self, file_id: str) -> sqlite3.Row | None:
|
|
313
|
-
row =
|
|
343
|
+
row = _safe_fetchone(
|
|
344
|
+
self._conn,
|
|
314
345
|
"SELECT value, fetched_at, dirty FROM cache WHERE namespace=? AND key=?",
|
|
315
346
|
(self._NS, file_id),
|
|
316
|
-
)
|
|
347
|
+
)
|
|
317
348
|
if row is None or row["dirty"]:
|
|
318
349
|
return None
|
|
319
350
|
if time.time() - row["fetched_at"] > self._ttl:
|
|
320
|
-
|
|
321
|
-
|
|
351
|
+
_safe_write(
|
|
352
|
+
self._conn,
|
|
353
|
+
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?",
|
|
354
|
+
(self._NS, file_id),
|
|
322
355
|
)
|
|
323
|
-
self._conn.commit()
|
|
324
356
|
return None
|
|
325
357
|
return row
|
|
326
358
|
|
|
@@ -333,25 +365,24 @@ class DocContentCache:
|
|
|
333
365
|
return json.loads(row["value"])
|
|
334
366
|
|
|
335
367
|
def store(self, file_id: str, doc: dict):
|
|
336
|
-
|
|
368
|
+
_safe_write(
|
|
369
|
+
self._conn,
|
|
337
370
|
"INSERT OR REPLACE INTO cache (namespace, key, value, fetched_at, dirty)"
|
|
338
371
|
" VALUES (?,?,?,?,0)",
|
|
339
372
|
(self._NS, file_id, json.dumps(doc), time.time()),
|
|
340
373
|
)
|
|
341
|
-
self._conn.commit()
|
|
342
374
|
logger.debug("Cached doc %s", file_id)
|
|
343
375
|
|
|
344
376
|
def mark_dirty(self, file_id: str):
|
|
345
|
-
|
|
377
|
+
_safe_write(
|
|
378
|
+
self._conn,
|
|
346
379
|
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?",
|
|
347
380
|
(self._NS, file_id),
|
|
348
381
|
)
|
|
349
|
-
self._conn.commit()
|
|
350
382
|
logger.debug("Marked doc cache dirty for %s", file_id)
|
|
351
383
|
|
|
352
384
|
def mark_all_dirty(self):
|
|
353
|
-
self._conn
|
|
354
|
-
self._conn.commit()
|
|
385
|
+
_safe_write(self._conn, "UPDATE cache SET dirty=1 WHERE namespace=?", (self._NS,))
|
|
355
386
|
logger.debug("Invalidated all doc cache entries")
|
|
356
387
|
|
|
357
388
|
def close(self):
|
|
@@ -370,17 +401,17 @@ class CalendarCache:
|
|
|
370
401
|
logger.debug("Calendar cache opened: %s", db_path)
|
|
371
402
|
|
|
372
403
|
def _get_valid(self, key: str) -> sqlite3.Row | None:
|
|
373
|
-
row =
|
|
404
|
+
row = _safe_fetchone(
|
|
405
|
+
self._conn,
|
|
374
406
|
"SELECT value, fetched_at, dirty FROM cache WHERE namespace=? AND key=?",
|
|
375
407
|
(self._NS, key),
|
|
376
|
-
)
|
|
408
|
+
)
|
|
377
409
|
if row is None or row["dirty"]:
|
|
378
410
|
return None
|
|
379
411
|
if time.time() - row["fetched_at"] > self._ttl:
|
|
380
|
-
|
|
381
|
-
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
412
|
+
_safe_write(
|
|
413
|
+
self._conn, "UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
382
414
|
)
|
|
383
|
-
self._conn.commit()
|
|
384
415
|
return None
|
|
385
416
|
return row
|
|
386
417
|
|
|
@@ -393,12 +424,12 @@ class CalendarCache:
|
|
|
393
424
|
return json.loads(row["value"])
|
|
394
425
|
|
|
395
426
|
def store_list(self, calendars: list):
|
|
396
|
-
|
|
427
|
+
_safe_write(
|
|
428
|
+
self._conn,
|
|
397
429
|
"INSERT OR REPLACE INTO cache (namespace, key, value, fetched_at, dirty)"
|
|
398
430
|
" VALUES (?,?,?,?,0)",
|
|
399
431
|
(self._NS, self._LIST_KEY, json.dumps(calendars), time.time()),
|
|
400
432
|
)
|
|
401
|
-
self._conn.commit()
|
|
402
433
|
logger.debug("Cached calendar list (%d entries)", len(calendars))
|
|
403
434
|
|
|
404
435
|
def get(self, calendar_id: str) -> dict | None:
|
|
@@ -410,25 +441,23 @@ class CalendarCache:
|
|
|
410
441
|
return json.loads(row["value"])
|
|
411
442
|
|
|
412
443
|
def store(self, calendar_id: str, calendar: dict):
|
|
413
|
-
|
|
444
|
+
_safe_write(
|
|
445
|
+
self._conn,
|
|
414
446
|
"INSERT OR REPLACE INTO cache (namespace, key, value, fetched_at, dirty)"
|
|
415
447
|
" VALUES (?,?,?,?,0)",
|
|
416
448
|
(self._NS, calendar_id, json.dumps(calendar), time.time()),
|
|
417
449
|
)
|
|
418
|
-
self._conn.commit()
|
|
419
450
|
logger.debug("Cached calendar %s", calendar_id)
|
|
420
451
|
|
|
421
452
|
def mark_dirty(self, calendar_id: str):
|
|
422
453
|
for key in (calendar_id, self._LIST_KEY):
|
|
423
|
-
|
|
424
|
-
"UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
454
|
+
_safe_write(
|
|
455
|
+
self._conn, "UPDATE cache SET dirty=1 WHERE namespace=? AND key=?", (self._NS, key)
|
|
425
456
|
)
|
|
426
|
-
self._conn.commit()
|
|
427
457
|
logger.debug("Marked calendar cache dirty for %s", calendar_id)
|
|
428
458
|
|
|
429
459
|
def mark_all_dirty(self):
|
|
430
|
-
self._conn
|
|
431
|
-
self._conn.commit()
|
|
460
|
+
_safe_write(self._conn, "UPDATE cache SET dirty=1 WHERE namespace=?", (self._NS,))
|
|
432
461
|
logger.debug("Invalidated all calendar cache entries")
|
|
433
462
|
|
|
434
463
|
def close(self):
|
{mcp_gee_sweet-0.7.0.dev74 → mcp_gee_sweet-0.7.0.dev80}/src/mcp_gee_sweet/tools/docs/emitter.py
RENAMED
|
@@ -28,6 +28,14 @@ def ast_to_requests(nodes: list[DocNode], start_index: int = 1) -> tuple[list[di
|
|
|
28
28
|
|
|
29
29
|
for node in nodes:
|
|
30
30
|
if isinstance(node, Table):
|
|
31
|
+
num_rows = len(node.rows)
|
|
32
|
+
num_cols = max((sum(c.colspan for c in row.cells) for row in node.rows), default=0)
|
|
33
|
+
if num_rows == 0 or num_cols == 0:
|
|
34
|
+
# No insertTable request is emitted below for a degenerate table, so it must
|
|
35
|
+
# not be added to `tables` either — fill_tables() zips this list positionally
|
|
36
|
+
# against the tables actually present in the live doc, and a table with no
|
|
37
|
+
# insertTable request never shows up there.
|
|
38
|
+
continue
|
|
31
39
|
tables.append(node)
|
|
32
40
|
table_positions.append(start_index + len(full_text))
|
|
33
41
|
else:
|
|
@@ -105,16 +113,15 @@ def ast_to_requests(nodes: list[DocNode], start_index: int = 1) -> tuple[list[di
|
|
|
105
113
|
table = tables[i]
|
|
106
114
|
num_rows = len(table.rows)
|
|
107
115
|
num_cols = max((sum(c.colspan for c in row.cells) for row in table.rows), default=0)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"location": {"index": table_positions[i]},
|
|
115
|
-
}
|
|
116
|
+
requests.append(
|
|
117
|
+
{
|
|
118
|
+
"insertTable": {
|
|
119
|
+
"rows": num_rows,
|
|
120
|
+
"columns": num_cols,
|
|
121
|
+
"location": {"index": table_positions[i]},
|
|
116
122
|
}
|
|
117
|
-
|
|
123
|
+
}
|
|
124
|
+
)
|
|
118
125
|
|
|
119
126
|
return requests, tables
|
|
120
127
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sqlite3
|
|
2
3
|
import time
|
|
3
4
|
|
|
4
5
|
from mcp_gee_sweet.cache import (
|
|
@@ -8,6 +9,8 @@ from mcp_gee_sweet.cache import (
|
|
|
8
9
|
SheetInfo,
|
|
9
10
|
SheetStructureCache,
|
|
10
11
|
_open,
|
|
12
|
+
_safe_fetchone,
|
|
13
|
+
_safe_write,
|
|
11
14
|
)
|
|
12
15
|
|
|
13
16
|
# All tests use an in-memory SQLite database.
|
|
@@ -282,6 +285,63 @@ class TestSharedDb:
|
|
|
282
285
|
assert doc.get("fid") is not None # different namespace, unaffected
|
|
283
286
|
|
|
284
287
|
|
|
288
|
+
class TestBusyTimeout:
|
|
289
|
+
"""issue #234: a busy_timeout must be set so a concurrent writer from another
|
|
290
|
+
session waits instead of raising SQLITE_BUSY immediately."""
|
|
291
|
+
|
|
292
|
+
def test_busy_timeout_is_set(self, tmp_path):
|
|
293
|
+
db = str(tmp_path / "test.db")
|
|
294
|
+
conn = _open(db)
|
|
295
|
+
try:
|
|
296
|
+
(value,) = conn.execute("PRAGMA busy_timeout").fetchone()
|
|
297
|
+
assert value == 5000
|
|
298
|
+
finally:
|
|
299
|
+
conn.close()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestFailOpenOnSqliteError:
|
|
303
|
+
"""issue #234: a cache read/write must never crash a tool call — any sqlite
|
|
304
|
+
error (locked/corrupted DB from another session) is treated as a cache miss
|
|
305
|
+
on reads and silently dropped on writes."""
|
|
306
|
+
|
|
307
|
+
def test_safe_fetchone_returns_none_on_error(self):
|
|
308
|
+
conn = sqlite3.connect(":memory:")
|
|
309
|
+
conn.close() # any operation on a closed connection raises sqlite3.ProgrammingError
|
|
310
|
+
assert _safe_fetchone(conn, "SELECT 1", ()) is None
|
|
311
|
+
|
|
312
|
+
def test_safe_write_swallows_error(self):
|
|
313
|
+
conn = sqlite3.connect(":memory:")
|
|
314
|
+
conn.close()
|
|
315
|
+
_safe_write(conn, "SELECT 1", ()) # must not raise
|
|
316
|
+
|
|
317
|
+
def test_cache_get_survives_closed_connection(self):
|
|
318
|
+
cache = SheetStructureCache(db_path=":memory:", ttl=60)
|
|
319
|
+
cache.store("sid", [SheetInfo(title="S1", sheet_id=0)])
|
|
320
|
+
cache._conn.close()
|
|
321
|
+
assert cache.get_sheets("sid") is None
|
|
322
|
+
|
|
323
|
+
def test_cache_store_survives_closed_connection(self):
|
|
324
|
+
cache = SheetStructureCache(db_path=":memory:", ttl=60)
|
|
325
|
+
cache._conn.close()
|
|
326
|
+
cache.store("sid", [SheetInfo(title="S1", sheet_id=0)]) # must not raise
|
|
327
|
+
|
|
328
|
+
def test_mark_dirty_survives_closed_connection(self):
|
|
329
|
+
cache = SheetStructureCache(db_path=":memory:", ttl=60)
|
|
330
|
+
cache._conn.close()
|
|
331
|
+
cache.mark_dirty("sid") # must not raise
|
|
332
|
+
|
|
333
|
+
def test_mark_all_dirty_survives_closed_connection(self):
|
|
334
|
+
cache = SheetStructureCache(db_path=":memory:", ttl=60)
|
|
335
|
+
cache._conn.close()
|
|
336
|
+
cache.mark_all_dirty() # must not raise
|
|
337
|
+
|
|
338
|
+
def test_doc_cache_get_survives_closed_connection(self):
|
|
339
|
+
cache = DocContentCache(db_path=":memory:", ttl=60)
|
|
340
|
+
cache.store("fid", {"title": "Doc"})
|
|
341
|
+
cache._conn.close()
|
|
342
|
+
assert cache.get("fid") is None
|
|
343
|
+
|
|
344
|
+
|
|
285
345
|
class TestOpenFallback:
|
|
286
346
|
"""_open() must survive a read-only or corrupted DB file without crashing."""
|
|
287
347
|
|