mcp-gee-sweet 0.9.2.dev283__tar.gz → 0.9.2.dev289__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.
Files changed (253) hide show
  1. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/amy.md +3 -1
  2. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/kai.md +1 -1
  3. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/PKG-INFO +1 -1
  4. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/doc-to-markdown.md +17 -0
  5. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_content.md +119 -1
  6. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/roadmap.md +2 -2
  7. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/pyproject.toml +1 -0
  8. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/ast_to_markdown.py +153 -43
  9. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/content.py +73 -64
  10. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/emitter.py +8 -4
  11. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/html_parser.py +6 -3
  12. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/images.py +28 -11
  13. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_content.py +80 -0
  14. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_core.py +76 -42
  15. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_images.py +39 -0
  16. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_markdown_export.py +184 -5
  17. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_tables.py +13 -0
  18. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/uv.lock +24 -1
  19. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/cleanup-worktrees.md +0 -0
  20. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/merge-pr.md +0 -0
  21. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/notify-kai.md +0 -0
  22. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/notify-partner.md +0 -0
  23. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/orchestrator.md +0 -0
  24. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/prep-for-pr.md +0 -0
  25. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/qa-kickoff.md +0 -0
  26. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/release.md +0 -0
  27. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/retro.md +0 -0
  28. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/roadmap-status.md +0 -0
  29. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/start-worktree.md +0 -0
  30. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/team-member.md +0 -0
  31. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/commands/verify-pr.md +0 -0
  32. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/settings.json +0 -0
  33. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/aziz.md +0 -0
  34. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/bob.md +0 -0
  35. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/dev.md +0 -0
  36. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/joy.md +0 -0
  37. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.claude/team-roles/qa.md +0 -0
  38. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.dockerignore +0 -0
  39. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  40. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  41. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  42. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/dependabot.yml +0 -0
  43. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/ci.yml +0 -0
  44. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/docs.yml +0 -0
  45. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/publish-dev.yml +0 -0
  46. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.github/workflows/release.yml +0 -0
  47. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.gitignore +0 -0
  48. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.pre-commit-config.yaml +0 -0
  49. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/.python-version +0 -0
  50. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/CHANGELOG.md +0 -0
  51. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/CLAUDE.md +0 -0
  52. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/CODE_OF_CONDUCT.md +0 -0
  53. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/CONTRIBUTING.md +0 -0
  54. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/Dockerfile +0 -0
  55. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/LICENSE +0 -0
  56. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/Makefile +0 -0
  57. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/README.md +0 -0
  58. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/SECURITY.md +0 -0
  59. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docker-compose.yml +0 -0
  60. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/assets/logo.png +0 -0
  61. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/auth.md +0 -0
  62. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/client-setup.md +0 -0
  63. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/configuration.md +0 -0
  64. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-async-tool-execution.md +0 -0
  65. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-auth-status-limitation-categories.md +0 -0
  66. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-cache-invalidation.md +0 -0
  67. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-chart-theming.md +0 -0
  68. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-comments-first-class.md +0 -0
  69. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-composite-tools.md +0 -0
  70. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-condition-type-docstring-generation.md +0 -0
  71. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-converted-doc-change-detection.md +0 -0
  72. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-dev-team.md +0 -0
  73. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-docs-formatting.md +0 -0
  74. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-fork.md +0 -0
  75. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-gmail-domain.md +0 -0
  76. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-grid-data-size-cap.md +0 -0
  77. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-local-fs-test-harness.md +0 -0
  78. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-mcp-v2-resource-context-access.md +0 -0
  79. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-pillow-image-dependency.md +0 -0
  80. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-prompt-qa-role.md +0 -0
  81. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-publishing.md +0 -0
  82. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-release-docs-roles.md +0 -0
  83. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-repositioning.md +0 -0
  84. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-response-size-cap-generalization.md +0 -0
  85. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-response-size-cap-reevaluation-519.md +0 -0
  86. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-review-finding-ticket-bar.md +0 -0
  87. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-supports-all-drives-lint-check.md +0 -0
  88. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-sync-folder-recursion.md +0 -0
  89. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-testing.md +0 -0
  90. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/decision-tool-error-visibility.md +0 -0
  91. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/index.md +0 -0
  92. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/decisions/role-file-history.md +0 -0
  93. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/blockquote-representation.md +0 -0
  94. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/borderless-table-columns.md +0 -0
  95. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/docs-ast-pipeline.md +0 -0
  96. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/effectiveformat-spike.md +0 -0
  97. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/heading-anchor-resolution.md +0 -0
  98. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/image-conversion.md +0 -0
  99. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/index.md +0 -0
  100. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/markdown-support.md +0 -0
  101. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/module-history.md +0 -0
  102. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/oauth-reauthorize-from-tool-call.md +0 -0
  103. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/phase3-theme-system.md +0 -0
  104. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design/sse-consent-off-event-loop.md +0 -0
  105. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/design.md +0 -0
  106. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/index.md +0 -0
  107. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/known-limitations.md +0 -0
  108. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/notes-read.md +0 -0
  109. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/README.md +0 -0
  110. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/qa-fixture-pixel.png +0 -0
  111. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d195-create-doc.md +0 -0
  112. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d196-create-doc.html +0 -0
  113. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d213-dollar-escape.md +0 -0
  114. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-d226-heading-table.md +0 -0
  115. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc102-image-conversion.html +0 -0
  116. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc103-image-conversion.md +0 -0
  117. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc104-nested-lists.html +0 -0
  118. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc105-nested-lists-gfm.md +0 -0
  119. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc106-nested-lists-4space.md +0 -0
  120. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc135-paragraph-boundary.md +0 -0
  121. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc136-interrupted-block-boundary.md +0 -0
  122. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc137-inline-hr-no-block-ancestor.html +0 -0
  123. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc138-nbsp-spacer-paragraph.md +0 -0
  124. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc141-mismatched-list-tags.html +0 -0
  125. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc142-heading-anchors.md +0 -0
  126. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc144-anchor-in-table-cell.md +0 -0
  127. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures/tc-doc145-anchor-on-first-element.md +0 -0
  128. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/fixtures.template.md +0 -0
  129. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/gmail-test-plan.md +0 -0
  130. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/operations.yaml +0 -0
  131. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/playwright_oauth.md +0 -0
  132. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/.gitkeep +0 -0
  133. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-02.md +0 -0
  134. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-14.md +0 -0
  135. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-17.md +0 -0
  136. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-18.md +0 -0
  137. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-21-drive-discovery.md +0 -0
  138. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-21-sheets-delete-clear.md +0 -0
  139. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-06-28.md +0 -0
  140. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-docs.md +0 -0
  141. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-drive-smoke.md +0 -0
  142. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-infra.md +0 -0
  143. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04-sheets_read.md +0 -0
  144. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-07-04.md +0 -0
  145. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/results/2026-09-04.md +0 -0
  146. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/retro-v0.8.0.md +0 -0
  147. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/retro-v0.9.0.md +0 -0
  148. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/run.md +0 -0
  149. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/README.md +0 -0
  150. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.8.0.md +0 -0
  151. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.8.1.md +0 -0
  152. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/runs/v0.9.0.md +0 -0
  153. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/setup.md +0 -0
  154. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/calendar.md +0 -0
  155. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_layout.md +0 -0
  156. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_style.md +0 -0
  157. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/docs_tables.md +0 -0
  158. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_activity.md +0 -0
  159. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_files.md +0 -0
  160. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_sharing.md +0 -0
  161. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/drive_transfer.md +0 -0
  162. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/gmail.md +0 -0
  163. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/infra.md +0 -0
  164. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_charts.md +0 -0
  165. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_mgmt.md +0 -0
  166. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_read.md +0 -0
  167. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/qa/tests/sheets_write.md +0 -0
  168. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/style-guide.md +0 -0
  169. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/docs/tools.md +0 -0
  170. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/mkdocs.yml +0 -0
  171. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/pyrightconfig.json +0 -0
  172. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/check_supports_all_drives.py +0 -0
  173. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/gen_tool_docs.py +0 -0
  174. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/lane_context_hook.py +0 -0
  175. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/oauth_setup.py +0 -0
  176. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/qa_cancel_insert_local_images.py +0 -0
  177. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/qa_gmail_fixtures.py +0 -0
  178. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/setup_team.sh +0 -0
  179. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/scripts/write_team_mcp_configs.sh +0 -0
  180. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/server.json +0 -0
  181. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/.env.template +0 -0
  182. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/__init__.py +0 -0
  183. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/auth.py +0 -0
  184. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/cache.py +0 -0
  185. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/http_transport.py +0 -0
  186. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/reauth.py +0 -0
  187. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/server.py +0 -0
  188. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/sse_shutdown.py +0 -0
  189. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/__init__.py +0 -0
  190. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/cache.py +0 -0
  191. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/calendar.py +0 -0
  192. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/concurrency.py +0 -0
  193. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/__init__.py +0 -0
  194. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/anchors.py +0 -0
  195. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/ast.py +0 -0
  196. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/comments.py +0 -0
  197. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/doc_to_ast.py +0 -0
  198. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/editing.py +0 -0
  199. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/indices.py +0 -0
  200. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/layout.py +0 -0
  201. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/markdown_export.py +0 -0
  202. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/named_ranges.py +0 -0
  203. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/style.py +0 -0
  204. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/docs/tables.py +0 -0
  205. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/__init__.py +0 -0
  206. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/activity.py +0 -0
  207. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/files.py +0 -0
  208. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/sharing.py +0 -0
  209. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/drive/transfer.py +0 -0
  210. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/gmail.py +0 -0
  211. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/pagination.py +0 -0
  212. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/response_limits.py +0 -0
  213. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/__init__.py +0 -0
  214. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/data.py +0 -0
  215. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/helpers.py +0 -0
  216. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/src/mcp_gee_sweet/tools/sheets/structure.py +0 -0
  217. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/__init__.py +0 -0
  218. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/conftest.py +0 -0
  219. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/__init__.py +0 -0
  220. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_activity.py +0 -0
  221. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_files.py +0 -0
  222. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_sharing.py +0 -0
  223. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/drive/test_transfer.py +0 -0
  224. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/fakes.py +0 -0
  225. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/__init__.py +0 -0
  226. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/conftest.py +0 -0
  227. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/integration/test_local_fs_harness.py +0 -0
  228. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/__init__.py +0 -0
  229. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_data.py +0 -0
  230. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_helpers.py +0 -0
  231. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/sheets/test_structure.py +0 -0
  232. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_auth.py +0 -0
  233. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_cache.py +0 -0
  234. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_calendar.py +0 -0
  235. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_check_supports_all_drives.py +0 -0
  236. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_concurrency.py +0 -0
  237. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_anchors.py +0 -0
  238. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_comments.py +0 -0
  239. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_editing.py +0 -0
  240. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_indices.py +0 -0
  241. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_layout.py +0 -0
  242. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_named_ranges.py +0 -0
  243. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_docs_style.py +0 -0
  244. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_gen_tool_docs.py +0 -0
  245. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_gmail.py +0 -0
  246. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_lane_context_hook.py +0 -0
  247. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_qa_gmail_fixtures.py +0 -0
  248. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_reauth.py +0 -0
  249. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_response_limits.py +0 -0
  250. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_server.py +0 -0
  251. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_server_json.py +0 -0
  252. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_sse_shutdown.py +0 -0
  253. {mcp_gee_sweet-0.9.2.dev283 → mcp_gee_sweet-0.9.2.dev289}/tests/test_team_mcp_configs.py +0 -0
@@ -2,7 +2,9 @@ Tech Writer (Amy). Reached via `/team-member Amy` after that command's shared is
2
2
 
3
3
  Amy keeps documentation accurate, friendly, comprehensive, and fun — README, `docs/`, tool docstrings surfaced to users, and QA test docs' clarity (not their results).
4
4
 
5
- - **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. Before picking one, check whether any open issues describe the same underlying problem from different angles (e.g. several tickets all pointing at one stale doc file) — if so, consolidate into a single new issue, close the redundant ones as `not planned` with a comment linking to the survivor, and work the consolidated issue instead of the originals (see #303 for precedent). Once duplicates are consolidated, pick the highest-priority remaining issue by this order, fixed so the choice doesn't need re-deriving each session: (1) issues labeled with the current in-progress version (e.g. `v0.9`) — an active release commitment; (2) issues with no version label — untriaged, but not explicitly deprioritized; (3) issues labeled `backlog` (Tier 4) last, since that label is an explicit no-assigned-version deprioritization. Within a tier, take the lowest issue number (oldest) first. 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).
5
+ - **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. Before picking one, check whether any open issues describe the same underlying problem from different angles (e.g. several tickets all pointing at one stale doc file) — if so, propose consolidating them into a single new issue, closing the redundant ones as `not planned` with a comment linking to the survivor, and working the consolidated issue instead of the originals (see #303 for precedent). After checking for duplicates, pick the highest-priority remaining issue by this order, fixed so the choice doesn't need re-deriving each session: (1) issues labeled with the current in-progress version (e.g. `v0.9`) — an active release commitment; (2) issues with no version label — untriaged, but not explicitly deprioritized; (3) issues labeled `backlog` (Tier 4) last, since that label is an explicit no-assigned-version deprioritization. Within a tier, take the lowest issue number (oldest) first. 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).
6
+
7
+ **Confirm the pick with the user before step 1.** Show the ticket you'd take (number, title, one line on the work), each higher-tier ticket you skipped and why, and any duplicate consolidation you'd do (which issues you'd close and what the new one would say). Then wait. Don't create a branch, edit a file, or open, close, comment on, or relabel an issue until the user gives an explicit go-ahead for that ticket in this conversation. The one exception: a message from Kai that names the ticket and says the user directed the handoff. That counts as the go-ahead. A request from any other peer, or a Kai message that doesn't say this, doesn't count. Why: Amy kept starting a ticket as soon as she launched, without checking with the user first (user correction, 2026-10-10).
6
8
  1. `git fetch origin develop && git checkout -b docs/amy/issue-<n> origin/develop`.
7
9
  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.
8
10
  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.
@@ -25,7 +25,7 @@ The incidents behind the rules in this file are in [`docs/decisions/role-file-hi
25
25
  - Team-process PRs (edits to `CLAUDE.md`, `.claude/team-roles/*.md`, `.claude/commands/*.md`) are Bob's own merge track, not Kai's — Bob reviews, labels `prompt-qa-approved`, and merges those himself (see `.claude/team-roles/bob.md` step 4). This used to be Kai's job too; per direct user instruction, 2026-07-21, it isn't anymore.
26
26
  - Cutting a release once Aziz has signed off (`/release`)
27
27
  - Worktree cleanup after merge (`cleanup-worktrees`) — this must never remove the persistent dev-team slot worktrees (the full `WORKTREE_ROLES` list in `scripts/setup_team.sh`, currently eight), only ad hoc worker worktrees
28
- - 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.
28
+ - 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. Message Amy to take a specific ticket only when the user has told you to, and say so in the message (e.g. "the user asked me to hand you #N"): `amy.md` treats only a handoff that names the ticket and says the user directed it as her go-ahead.
29
29
 
30
30
  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.
31
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-gee-sweet
3
- Version: 0.9.2.dev283
3
+ Version: 0.9.2.dev289
4
4
  Summary: MCP server for Google Workspace — Sheets, Drive, Docs (HTML or Markdown), and Calendar.
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
@@ -120,6 +120,23 @@ Three gaps were kept **as documented limitations, not implementation shortcuts**
120
120
  next to ordinary text in the same paragraph — the one case it can't resolve (a paragraph
121
121
  containing *only* a single inline code span, nothing else) is the genuinely ambiguous case
122
122
  documented above, not a bug in the heuristic.
123
+ - **Code delimiters** (#923, #940, #938): a code span's content is literal, so the serializer
124
+ can only pick a delimiter and padding. A fenced block's fence and an inline span's delimiter
125
+ are one backtick longer than the longest backtick run in the content (fences at least 3).
126
+ An inline span is padded with one space a side when its content starts or ends with a
127
+ backtick, or starts *and* ends with a space (CommonMark strips one space from each end in that
128
+ case). Adjacent code runs sharing a link merge into one span, since two spans side by side
129
+ would read as one. An inline code run's line breaks become `<br>` between separate spans: a
130
+ span can't hold a line break, and a raw newline could start a new block. In a table cell, a
131
+ code line containing `|` renders as raw HTML `<code>`, because the GFM `\|` escape shows its
132
+ backslash in renderers that don't implement it. Text inside raw HTML is still parsed as
133
+ Markdown, so every ASCII punctuation character in that line becomes a numeric entity
134
+ (`&#124;`, `&#42;`, `&#92;`, ...). Line breaks at either end of a linked code run go outside the
135
+ link. Because the exporter writes its own `<br>`/`<code>` HTML, plain text escapes `~`, a `<`
136
+ that could open a tag, and an `&` that starts an entity, so typed markup can't pass for
137
+ exporter markup. The export tests
138
+ check these against a strict CommonMark parser (`markdown-it-py`), since Python-Markdown
139
+ strips whitespace in code spans and closes a fence only on an exact-length match.
123
140
  - **Tight vs. loose lists**: consecutive `BulletItem` nodes are joined with a single `"\n"`
124
141
  rather than the `"\n\n"` used between other block types — CommonMark still parses a
125
142
  blank-line-separated run of list items as one list either way (just "loose", wrapped in `<p>`
@@ -1016,6 +1016,9 @@ Markdown pipe table. 3 rows, 2 columns: Name/Value, Alpha/1, Beta/2 — all corr
1016
1016
  **Result (2026-09-04) ✅ PASS**
1017
1017
  create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_structure confirmed HEADING_1 "QA Test Document", bold/italic runs, ☑/☐ bullet items, table (Col A/Col B, one/two). Trashed.
1018
1018
 
1019
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
1020
+ create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_structure: HEADING_1 "QA Test Document", bold/italic runs, ☑/☐/Plain item bullets, Col A/Col B table. Playwright (account check passed): heading, bold/italic, checkbox glyphs, and table render correctly. Trashed.
1021
+
1019
1022
  ---
1020
1023
 
1021
1024
  ### TC-DOC45: `create_doc_from_file` with a local .html file ⚠️ requires-oauth ⚠️ destructive
@@ -1036,6 +1039,9 @@ create_doc_from_file(tc-d195-create-doc.md): docId+web_link, no error. get_doc_s
1036
1039
  **Result (2026-09-04) ✅ PASS**
1037
1040
  create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc_structure confirmed HEADING_2 "From HTML file", paragraph "Content paragraph." Trashed.
1038
1041
 
1042
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
1043
+ create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph." Trashed.
1044
+
1039
1045
  ---
1040
1046
 
1041
1047
  ### TC-DOC46: `create_doc_from_file` file not found
@@ -1051,6 +1057,9 @@ create_doc_from_file(tc-d196-create-doc.html): docId+web_link, no error. get_doc
1051
1057
  **Result (2026-09-04) ✅ PASS**
1052
1058
  create_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception.
1053
1059
 
1060
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
1061
+ create_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception. Unsupported .txt also still returns the no-hint message "Use .md or .html/.htm" (shared helper, format_overridable=False).
1062
+
1054
1063
  ---
1055
1064
 
1056
1065
  ### TC-DOC47: `write_doc_content` inline code monospace ⚠️ destructive
@@ -2804,6 +2813,9 @@ insert_local_images (default revoke_sharing) on scratch doc with "Marker: IMGMAR
2804
2813
  **Result (2026-09-04) ✅ PASS**
2805
2814
  create_doc HTML table with image in a cell: no error. get_doc_structure: cell 0 "Before", cell 2 "After", cell 1 (image) empty (""). No images key in response (image dropped at parse time, table-cell images still out of scope). No timeout/hang. Trashed.
2806
2815
 
2816
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
2817
+ create_doc HTML table with an image in a cell: returned at once, no error, no `images` key. get_doc_structure: cell 0 "Before", cell 1 "", cell 2 "After". No hang. Trashed.
2818
+
2807
2819
  ---
2808
2820
 
2809
2821
  ### TC-DOC154: A `<pre>` block resumed after a nested table doesn't leave a spurious trailing whitespace-only paragraph ⚠️ requires-oauth ⚠️ destructive
@@ -3107,6 +3119,9 @@ Generated real ~76MB 4800x5000 (24.0MP, under 25MP pre-check) noise PNG. Main ca
3107
3119
  **Result (2026-09-04) ✅ PASS**
3108
3120
  update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: returned docId matching DOC_ID, no error. get_doc_structure matches TC-DOC44's create_doc_from_file output exactly (HEADING_1, bold/italic runs, ☑/☐ items, Col A/Col B table). get_file_metadata before/after identical name/parents/webViewLink — doc identity/location preserved.
3109
3121
 
3122
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3123
+ update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: docId matched, web_link present, no error. get_doc_structure matches TC-DOC44's output (HEADING_1, bold/italic, ☑/☐ items, Col A/Col B table). get_file_metadata before/after: same name/parents/webViewLink.
3124
+
3110
3125
  ### TC-DOC169: `update_doc_from_file` with a local .html file ⚠️ destructive
3111
3126
  **Prompt**
3112
3127
  > "Update Google Doc {DOC_ID} from the file <repo-root>/docs/qa/fixtures/tc-d196-create-doc.html"
@@ -3123,6 +3138,9 @@ update_doc_from_file(tc-d195-create-doc.md) on DOC_ID: returned docId matching D
3123
3138
  **Result (2026-09-04) ✅ PASS**
3124
3139
  update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph." — matches TC-DOC45.
3125
3140
 
3141
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3142
+ update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_structure: HEADING_2 "From HTML file", paragraph "Content paragraph."
3143
+
3126
3144
  ### TC-DOC170: `update_doc_from_file` file not found
3127
3145
  **Prompt**
3128
3146
  > "Update Google Doc {DOC_ID} from the file ~/does-not-exist.md"
@@ -3137,6 +3155,9 @@ update_doc_from_file(tc-d196-create-doc.html): docId matched, no error. get_doc_
3137
3155
  **Result (2026-09-04) ✅ PASS**
3138
3156
  update_doc_from_file('~/does-not-exist.md') returned {"error":"File not found: ~/does-not-exist.md"}, no exception. get_doc_content confirmed doc unchanged (still HEADING_2/paragraph from DOC169).
3139
3157
 
3158
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3159
+ update_doc_from_file('~/does-not-exist.md') returned {"error": "File not found: ~/does-not-exist.md"}, no exception. get_doc_content unchanged.
3160
+
3140
3161
  ### TC-DOC171: `update_doc_from_file` unsupported extension with no override returns an error, doc untouched
3141
3162
  **Setup:** create a local file `qa-update.txt` with arbitrary plain text
3142
3163
 
@@ -3155,6 +3176,9 @@ update_doc_from_file('~/does-not-exist.md') returned {"error":"File not found: ~
3155
3176
  **Result (2026-09-04) ✅ PASS**
3156
3177
  update_doc_from_file(qa-update.txt, no override) returned {"error":"Unsupported file extension '.txt'. Use .md or .html/.htm, or pass content_format explicitly."}. get_doc_content confirmed doc untouched — no Docs API call made.
3157
3178
 
3179
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3180
+ update_doc_from_file(qa-update.txt) returned {"error": "Unsupported file extension '.txt'. Use .md or .html/.htm, or pass content_format explicitly."}. get_doc_content unchanged.
3181
+
3158
3182
  ### TC-DOC172: `update_doc_from_file` `content_format` explicitly overrides an unrecognized extension ⚠️ destructive
3159
3183
  **Setup:** create a local file `qa-update-override.txt` containing `# Overridden Heading\n\nParagraph text.\n`
3160
3184
 
@@ -3171,6 +3195,9 @@ update_doc_from_file(qa-update.txt, no override) returned {"error":"Unsupported
3171
3195
  **Result (2026-09-04) ✅ PASS**
3172
3196
  update_doc_from_file(qa-update-override.txt, content_format='markdown') succeeded, no error. get_doc_structure: HEADING_1 "Overridden Heading", paragraph "Paragraph text." Fixture restored after.
3173
3197
 
3198
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3199
+ update_doc_from_file(qa-update-override.txt, content_format='markdown'): no error. get_doc_structure: HEADING_1 "Overridden Heading", paragraph "Paragraph text." Fixture restored to setup.md's seed HTML via write_doc_content afterward.
3200
+
3174
3201
  ---
3175
3202
 
3176
3203
  ## `get_doc_as_markdown`
@@ -3514,6 +3541,69 @@ Edge spacers (single <br> each): markdown = "| Trailing test<br> |\n| --- |\n| <
3514
3541
 
3515
3542
  ---
3516
3543
 
3544
+ ### TC-DOC206: Inline code keeps a space at both ends, and adjacent code runs export as one span (#938)
3545
+ **Setup:** raw HTML via `write_doc_content(doc_id={DOC_ID}, content_format='html', content="<p>pad <code> both ends </code> end</p><p>adj <code>x`</code><b><code>`y</code></b> end</p>")`. The bold change splits the second paragraph's code into two Courier New runs.
3546
+
3547
+ **Prompt**
3548
+ > "Export doc {DOC_ID} as Markdown"
3549
+
3550
+ **Checks**
3551
+ - `markdown` contains ``pad ` both ends ` end``: one extra space a side, since CommonMark strips one space from each end when both ends have one
3552
+ - `markdown` contains ```` adj ```x``y``` end ````: the two runs merge into one span, with a three-backtick delimiter around the two-backtick run
3553
+ - Rendering `markdown` with a strict CommonMark renderer (e.g. `markdown-it-py`'s `commonmark` preset, not Python `markdown`, which strips code-span whitespace) gives `<code> both ends </code>` and `<code>x``y</code>`
3554
+
3555
+ **Cleanup:** write fixture content back over `{DOC_ID}`
3556
+
3557
+ **Result (2026-10-10) ✅ PASS — Kit, PR #968 round 1 (`d2449f7`).** `get_doc_as_markdown` returned ``pad ` both ends ` end\n\nadj ```x``y``` end``. markdown-it-py 4.2.0's `commonmark` preset rendered `<code> both ends </code>` and `<code>x``y</code>`. Fixture content was written back.
3558
+
3559
+ **Result (2026-10-10) ✅ PASS — Kit, PR #968 round 2 (`759362a`).** Re-run because the fix changed `_render_run`. Output was identical to round 1. Fixture content was written back.
3560
+
3561
+ ---
3562
+
3563
+ ### TC-DOC207: Inline code in a table cell keeps a literal `|` and puts line breaks outside the code (#938)
3564
+ **Setup:** raw HTML via `write_doc_content(doc_id={DOC_ID}, content_format='html', content="<table><tr><td>Head</td></tr><tr><td>pipe <code>a|b</code> and a|b</td></tr><tr><td><code>x</code><br><code>y|z</code></td></tr></table>")`. The last cell has two code-styled paragraphs.
3565
+
3566
+ **Prompt**
3567
+ > "Export doc {DOC_ID} as Markdown"
3568
+
3569
+ **Checks**
3570
+ - `markdown` contains the row `| pipe <code>a&#124;b</code> and a\|b |`: the code's pipe is an HTML entity inside raw `<code>`, with no backslash, and the plain-text pipe is still escaped
3571
+ - `markdown` contains the row `` | `x`<br><code>y&#124;z</code> | ``: the cell's paragraph break is a `<br>` between two code elements, not inside one
3572
+ - Rendering `markdown` with a GFM-table renderer (e.g. `markdown-it-py`'s `commonmark` preset with `table` enabled) gives a one-column table whose cells contain `<code>a|b</code>` and `<code>x</code><br><code>y|z</code>`. No cell shows a backslash
3573
+
3574
+ Not covered live: a soft break (Shift+Enter) inside an inline code run in a paragraph that also has plain text. No tool can build one (`style_doc_range` doesn't set a font), so unit tests cover it (`TestInlineCodeCommonMark::test_newline_in_inline_code_does_not_start_a_new_block`).
3575
+
3576
+ **Cleanup:** write fixture content back over `{DOC_ID}`
3577
+
3578
+ **Result (2026-10-10) ✅ PASS (checks as written); ❌ probe beyond the case — Kit, PR #968 round 1 (`d2449f7`).** `get_doc_as_markdown` returned both expected rows exactly: `| pipe <code>a&#124;b</code> and a\|b |` and `` | `x`<br><code>y&#124;z</code> | ``. markdown-it-py's `commonmark` preset with `table` enabled rendered a one-column table with `<code>a|b</code>` and `<code>x</code><br><code>y|z</code>`, and no backslash. Probe: a cell with code `a|*b* _c_` exported as `<code>a&#124;*b* _c_</code>`, which renders `<em>` inside the `<code>`. A cell with code `C:\|x` exported as `<code>C:\&#124;x</code>`, which renders as `C:&amp;#124;x`. Both are sent back on the PR. Fixture content was written back.
3579
+
3580
+ **Result (2026-10-10) ✅ PASS — Kit, PR #968 round 2 (`759362a`).** Re-run because the fix changed the table-cell `<code>` path. Both expected rows came back exactly, and the render was the same as round 1. The round 1 probe is now TC-DOC208, which passes. Fixture content was written back.
3581
+
3582
+ ---
3583
+
3584
+ ### TC-DOC208: Markdown punctuation inside table-cell `<code>`, linked code after a cell break, and HTML-like plain text all export literally (#938)
3585
+ **Setup:** raw HTML via `write_doc_content(doc_id={DOC_ID}, content_format='html', content=...)` with this content. The `\` in `C:\|x` is one literal backslash character.
3586
+
3587
+ ```html
3588
+ <table><tr><td>Head</td></tr><tr><td><code>a|*b* _c_</code></td></tr><tr><td><code>C:\|x</code></td></tr><tr><td><code>x</code><br><a href="https://example.com/b"><code>b</code></a></td></tr></table><p>Use &lt;br&gt; or &amp;#124; and ~~old~~, AT&amp;T a &lt; b</p>
3589
+ ```
3590
+
3591
+ **Prompt**
3592
+ > "Export doc {DOC_ID} as Markdown"
3593
+
3594
+ **Checks**
3595
+ - `markdown` contains the row `| <code>a&#124;&#42;b&#42; &#95;c&#95;</code> |`: every punctuation character inside raw `<code>` is a numeric entity
3596
+ - `markdown` contains the row `| <code>C&#58;&#92;&#124;x</code> |`: the backslash is an entity too, so it can't escape the pipe's `&`
3597
+ - `markdown` contains the row `` | `x`<br>[`b`](https://example.com/b) | ``: the `<br>` from the cell's paragraph break is outside the link
3598
+ - `markdown` contains the paragraph `Use \<br> or \&#124; and \~\~old\~\~, AT&T a < b`: `<` before a tag, `&` starting an entity, and `~` are escaped; `AT&T` and `a < b` are not
3599
+ - Rendering `markdown` with `markdown-it-py`'s `commonmark` preset with `table` enabled gives cells `<code>a|*b* _c_</code>`, `<code>C:\|x</code>`, and `<code>x</code><br><a href="https://example.com/b"><code>b</code></a>`, with no `<em>` inside any `<code>`, and the paragraph renders as literal text (no line break, no `|`, no strikethrough)
3600
+
3601
+ **Cleanup:** write fixture content back over `{DOC_ID}`
3602
+
3603
+ **Result (2026-10-10) ✅ PASS — Kit, PR #968 round 2 (`759362a`).** `get_doc_as_markdown` returned all four expected lines exactly. markdown-it-py 4.2.0 (`commonmark` + `table` + `strikethrough`) rendered the cells `<code>a|*b* _c_</code>`, `<code>C:\|x</code>`, and `<code>x</code><br><a href="https://example.com/b"><code>b</code></a>`, and the paragraph `Use &lt;br&gt; or &amp;#124; and ~~old~~, AT&amp;T a &lt; b`, with no `<em>`, `<del>`, or line break. Fixture content was written back.
3604
+
3605
+ ---
3606
+
3517
3607
  ## Orphan fileId on inline-image sharing failure (#649)
3518
3608
 
3519
3609
  Mirrors #420's fix in `drive/transfer.py` (see TC-D249/TC-D250 in `docs/qa/tests/drive_transfer.md`) applied to the same create()-succeeds-but-follow-up-fails shape in `docs/images.py`'s `upload_and_share_image` and `docs/content.py`'s `_resolve_image_source`: a transient failure in the `permissions().create()`/`files().get()` sharing step, after `files().create()` (or an already-uploaded `_upload_local_file`) had already succeeded, previously returned a bare `{"error": ...}` with no way to find the Drive file that now genuinely exists. Not reliably reproducible live (would require forcing a transient API failure in the exact window between create and share); verified by unit test instead, matching TC-D249/TC-D250's own convention.
@@ -3747,12 +3837,15 @@ Tool call: `insert_local_images(doc_id={DOC_ID}, images=[{"marker": "IMGMARKERBO
3747
3837
  **Checks**
3748
3838
  - `results[0]` has no `fileId` and an `error` containing "14000x13000 (182.0 megapixels)" and "25 megapixels"
3749
3839
  - No `qa-bomb.png` appears in `{FOLDER_ID}`, and the marker text is unchanged
3750
- - Re-run with `auto_downscale=True`: `results[0]` still has no `fileId`, and its `error` ends with "Could not auto-downscale it (unreadable or corrupt image, animated, or too large to decode safely)."
3840
+ - Re-run with `auto_downscale=True`: `results[0]` still has no `fileId`, and its `error` ends with "Could not auto-downscale it (unreadable or corrupt image, animated, too large to decode safely, or still over the file-size limit after shrinking)." (the last reason was added by #894/#581)
3751
3841
 
3752
3842
  **Cleanup:** trash the doc; `rm /tmp/qa-bomb.png`
3753
3843
 
3754
3844
  **Result (2026-09-24) ✅ PASS — Kit, PR #801 round 2 (fix `4d5f842`, issue #560).** Both checks matched exactly, and nothing was uploaded. (Round 1 reproduced the pre-fix behavior locally: `check_image_file` returned `None` for this file.) Doc trashed.
3755
3845
 
3846
+ **Result (2026-10-10) ✅ PASS — PR #961 (Sky)**
3847
+ insert_local_images with the 14000x13000 PNG: results[0] no fileId, error names "14000x13000 (182.0 megapixels)" and "25 megapixels". With auto_downscale=True: error ends with "Could not auto-downscale it (unreadable or corrupt image, animated, too large to decode safely, or still over the file-size limit after shrinking)." No qa-bomb.png in FOLDER_ID; marker text unchanged. Doc trashed.
3848
+
3756
3849
  ---
3757
3850
 
3758
3851
  ### TC-DOC198: `insert_local_images` — an unreadable local file fails with "failed to read local file", not a raw upload error (PR #801)
@@ -3935,3 +4028,28 @@ Two extra probes ran from a scratch copy of the script (not committed):
3935
4028
  All files from both probes and the three steps, and the doc, were trashed.
3936
4029
 
3937
4030
  **Result (2026-10-04) ✅ PASS — Kit, PR #903 round 2 (`4e392c3`).** All six steps ran from the kit worktree with its OAuth token, on a fresh scratch doc in `{FOLDER_ID}`. Each step printed `cancelled at '<point>'; the tool returned nothing` and the WARNING `Doc edit for {DOC_ID} failed or was cancelled after its images were resolved; files created by this call: <one file ID>; pre-existing files: none; still link-shared by this call: ...`. That last field listed `{KEPT_FILE_ID}` for step 4 and was `none` for every other step. Steps 2 (`share-fail`, the orphan) and 6 (`revoke`), which logged nothing in round 1's probe, now log. `list_permissions` showed no `anyone` grant on `{SHARE_FILE_ID}`, `{ORPHAN_FILE_ID}`, `{EDIT_FILE_ID}`, `{INFLIGHT_FILE_ID}`, or `{REVOKE_FILE_ID}`, and showed `anyoneWithLink`/`reader` on `{KEPT_FILE_ID}`. After `refresh_cache`, `get_doc_as_markdown` read `Before`, `IMGMARKER883`, an image, an image, `After`. So steps 1–4 left `IMGMARKER883` untouched, step 6's edit landed before its cancel, and step 5's in-flight edit also landed (an image replaced `INFLIGHT883`). All six files and the doc were trashed.
4031
+
4032
+ ## Unreadable source file returns an error — `create_doc_from_file` / `update_doc_from_file` (PR #961)
4033
+
4034
+ Both tools read the local file through the shared `_read_doc_source_file` helper. Before PR #961's round 2, a path that existed but wasn't a readable UTF-8 regular file raised out of the tool (`[Errno 21] Is a directory`, `'utf-8' codec can't decode byte 0xe9 ...`) instead of returning `{"error": ...}`.
4035
+
4036
+ ### TC-DOC208: A directory or non-UTF-8 source file returns `{"error": ...}`, and nothing is created or edited
4037
+ **Setup:**
4038
+ - `mkdir -p /tmp/tc-doc208/dir.md`
4039
+ - `python3 -c "open('/tmp/tc-doc208/latin1.html','wb').write('<p>caf\xe9</p>'.encode('latin-1'))"`
4040
+ - `get_doc_content(file_id={DOC_ID})`, note the content for comparison afterward
4041
+
4042
+ **Steps**
4043
+ 1. `create_doc_from_file(local_path="/tmp/tc-doc208/dir.md", folder_id={FOLDER_ID})`
4044
+ 2. `create_doc_from_file(local_path="/tmp/tc-doc208/latin1.html", folder_id={FOLDER_ID})`
4045
+ 3. `update_doc_from_file(doc_id={DOC_ID}, local_path="/tmp/tc-doc208/dir.md")`
4046
+ 4. `update_doc_from_file(doc_id={DOC_ID}, local_path="/tmp/tc-doc208/latin1.html")`
4047
+
4048
+ **Checks**
4049
+ - Each call returns a dict with an `error` key naming the path (any wording). None comes back as `Error executing tool ...`
4050
+ - `list_files(folder_id={FOLDER_ID})` shows no new Doc named `dir` or `latin1` (the filename stem `create_doc_from_file` uses as the default title; steps 1–2 fail before the Drive `create`)
4051
+ - `get_doc_content(file_id={DOC_ID})` matches the Setup snapshot (steps 3–4 fail before any Docs API call)
4052
+
4053
+ **Cleanup:** `rm -rf /tmp/tc-doc208`
4054
+
4055
+ **Result (2026-10-10) ✅ PASS — Sky, PR #961 round 2 (`849f0bb`).** Ran with the fixtures under this job's scratch dir instead of `/tmp/tc-doc208`. Steps 1 and 3 returned `{"error": "Not a regular file: <path>/dir.md"}`; steps 2 and 4 returned `{"error": "File is not valid UTF-8: <path>/latin1.html (invalid continuation byte at byte 6)"}`. No call surfaced as `Error executing tool`. `list_files({FOLDER_ID})` showed only the three standing fixtures. After `refresh_cache`, `get_doc_content({DOC_ID})` matched the Setup snapshot. Round 1 reproduced both raises on the pre-fix code (`075ce99`).
@@ -192,7 +192,7 @@ Same shape as v0.8.1 — stabilize on defects that surfaced since v0.9.0 shipped
192
192
  - [x] `create_paragraph_bullets` (`style.py`) has the same isolated-run collapse-to-`nestingLevel 0` bug #439 fixed in `emitter.py` only — port PR #711's anchor-paragraph approach to the sibling path (PR #723) ([#713](https://github.com/khuisman/mcp-gee-sweet/issues/713))
193
193
  - [x] Lock in the isolated-run rendering rule — an isolated bullet run renders at its true depth (`depth N` → `nestingLevel N`) at *every* depth, never clamped (decided 2026-09-09: consistency over avoiding orphaned indent; the markup generator owns the depth it emits). Extend TC-DOC190 with a `min_depth >= 2` case; state the rule in `emitter.py`'s bullet-pass comment and the `docs/emitter.py` paragraph of `CLAUDE.md` (PR #731) ([#714](https://github.com/khuisman/mcp-gee-sweet/issues/714))
194
194
  - [x] `create_doc` markdown: a fenced code block whose body is raw HTML tags renders as loose plain paragraphs (fence ignored, indentation flattened) instead of a monospaced code block — python-markdown's raw-HTML-block handling appears to pre-empt `fenced_code`. Surfaced during #714 triage (PR #730) ([#719](https://github.com/khuisman/mcp-gee-sweet/issues/719))
195
- - [ ] **Bundle:** Docs write side (emitter, html_parser, images, content), one PR closing [#509](https://github.com/khuisman/mcp-gee-sweet/issues/509), [#567](https://github.com/khuisman/mcp-gee-sweet/issues/567), [#581](https://github.com/khuisman/mcp-gee-sweet/issues/581), [#638](https://github.com/khuisman/mcp-gee-sweet/issues/638), [#715](https://github.com/khuisman/mcp-gee-sweet/issues/715) — lane A ([#894](https://github.com/khuisman/mcp-gee-sweet/issues/894))
195
+ - [x] **Bundle:** Docs write side (emitter, html_parser, images, content), one PR closing [#509](https://github.com/khuisman/mcp-gee-sweet/issues/509), [#567](https://github.com/khuisman/mcp-gee-sweet/issues/567), [#581](https://github.com/khuisman/mcp-gee-sweet/issues/581), [#638](https://github.com/khuisman/mcp-gee-sweet/issues/638), [#715](https://github.com/khuisman/mcp-gee-sweet/issues/715) — lane A (PR #961) ([#894](https://github.com/khuisman/mcp-gee-sweet/issues/894))
196
196
  - [x] **Bundle:** Calendar hardening, one PR closing [#459](https://github.com/khuisman/mcp-gee-sweet/issues/459), [#615](https://github.com/khuisman/mcp-gee-sweet/issues/615), [#626](https://github.com/khuisman/mcp-gee-sweet/issues/626), [#627](https://github.com/khuisman/mcp-gee-sweet/issues/627), [#726](https://github.com/khuisman/mcp-gee-sweet/issues/726) — lane B (PR #912) ([#890](https://github.com/khuisman/mcp-gee-sweet/issues/890))
197
197
  - [x] `delete_file(permanent=True)` surfaces a misleading 404 when the caller can trash but not permanently delete (Shared Drive Content manager) — an agent can read it as "already gone" and skip cleanup; check `capabilities.canDelete` first — lane B ([#876](https://github.com/khuisman/mcp-gee-sweet/issues/876)) (PR #888)
198
198
  - [x] SSE: SIGTERM with an SSE stream open logs an "Exception in ASGI application" traceback; the server still exits cleanly, pre-existing before #833 — lane A (PR #911) ([#868](https://github.com/khuisman/mcp-gee-sweet/issues/868))
@@ -351,7 +351,7 @@ Requires `gmail/v1` client and `https://www.googleapis.com/auth/gmail.modify` sc
351
351
  - [ ] Define the correctness rubric and own execution for the composite-vs-primitive efficiency benchmark — Aziz ([#584](https://github.com/khuisman/mcp-gee-sweet/issues/584))
352
352
  - [ ] QA `.env` has no single source of truth — `setup_team.sh` only copies `.env` into a worktree when absent, so fixture-ID changes never propagate (had to hand-sync 9 copies for #305); also `run.md` vs `setup.md` disagree on `docs/qa/.env` vs repo-root `.env`. Surfaced by PR #677, relates to [#648](https://github.com/khuisman/mcp-gee-sweet/issues/648) ([#679](https://github.com/khuisman/mcp-gee-sweet/issues/679))
353
353
  - [x] `list_files`/`get_file_metadata` docstrings cite a stale `upload_local_file` `modifiedTime` caveat (fixed at the root by PR #472) and carry a broken self-referential contrast — from Bob's `docs/tools.md` docstring sweep; Dev/QA track, batches with #682/#683 ([#681](https://github.com/khuisman/mcp-gee-sweet/issues/681)) (PR 898)
354
- - [ ] `create_doc` family docstrings name only the ~25MP inline-image limit; the ~50MB byte-size limit (#562) is equally enforced, and 3 deferring docstrings inherit the gap — from Bob's `docs/tools.md` docstring sweep. Routed to Amy (`documentation`); closed community PR #932 has a diff ([#682](https://github.com/khuisman/mcp-gee-sweet/issues/682))
354
+ - [x] `create_doc` family docstrings name only the ~25MP inline-image limit; the ~50MB byte-size limit (#562) is equally enforced, and 3 deferring docstrings inherit the gap — from Bob's `docs/tools.md` docstring sweep. Routed to Amy (`documentation`); closed community PR #932 has a diff ([#682](https://github.com/khuisman/mcp-gee-sweet/issues/682)) (PR 970)
355
355
  - [ ] `qa(docs)`: no comprehensive fixture exercising every Docs formatting construct in one pass — Aziz, triaged out of `backlog` 2026-09-11 ([#477](https://github.com/khuisman/mcp-gee-sweet/issues/477))
356
356
  - [ ] Fact-check and word competitive/benchmark claims once real benchmark numbers exist — routed to Amy, triaged out of `backlog` 2026-09-11 ([#585](https://github.com/khuisman/mcp-gee-sweet/issues/585))
357
357
  - [x] QA test-doc cross-reference cleanup after the `drive.md`/`docs.md` split (PR #582) — triaged out of `backlog` 2026-09-11 ([#590](https://github.com/khuisman/mcp-gee-sweet/issues/590)) (PR #851)
@@ -97,4 +97,5 @@ dev = [
97
97
  "pytest-cov>=7.1.0",
98
98
  "ruff>=0.16.5",
99
99
  "tomli>=2.0.1; python_version < '3.11'",
100
+ "markdown-it-py>=3.0.0",
100
101
  ]
@@ -21,11 +21,21 @@ Known, deliberate gaps (documented rather than silently dropped):
21
21
  from __future__ import annotations
22
22
 
23
23
  import re
24
+ import string
24
25
 
25
26
  from .ast import BulletItem, Cell, DocNode, Heading, Image, NamedBlock, Paragraph, Run, Table
26
27
  from .emitter import _build_phantom_set
27
28
 
28
- _MD_ESCAPE = re.compile(r"([\\`*_\[\]])")
29
+ # Characters in plain text that Markdown would otherwise read as syntax. "~" is
30
+ # GFM strikethrough. "<" only when it could open an HTML tag or autolink, and
31
+ # "&" only when it starts an entity reference, so ordinary prose ("a < b",
32
+ # "AT&T") stays readable. Exported text must not be mistaken for the <br> and
33
+ # <code> HTML the exporter writes itself (#938).
34
+ _MD_ESCAPE = re.compile(
35
+ r"[\\`*_\[\]~]"
36
+ r"|<(?=[A-Za-z/!?])"
37
+ r"|&(?=#[0-9]{1,7};|#[xX][0-9a-fA-F]{1,6};|[A-Za-z][A-Za-z0-9]*;)"
38
+ )
29
39
  _BACKTICK_RUN = re.compile(r"`+")
30
40
 
31
41
  # A paragraph whose rendered text starts with one of these (unescaped) would be
@@ -93,23 +103,16 @@ def _render_code_block(node: Paragraph) -> str:
93
103
  Run text did."""
94
104
  segments: list[str] = []
95
105
  buffer = ""
96
-
97
- def fenced(text: str) -> str:
98
- # Literal fences in the content must never close the surrounding block.
99
- ticks = max((len(match.group()) for match in _BACKTICK_RUN.finditer(text)), default=0)
100
- fence = "`" * max(3, ticks + 1)
101
- return f"{fence}\n{text}\n{fence}"
102
-
103
106
  for item in node.runs:
104
107
  if isinstance(item, Run):
105
108
  buffer += item.text
106
109
  else:
107
110
  if buffer:
108
- segments.append(fenced(buffer))
111
+ segments.append(_fenced_code_block(buffer))
109
112
  buffer = ""
110
113
  segments.append(f"![{_escape(item.alt or '')}]({_md_link_dest(item.src)})")
111
114
  if buffer:
112
- segments.append(fenced(buffer))
115
+ segments.append(_fenced_code_block(buffer))
113
116
  return "\n\n".join(segments)
114
117
 
115
118
 
@@ -171,7 +174,7 @@ def _md_link_dest(url: str) -> str:
171
174
 
172
175
  def _render_inline(items: list[Run | Image]) -> str:
173
176
  parts: list[str] = []
174
- for item in items:
177
+ for item in _coalesce_code_runs(items):
175
178
  if isinstance(item, Image):
176
179
  parts.append(f"![{_escape(item.alt or '')}]({_md_link_dest(item.src)})")
177
180
  else:
@@ -179,22 +182,63 @@ def _render_inline(items: list[Run | Image]) -> str:
179
182
  return "".join(parts)
180
183
 
181
184
 
182
- def _render_run(run: Run) -> str:
185
+ def _is_code_run(item: Run | Image | Table) -> bool:
186
+ return isinstance(item, Run) and item.font_family == "Courier New"
187
+
188
+
189
+ def _coalesce_code_runs(items: list) -> list:
190
+ """Merge adjacent code runs that share a link. Docs splits a run at any
191
+ style change (bold, color), but rendered side by side two code spans
192
+ would read as one: `` `a``b` `` parses as the single span "a``b", since
193
+ the first span's closing delimiter and the second's opening one form one
194
+ backtick run. A code span can't carry those styles anyway, so the merge
195
+ loses nothing. Runs with different links stay apart; the link brackets
196
+ already separate them."""
197
+ merged: list = []
198
+ for item in items:
199
+ prev = merged[-1] if merged else None
200
+ if (
201
+ _is_code_run(item)
202
+ and prev is not None
203
+ and _is_code_run(prev)
204
+ and prev.link_url == item.link_url
205
+ ):
206
+ merged[-1] = Run(
207
+ text=prev.text + item.text, font_family="Courier New", link_url=prev.link_url
208
+ )
209
+ else:
210
+ merged.append(item)
211
+ return merged
212
+
213
+
214
+ def _render_run(run: Run, in_table: bool = False) -> str:
215
+ """Markdown for one run. `in_table` makes the result safe inside a
216
+ pipe-table cell (see `_table_safe`)."""
183
217
  if run.text == "":
184
218
  return ""
185
219
  if run.font_family == "Courier New":
186
- # Use a delimiter longer than any literal backtick sequence in the
187
- # content, and separate edge backticks from that delimiter.
188
- ticks = (
189
- max((len(match.group()) for match in _BACKTICK_RUN.finditer(run.text)), default=0) + 1
190
- )
191
- fence = "`" * ticks
192
- content = run.text
193
- if content.startswith("`") or content.endswith("`"):
194
- content = f" {content} "
195
- body = f"{fence}{content}{fence}"
196
- return f"[{body}]({_md_link_dest(run.link_url)})" if run.link_url else body
220
+ return _render_code_run(run, in_table)
221
+ body = _render_plain_run(run)
222
+ return _table_safe(body) if in_table else body
223
+
224
+
225
+ def _render_code_run(run: Run, in_table: bool) -> str:
226
+ # Line breaks at either end go outside the link, so a cell's paragraph
227
+ # break (a leading "\n" from doc_to_ast) or a trailing soft break doesn't
228
+ # render as a <br> inside the <a>.
229
+ inner = run.text.strip("\n")
230
+ if not inner:
231
+ return "<br>" * len(run.text)
232
+ lead = "<br>" * (len(run.text) - len(run.text.lstrip("\n")))
233
+ trail = "<br>" * (len(run.text) - len(run.text.rstrip("\n")))
234
+ body = _render_code_run_text(inner, in_table)
235
+ if run.link_url:
236
+ dest = _md_link_dest(run.link_url)
237
+ body = f"[{body}]({_table_safe(dest) if in_table else dest})"
238
+ return f"{lead}{body}{trail}"
239
+
197
240
 
241
+ def _render_plain_run(run: Run) -> str:
198
242
  # CommonMark emphasis delimiters can't have whitespace on the inner side
199
243
  # (e.g. "**Bold **" doesn't close — the space right before "**" means it
200
244
  # renders as literal asterisks, not bold). Keep leading/trailing
@@ -220,7 +264,64 @@ def _render_run(run: Run) -> str:
220
264
 
221
265
 
222
266
  def _escape(text: str) -> str:
223
- return _MD_ESCAPE.sub(r"\\\1", text)
267
+ return _MD_ESCAPE.sub(lambda m: "\\" + m.group(), text)
268
+
269
+
270
+ def _longest_backtick_run(text: str) -> int:
271
+ return max((len(match.group()) for match in _BACKTICK_RUN.finditer(text)), default=0)
272
+
273
+
274
+ def _fenced_code_block(text: str) -> str:
275
+ # A fence must be longer than any backtick run in the content, or a
276
+ # literal ``` line inside the block would close it early.
277
+ fence = "`" * max(3, _longest_backtick_run(text) + 1)
278
+ return f"{fence}\n{text}\n{fence}"
279
+
280
+
281
+ def _code_span(text: str) -> str:
282
+ """One CommonMark code span for `text` (which has no newline).
283
+
284
+ A code span's content is literal: backslash escapes don't work inside
285
+ it, so the only tools are the delimiter length and padding. The
286
+ delimiter is one backtick longer than the longest run in the content, so
287
+ no literal run can close the span. Padding is exactly one space a side
288
+ because CommonMark strips exactly one space from each end when both ends
289
+ have one (and the content isn't all spaces). That stripping is what
290
+ separates an edge backtick from the delimiter, and it would also eat a
291
+ real leading and trailing space, so both cases get padded."""
292
+ fence = "`" * (_longest_backtick_run(text) + 1)
293
+ pad = text.startswith("`") or text.endswith("`")
294
+ pad = pad or (text.startswith(" ") and text.endswith(" ") and text.strip(" ") != "")
295
+ if pad:
296
+ text = f" {text} "
297
+ return f"{fence}{text}{fence}"
298
+
299
+
300
+ def _render_code_run_text(text: str, in_table: bool) -> str:
301
+ """Inline code for `text`, which may span lines.
302
+
303
+ A code span can't hold a line break: CommonMark turns one into a space,
304
+ and in a paragraph a raw newline can start a new block (a "# " heading,
305
+ a "- " item) before the span is parsed. So each line becomes its own
306
+ span, joined by <br> the same way a table cell joins its lines.
307
+
308
+ In a table cell, a "|" splits the cell before inline parsing, and the
309
+ GFM `\\|` escape inside a code span shows its backslash in renderers
310
+ that don't implement that rule. A line holding a "|" is written as raw
311
+ HTML <code> instead, which no table parser splits on. Text inside raw
312
+ HTML is still parsed as Markdown, so every ASCII punctuation character
313
+ becomes a numeric entity: an entity is never Markdown syntax, and a
314
+ backslash can't escape the "&" that starts one."""
315
+ spans: list[str] = []
316
+ for line in text.split("\n"):
317
+ if not line:
318
+ spans.append("")
319
+ elif in_table and "|" in line:
320
+ escaped = "".join(f"&#{ord(c)};" if c in string.punctuation else c for c in line)
321
+ spans.append(f"<code>{escaped}</code>")
322
+ else:
323
+ spans.append(_code_span(line))
324
+ return "<br>".join(spans)
224
325
 
225
326
 
226
327
  def _render_table(table: Table) -> str:
@@ -249,29 +350,38 @@ def _render_table(table: Table) -> str:
249
350
 
250
351
  def _render_cell(cell: Cell) -> str:
251
352
  parts: list[str] = []
252
- for child in cell.children:
353
+ for child in _coalesce_code_runs(cell.children):
253
354
  if isinstance(child, Table):
254
355
  parts.append(_NESTED_TABLE_PLACEHOLDER)
255
356
  elif isinstance(child, Image):
256
- parts.append(f"![{_escape(child.alt or '')}]({_md_link_dest(child.src)})")
357
+ parts.append(_table_safe(f"![{_escape(child.alt or '')}]({_md_link_dest(child.src)})"))
257
358
  else:
258
- parts.append(_render_run(child))
259
- # Pipe-table cells are single-line: a literal newline (from a multi-
260
- # paragraph cell, see doc_to_ast.py's _cell_content_to_children) would
261
- # break the table's row structure, so it becomes a <br> instead. "|" is
262
- # escaped for the same structural reason.
263
- #
264
- # \n -> <br> conversion must happen BEFORE strip(), not after (#594 QA
265
- # round 2): a spacer paragraph at the very start or end of a cell (as
266
- # opposed to one sandwiched between two real lines) shows up here as a
267
- # leading/trailing "\n" on the joined text — stripping first, as an
268
- # earlier version of this function did, silently ate exactly that
269
- # newline, making an edge spacer indistinguishable from a cell with no
270
- # spacer at all. "<br>" is not whitespace, so converting first means
271
- # strip() only removes genuine incidental whitespace, never a spacer
272
- # that's already been turned into a <br> marker.
273
- text = "".join(parts).replace("\n", "<br>").replace("|", "\\|")
274
- return text.strip()
359
+ parts.append(_render_run(child, in_table=True))
360
+ # Every part is already table-safe, with its newlines turned into <br>,
361
+ # so strip() here only removes incidental whitespace, never an edge
362
+ # spacer paragraph (#594 QA round 2; see _table_safe).
363
+ return "".join(parts).strip()
364
+
365
+
366
+ def _table_safe(text: str) -> str:
367
+ """Make rendered non-code Markdown safe inside a pipe-table cell.
368
+
369
+ Pipe-table cells are single-line: a literal newline (from a multi-
370
+ paragraph cell, see doc_to_ast.py's _cell_content_to_children) would
371
+ break the table's row structure, so it becomes a <br> instead. "|" is
372
+ escaped for the same structural reason.
373
+
374
+ This runs per part, before _render_cell's strip(), not on the joined
375
+ cell text after it (#594 QA round 2): a spacer paragraph at the very
376
+ start or end of a cell shows up as a leading/trailing "\\n", and
377
+ stripping first silently ate exactly that newline. "<br>" is not
378
+ whitespace, so converting first keeps the spacer.
379
+
380
+ Code runs never go through this: _render_code_run_text writes its own
381
+ <br> and table-safe <code>, and escaping its output again would put a
382
+ literal backslash inside the code (#938). Only a code link's
383
+ destination is passed through here."""
384
+ return text.replace("\n", "<br>").replace("|", "\\|")
275
385
 
276
386
 
277
387
  def _render_comment(comment: dict) -> str: