interlatch 0.27.0__tar.gz → 0.27.2__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 (454) hide show
  1. {interlatch-0.27.0 → interlatch-0.27.2}/CHANGELOG.md +15 -0
  2. {interlatch-0.27.0 → interlatch-0.27.2}/PKG-INFO +1 -1
  3. interlatch-0.27.2/changelog.d/hub-browser-sessions.md +4 -0
  4. {interlatch-0.27.0 → interlatch-0.27.2}/docs/analysis.md +5 -3
  5. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/analysis.md +4 -3
  6. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/bob.py +32 -1
  7. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/settings.py +8 -0
  8. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/commands/knowledge.py +2 -2
  9. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/commands/operations.py +4 -0
  10. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/commands/recording.py +2 -2
  11. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/commands/setup.py +32 -2
  12. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/common.py +17 -0
  13. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/hub_admin.py +8 -4
  14. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/handler.py +8 -4
  15. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/routes.py +1 -1
  16. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/people.py +14 -5
  17. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/app.css +15 -2
  18. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/app.js +23 -3
  19. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/ja.js +6 -0
  20. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_bob_analysis.py +29 -2
  21. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_people.py +14 -0
  22. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_people_web.py +23 -0
  23. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_setup.py +36 -0
  24. interlatch-0.27.0/changelog.d/party-formations.md +0 -4
  25. {interlatch-0.27.0 → interlatch-0.27.2}/.github/dependabot.yml +0 -0
  26. {interlatch-0.27.0 → interlatch-0.27.2}/.github/workflows/changelog.yml +0 -0
  27. {interlatch-0.27.0 → interlatch-0.27.2}/.github/workflows/ci.yml +0 -0
  28. {interlatch-0.27.0 → interlatch-0.27.2}/.github/workflows/docs.yml +0 -0
  29. {interlatch-0.27.0 → interlatch-0.27.2}/.github/workflows/release.yml +0 -0
  30. {interlatch-0.27.0 → interlatch-0.27.2}/.gitignore +0 -0
  31. {interlatch-0.27.0 → interlatch-0.27.2}/.pre-commit-config.yaml +0 -0
  32. {interlatch-0.27.0 → interlatch-0.27.2}/.python-version +0 -0
  33. {interlatch-0.27.0 → interlatch-0.27.2}/CONTRIBUTING.md +0 -0
  34. {interlatch-0.27.0 → interlatch-0.27.2}/LICENSE +0 -0
  35. {interlatch-0.27.0 → interlatch-0.27.2}/README.ja.md +0 -0
  36. {interlatch-0.27.0 → interlatch-0.27.2}/README.md +0 -0
  37. {interlatch-0.27.0 → interlatch-0.27.2}/ROADMAP.ja.md +0 -0
  38. {interlatch-0.27.0 → interlatch-0.27.2}/ROADMAP.md +0 -0
  39. {interlatch-0.27.0 → interlatch-0.27.2}/SECURITY.md +0 -0
  40. {interlatch-0.27.0 → interlatch-0.27.2}/careers/README.md +0 -0
  41. {interlatch-0.27.0 → interlatch-0.27.2}/careers/boards.json +0 -0
  42. {interlatch-0.27.0 → interlatch-0.27.2}/careers/pipeline.py +0 -0
  43. {interlatch-0.27.0 → interlatch-0.27.2}/changelog.d/README.md +0 -0
  44. {interlatch-0.27.0 → interlatch-0.27.2}/dev.sh +0 -0
  45. {interlatch-0.27.0 → interlatch-0.27.2}/docker/.dockerignore +0 -0
  46. {interlatch-0.27.0 → interlatch-0.27.2}/docker/.env.example +0 -0
  47. {interlatch-0.27.0 → interlatch-0.27.2}/docker/Caddyfile +0 -0
  48. {interlatch-0.27.0 → interlatch-0.27.2}/docker/Dockerfile +0 -0
  49. {interlatch-0.27.0 → interlatch-0.27.2}/docker/compose.yaml +0 -0
  50. {interlatch-0.27.0 → interlatch-0.27.2}/docker/wheels/.gitignore +0 -0
  51. {interlatch-0.27.0 → interlatch-0.27.2}/docs/README.md +0 -0
  52. {interlatch-0.27.0 → interlatch-0.27.2}/docs/_site/hooks.py +0 -0
  53. {interlatch-0.27.0 → interlatch-0.27.2}/docs/cli.md +0 -0
  54. {interlatch-0.27.0 → interlatch-0.27.2}/docs/configuration.md +0 -0
  55. {interlatch-0.27.0 → interlatch-0.27.2}/docs/dashboard.md +0 -0
  56. {interlatch-0.27.0 → interlatch-0.27.2}/docs/demo/make_demo.py +0 -0
  57. {interlatch-0.27.0 → interlatch-0.27.2}/docs/demo/record_demo.py +0 -0
  58. {interlatch-0.27.0 → interlatch-0.27.2}/docs/design/ja/learning.md +0 -0
  59. {interlatch-0.27.0 → interlatch-0.27.2}/docs/design/learning.md +0 -0
  60. {interlatch-0.27.0 → interlatch-0.27.2}/docs/development.md +0 -0
  61. {interlatch-0.27.0 → interlatch-0.27.2}/docs/devices.md +0 -0
  62. {interlatch-0.27.0 → interlatch-0.27.2}/docs/diagrams/analysis.excalidraw.svg +0 -0
  63. {interlatch-0.27.0 → interlatch-0.27.2}/docs/diagrams/architecture.excalidraw.svg +0 -0
  64. {interlatch-0.27.0 → interlatch-0.27.2}/docs/docker.md +0 -0
  65. {interlatch-0.27.0 → interlatch-0.27.2}/docs/faq.md +0 -0
  66. {interlatch-0.27.0 → interlatch-0.27.2}/docs/images/demo.gif +0 -0
  67. {interlatch-0.27.0 → interlatch-0.27.2}/docs/images/home.png +0 -0
  68. {interlatch-0.27.0 → interlatch-0.27.2}/docs/images/map.png +0 -0
  69. {interlatch-0.27.0 → interlatch-0.27.2}/docs/images/palette.png +0 -0
  70. {interlatch-0.27.0 → interlatch-0.27.2}/docs/images/session.png +0 -0
  71. {interlatch-0.27.0 → interlatch-0.27.2}/docs/install.md +0 -0
  72. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/README.md +0 -0
  73. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/cli.md +0 -0
  74. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/configuration.md +0 -0
  75. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/dashboard.md +0 -0
  76. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/development.md +0 -0
  77. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/devices.md +0 -0
  78. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/docker.md +0 -0
  79. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/faq.md +0 -0
  80. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/install.md +0 -0
  81. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/join-a-hub.md +0 -0
  82. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/mcp.md +0 -0
  83. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/moving-from-chronicle.md +0 -0
  84. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/party.md +0 -0
  85. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/postgres.md +0 -0
  86. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/privacy.md +0 -0
  87. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/sources.md +0 -0
  88. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/suggestions.md +0 -0
  89. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/troubleshooting.md +0 -0
  90. {interlatch-0.27.0 → interlatch-0.27.2}/docs/ja/vscode.md +0 -0
  91. {interlatch-0.27.0 → interlatch-0.27.2}/docs/join-a-hub.md +0 -0
  92. {interlatch-0.27.0 → interlatch-0.27.2}/docs/mcp.md +0 -0
  93. {interlatch-0.27.0 → interlatch-0.27.2}/docs/moving-from-chronicle.md +0 -0
  94. {interlatch-0.27.0 → interlatch-0.27.2}/docs/party.md +0 -0
  95. {interlatch-0.27.0 → interlatch-0.27.2}/docs/postgres.md +0 -0
  96. {interlatch-0.27.0 → interlatch-0.27.2}/docs/privacy.md +0 -0
  97. {interlatch-0.27.0 → interlatch-0.27.2}/docs/sources.md +0 -0
  98. {interlatch-0.27.0 → interlatch-0.27.2}/docs/suggestions.md +0 -0
  99. {interlatch-0.27.0 → interlatch-0.27.2}/docs/troubleshooting.md +0 -0
  100. {interlatch-0.27.0 → interlatch-0.27.2}/docs/vscode.md +0 -0
  101. {interlatch-0.27.0 → interlatch-0.27.2}/leaderboard/.firebaserc +0 -0
  102. {interlatch-0.27.0 → interlatch-0.27.2}/leaderboard/.gitignore +0 -0
  103. {interlatch-0.27.0 → interlatch-0.27.2}/leaderboard/README.md +0 -0
  104. {interlatch-0.27.0 → interlatch-0.27.2}/leaderboard/firebase.json +0 -0
  105. {interlatch-0.27.0 → interlatch-0.27.2}/leaderboard/firestore.indexes.json +0 -0
  106. {interlatch-0.27.0 → interlatch-0.27.2}/leaderboard/firestore.rules +0 -0
  107. {interlatch-0.27.0 → interlatch-0.27.2}/mkdocs.yml +0 -0
  108. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/README.md +0 -0
  109. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/art.json +0 -0
  110. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/filing.html +0 -0
  111. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/loops.json +0 -0
  112. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/loops.py +0 -0
  113. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/members.js +0 -0
  114. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/render.html +0 -0
  115. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/render.py +0 -0
  116. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/scenes-more.js +0 -0
  117. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/icons3d/scenes.html +0 -0
  118. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/Chronicle.icns +0 -0
  119. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/Interlatch.spec +0 -0
  120. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/build.sh +0 -0
  121. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/entitlements.plist +0 -0
  122. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/icon-3d.webp +0 -0
  123. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/icon-blueprint-prompt.md +0 -0
  124. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/icon-blueprint.webp +0 -0
  125. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/icon.png +0 -0
  126. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/interlatch_app.py +0 -0
  127. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/macos/make_icon.py +0 -0
  128. {interlatch-0.27.0 → interlatch-0.27.2}/packaging/release.py +0 -0
  129. {interlatch-0.27.0 → interlatch-0.27.2}/pyproject.toml +0 -0
  130. {interlatch-0.27.0 → interlatch-0.27.2}/src/chronicle/__init__.py +0 -0
  131. {interlatch-0.27.0 → interlatch-0.27.2}/src/chronicle/__main__.py +0 -0
  132. {interlatch-0.27.0 → interlatch-0.27.2}/src/chronicle/windows.py +0 -0
  133. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/__init__.py +0 -0
  134. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/__main__.py +0 -0
  135. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/__init__.py +0 -0
  136. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/digest.py +0 -0
  137. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/language.py +0 -0
  138. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/queue.py +0 -0
  139. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/__init__.py +0 -0
  140. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/api.py +0 -0
  141. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/base.py +0 -0
  142. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/claude.py +0 -0
  143. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/codex.py +0 -0
  144. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/factory.py +0 -0
  145. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/process.py +0 -0
  146. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/runners/replies.py +0 -0
  147. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/screen.py +0 -0
  148. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/session.py +0 -0
  149. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/analysis/worker.py +0 -0
  150. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/application/__init__.py +0 -0
  151. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/application/records.py +0 -0
  152. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/application/search.py +0 -0
  153. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/application/workflows.py +0 -0
  154. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/artifacts/__init__.py +0 -0
  155. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/artifacts/files.py +0 -0
  156. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/artifacts/queries.py +0 -0
  157. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/cli.py +0 -0
  158. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/__init__.py +0 -0
  159. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/agents.py +0 -0
  160. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/artifact_records.py +0 -0
  161. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/config.py +0 -0
  162. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/diagrams.py +0 -0
  163. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/i18n/__init__.py +0 -0
  164. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/i18n/ja.py +0 -0
  165. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/machine.py +0 -0
  166. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/paths.py +0 -0
  167. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/pricing.py +0 -0
  168. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/process.py +0 -0
  169. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/redact.py +0 -0
  170. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/sessions.py +0 -0
  171. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/core/util.py +0 -0
  172. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/__init__.py +0 -0
  173. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/analyses.py +0 -0
  174. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/archive.py +0 -0
  175. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/chat_import.py +0 -0
  176. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/deletion.py +0 -0
  177. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/history.py +0 -0
  178. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/memory.py +0 -0
  179. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/persistence.py +0 -0
  180. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/report.py +0 -0
  181. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/ingestion/service.py +0 -0
  182. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/__init__.py +0 -0
  183. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/__init__.py +0 -0
  184. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/commands/__init__.py +0 -0
  185. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/commands/archive.py +0 -0
  186. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/commands/sharing.py +0 -0
  187. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/main.py +0 -0
  188. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/cli/parser.py +0 -0
  189. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/exports/__init__.py +0 -0
  190. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/exports/diagrams.py +0 -0
  191. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/exports/markdown.py +0 -0
  192. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/exports/sessions.py +0 -0
  193. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/exports/vault.py +0 -0
  194. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/mcp.py +0 -0
  195. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/__init__.py +0 -0
  196. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/__init__.py +0 -0
  197. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/artifacts.py +0 -0
  198. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/common.py +0 -0
  199. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/devices.py +0 -0
  200. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/knowledge.py +0 -0
  201. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/leaderboard.py +0 -0
  202. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/party.py +0 -0
  203. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/projects.py +0 -0
  204. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/sessions.py +0 -0
  205. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/settings.py +0 -0
  206. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/sources.py +0 -0
  207. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/status.py +0 -0
  208. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/storage.py +0 -0
  209. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/api/team.py +0 -0
  210. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/app.py +0 -0
  211. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/assets.py +0 -0
  212. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/context.py +0 -0
  213. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/jobs.py +0 -0
  214. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/interfaces/web/server.py +0 -0
  215. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/__init__.py +0 -0
  216. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/arena.py +0 -0
  217. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/bag.py +0 -0
  218. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/cases.py +0 -0
  219. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/formation.py +0 -0
  220. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/friction.py +0 -0
  221. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/glossary.py +0 -0
  222. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/instructions.py +0 -0
  223. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/ladder.py +0 -0
  224. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/learning.py +0 -0
  225. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/market.json +0 -0
  226. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/party.py +0 -0
  227. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/reviews.py +0 -0
  228. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/roadmap.json +0 -0
  229. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/suggest.py +0 -0
  230. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/knowledge/synthesize.py +0 -0
  231. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/projects/__init__.py +0 -0
  232. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/projects/groups.py +0 -0
  233. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/projects/labels.py +0 -0
  234. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/projects/progress.py +0 -0
  235. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/projects/routing.py +0 -0
  236. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/projects/systems.py +0 -0
  237. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/__init__.py +0 -0
  238. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/connectors.py +0 -0
  239. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/container.py +0 -0
  240. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/hooks.py +0 -0
  241. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/install.py +0 -0
  242. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/migrate.py +0 -0
  243. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/notify.py +0 -0
  244. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/platforms/__init__.py +0 -0
  245. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/platforms/desktop.py +0 -0
  246. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/platforms/menubar.py +0 -0
  247. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/platforms/windows.py +0 -0
  248. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/statusline.py +0 -0
  249. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/tailnet.py +0 -0
  250. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/runtime/update.py +0 -0
  251. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/__init__.py +0 -0
  252. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/access.py +0 -0
  253. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/arena.py +0 -0
  254. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/client.py +0 -0
  255. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/identity.py +0 -0
  256. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/leaderboard.py +0 -0
  257. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/market.py +0 -0
  258. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/news.py +0 -0
  259. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/projects.py +0 -0
  260. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/protocol.py +0 -0
  261. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/pull.py +0 -0
  262. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/push.py +0 -0
  263. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sharing/receiver.py +0 -0
  264. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/__init__.py +0 -0
  265. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/antigravity.py +0 -0
  266. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/bob.py +0 -0
  267. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/builder.py +0 -0
  268. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/chatgpt.py +0 -0
  269. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/chats.py +0 -0
  270. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/claude/__init__.py +0 -0
  271. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/claude/export.py +0 -0
  272. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/claude/files.py +0 -0
  273. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/claude/parser.py +0 -0
  274. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/codex/__init__.py +0 -0
  275. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/codex/cloud.py +0 -0
  276. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/codex/parser.py +0 -0
  277. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/copilot.py +0 -0
  278. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/discovery.py +0 -0
  279. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/opencode.py +0 -0
  280. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/sources/prompts.py +0 -0
  281. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/__init__.py +0 -0
  282. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/fts.py +0 -0
  283. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/kv.py +0 -0
  284. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/migrations.py +0 -0
  285. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/mirror.py +0 -0
  286. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/postgres.py +0 -0
  287. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/schema.py +0 -0
  288. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/sqlite.py +0 -0
  289. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/storage/team.py +0 -0
  290. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/AGENT-ICONS.txt +0 -0
  291. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/PLEX-OFL.txt +0 -0
  292. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/adventure.js +0 -0
  293. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-antigravity.svg +0 -0
  294. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-bob.svg +0 -0
  295. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-chatgpt.svg +0 -0
  296. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-claude-ai.svg +0 -0
  297. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-claude.svg +0 -0
  298. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-codex.svg +0 -0
  299. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-copilot.svg +0 -0
  300. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/agent-opencode.svg +0 -0
  301. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-book.webp +0 -0
  302. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-camera.webp +0 -0
  303. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-chip.webp +0 -0
  304. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-clock.webp +0 -0
  305. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-cloud.webp +0 -0
  306. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-coins.webp +0 -0
  307. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-container.webp +0 -0
  308. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-dashboard.webp +0 -0
  309. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-dex.webp +0 -0
  310. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-document.webp +0 -0
  311. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-flag.webp +0 -0
  312. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-folder.webp +0 -0
  313. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-gateway.webp +0 -0
  314. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-gears.webp +0 -0
  315. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-glitch.webp +0 -0
  316. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-globe.webp +0 -0
  317. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-laptop.webp +0 -0
  318. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-log.webp +0 -0
  319. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-lumi.webp +0 -0
  320. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-magnifier.webp +0 -0
  321. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-pip.webp +0 -0
  322. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-plug.webp +0 -0
  323. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-rack.webp +0 -0
  324. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/art-tink.webp +0 -0
  325. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/battle.js +0 -0
  326. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/boot.js +0 -0
  327. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/favicon.png +0 -0
  328. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/i18n.js +0 -0
  329. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/icon-180.png +0 -0
  330. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/icon-192.png +0 -0
  331. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/icon-512.png +0 -0
  332. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/icon.png +0 -0
  333. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/index.html +0 -0
  334. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/innerlatch.css +0 -0
  335. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/innerlatch.js +0 -0
  336. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/learning.js +0 -0
  337. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/manifest.webmanifest +0 -0
  338. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/panel.css +0 -0
  339. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/panel.html +0 -0
  340. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/panel.js +0 -0
  341. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/plex-mono-400.woff2 +0 -0
  342. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/plex-mono-500.woff2 +0 -0
  343. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/plex-mono-600.woff2 +0 -0
  344. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/plex-sans-400.woff2 +0 -0
  345. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/plex-sans-500.woff2 +0 -0
  346. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/plex-sans-600.woff2 +0 -0
  347. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/plex-sans-700.woff2 +0 -0
  348. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/rough.js +0 -0
  349. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/scene.js +0 -0
  350. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/web/sprites.js +0 -0
  351. {interlatch-0.27.0 → interlatch-0.27.2}/src/interlatch/windows.py +0 -0
  352. {interlatch-0.27.0 → interlatch-0.27.2}/tests/antigravity_fixture.py +0 -0
  353. {interlatch-0.27.0 → interlatch-0.27.2}/tests/codex_fixture.py +0 -0
  354. {interlatch-0.27.0 → interlatch-0.27.2}/tests/conftest.py +0 -0
  355. {interlatch-0.27.0 → interlatch-0.27.2}/tests/copilot_fixture.py +0 -0
  356. {interlatch-0.27.0 → interlatch-0.27.2}/tests/friction_fixture.py +0 -0
  357. {interlatch-0.27.0 → interlatch-0.27.2}/tests/golden.py +0 -0
  358. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/cli.json +0 -0
  359. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/diagram.json +0 -0
  360. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/parsers/antigravity.json +0 -0
  361. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/parsers/bob.json +0 -0
  362. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/parsers/claude_code.json +0 -0
  363. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/parsers/codex.json +0 -0
  364. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/parsers/copilot.json +0 -0
  365. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/parsers/opencode.json +0 -0
  366. {interlatch-0.27.0 → interlatch-0.27.2}/tests/goldens/systems.json +0 -0
  367. {interlatch-0.27.0 → interlatch-0.27.2}/tests/opencode_fixture.py +0 -0
  368. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_adventure_web.py +0 -0
  369. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_analysis.py +0 -0
  370. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_antigravity.py +0 -0
  371. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_architecture.py +0 -0
  372. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_arena.py +0 -0
  373. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_artifacts.py +0 -0
  374. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_bag.py +0 -0
  375. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_battle_web.py +0 -0
  376. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_boot_web.py +0 -0
  377. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_careers.py +0 -0
  378. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_case_files.py +0 -0
  379. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_cases.py +0 -0
  380. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_chat_import.py +0 -0
  381. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_cli_pins.py +0 -0
  382. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_codex.py +0 -0
  383. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_codex_analysis.py +0 -0
  384. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_codex_cloud.py +0 -0
  385. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_container.py +0 -0
  386. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_copilot_bob.py +0 -0
  387. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_dedicated_hub.py +0 -0
  388. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_dedupe.py +0 -0
  389. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_desktop.py +0 -0
  390. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_diagram.py +0 -0
  391. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_ee_boundary.py +0 -0
  392. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_formation.py +0 -0
  393. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_friction.py +0 -0
  394. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_glossary.py +0 -0
  395. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_group_sharing.py +0 -0
  396. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_home_agents.py +0 -0
  397. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_hub.py +0 -0
  398. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_hub_accept.py +0 -0
  399. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_hub_news.py +0 -0
  400. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_hub_projects.py +0 -0
  401. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_hub_projects_listed.py +0 -0
  402. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_hub_purge.py +0 -0
  403. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_hub_sharing_race.py +0 -0
  404. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_i18n_web.py +0 -0
  405. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_ingest.py +0 -0
  406. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_instructions.py +0 -0
  407. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_integration.py +0 -0
  408. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_ladder.py +0 -0
  409. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_language.py +0 -0
  410. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_leaderboard.py +0 -0
  411. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_learning_glance.py +0 -0
  412. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_learning_interests.py +0 -0
  413. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_learning_web.py +0 -0
  414. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_leave_project.py +0 -0
  415. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_legacy_package.py +0 -0
  416. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_map_goldens.py +0 -0
  417. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_market.py +0 -0
  418. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_menubar.py +0 -0
  419. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_migrate.py +0 -0
  420. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_mirror.py +0 -0
  421. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_notify.py +0 -0
  422. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_opencode.py +0 -0
  423. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_parser.py +0 -0
  424. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_parser_goldens.py +0 -0
  425. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_party.py +0 -0
  426. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_party_map.py +0 -0
  427. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_people_hub.py +0 -0
  428. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_project_groups.py +0 -0
  429. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_project_progress.py +0 -0
  430. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_providers.py +0 -0
  431. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_queue.py +0 -0
  432. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_regressions.py +0 -0
  433. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_release.py +0 -0
  434. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_roadmap_catalog.py +0 -0
  435. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_route_matrix.py +0 -0
  436. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_screen.py +0 -0
  437. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_security.py +0 -0
  438. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_statusline.py +0 -0
  439. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_suggest.py +0 -0
  440. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_suggest_cli.py +0 -0
  441. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_systems.py +0 -0
  442. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_team.py +0 -0
  443. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_team_home.py +0 -0
  444. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_update.py +0 -0
  445. {interlatch-0.27.0 → interlatch-0.27.2}/tests/test_windows.py +0 -0
  446. {interlatch-0.27.0 → interlatch-0.27.2}/tests/windows_smoke.py +0 -0
  447. {interlatch-0.27.0 → interlatch-0.27.2}/uv.lock +0 -0
  448. {interlatch-0.27.0 → interlatch-0.27.2}/vscode-extension/.vscodeignore +0 -0
  449. {interlatch-0.27.0 → interlatch-0.27.2}/vscode-extension/LICENSE +0 -0
  450. {interlatch-0.27.0 → interlatch-0.27.2}/vscode-extension/README.md +0 -0
  451. {interlatch-0.27.0 → interlatch-0.27.2}/vscode-extension/extension.js +0 -0
  452. {interlatch-0.27.0 → interlatch-0.27.2}/vscode-extension/media/icon.png +0 -0
  453. {interlatch-0.27.0 → interlatch-0.27.2}/vscode-extension/media/interlatch.svg +0 -0
  454. {interlatch-0.27.0 → interlatch-0.27.2}/vscode-extension/package.json +0 -0
@@ -3,6 +3,21 @@
3
3
  <!-- Don't add lines here: a pull request puts its line in a new file in changelog.d/ (see changelog.d/README.md),
4
4
  and each release moves the files it took into a dated section below. -->
5
5
 
6
+ ## 0.27.1 (2026-10-16)
7
+
8
+ - **IBM Bob as the analyzer:** a key that is only in your shell (`BOB_API_KEY`, or a provider's `*_API_KEY`) used to
9
+ leave sessions unanalyzed in the background while `interlatch install` called it ready; install now offers to save
10
+ it, and `interlatch status` says when background analysis can't see it. Bob is told it has no tools and a call that
11
+ reaches for one is tried once more, and failed knowledge bases are named by project instead of a cut-off path
12
+ ([Analysis](docs/analysis.md)).
13
+
14
+ ## 0.27.0 (2026-10-16)
15
+
16
+ - **Formations:** the Party page builds decks, teams of one to three agents in the order they fight, up to five
17
+ of them. The Arena fields the deck you pick, as a relay: lead against lead, and whoever wins a bout stays on with
18
+ the HP it has left. A deck of one is the old champion duel. Sheets now carry the lineup (format 3), so arena players
19
+ on older versions don't meet newer ones until they update ([Party](docs/party.md#formations)).
20
+
6
21
  ## 0.26.0 (2026-10-16)
7
22
 
8
23
  - **Adventure:** a new page in Innerlatch puts every lesson that counts on a pixel world map, one region per attribute
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: interlatch
3
- Version: 0.27.0
3
+ Version: 0.27.2
4
4
  Summary: Shared memory for your coding agents: records every session (Claude Code, Codex, Copilot, Bob, Antigravity, OpenCode) locally and keeps what they learned, each lesson linked to the session it came from.
5
5
  Project-URL: Homepage, https://interlatch.com
6
6
  Project-URL: Documentation, https://interlatch.com/docs/
@@ -0,0 +1,4 @@
1
+ - **One row per browser on Team › People:** opening the hub from your own dashboard again no longer leaves the
2
+ browser's previous session behind, so the same browser stops piling up as more and more rows. Older sessions already
3
+ there fold into one line under the browser, with one **Revoke them**, and the People table stacks each person on a
4
+ narrow window instead of cutting off its buttons.
@@ -54,9 +54,11 @@ instance, have no cache split (so no cost estimate), Bob tasks have no per-call
54
54
  call is thrown away and the analysis counts as failed.
55
55
  - **IBM Bob** (`backend = "bob"`): Bob Shell's `bob run --format stream-json --max-turns 1 --disable-mcp
56
56
  --disable-subagents`, with every tool group disabled, in a throwaway workspace whose custom mode holds
57
- Interlatch's instructions and no tools; as with Codex, a reply that follows any tool call is thrown away. Headless
58
- runs need a Bob API key (the app's sign-in isn't used): `interlatch config set-key bob`, the IBM Bob tab in
59
- **Status › Analysis**, or `BOB_API_KEY`. Bob picks its own model. It keeps each analysis in its own task list;
57
+ Interlatch's instructions and no tools; as with Codex, a reply that follows any tool call is thrown away (and the
58
+ call tried once more). Headless runs need a Bob API key (the app's sign-in isn't used): `interlatch config set-key
59
+ bob` or the IBM Bob tab in **Status › Analysis**. `BOB_API_KEY` works too, but only from a terminal that has it:
60
+ background analysis and the dashboard don't see your shell's variables, so `interlatch install` offers to save it.
61
+ Bob picks its own model. It keeps each analysis in its own task list;
60
62
  Interlatch doesn't import those as sessions.
61
63
 
62
64
  - **A model provider's API** (`backend = "anthropic"`, `"bedrock"`, `"openai"`, `"azure"`, `"openrouter"`,
@@ -49,9 +49,10 @@ Codex、Copilot、Bob、Antigravity、OpenCode のセッションも、ログに
49
49
  ないため、Interlatch はイベントストリームも読みます:ツール呼び出しのあとに来た応答は捨てられ、その分析は失敗として扱われます。
50
50
  - **IBM Bob**(`backend = "bob"`):Bob Shell の `bob run --format stream-json --max-turns 1 --disable-mcp
51
51
  --disable-subagents`。ツールグループをすべて無効にし、Interlatch の指示だけを持つツールなしのカスタムモードを置いた使い捨ての
52
- ワークスペースで実行します。Codex と同じく、ツール呼び出しのあとに来た応答は捨てます。ヘッドレス実行には Bob の API キーが
53
- 必要です(アプリのサインインは使われません):`interlatch config set-key bob`、**Status › Analysis** の IBM Bob タブ、
54
- または `BOB_API_KEY`。モデルは Bob が選びます。Bob は各分析を自身のタスク一覧に残しますが、Interlatch はそれをセッションとして
52
+ ワークスペースで実行します。Codex と同じく、ツール呼び出しのあとに来た応答は捨てます(その呼び出しは一度だけやり直します)。
53
+ ヘッドレス実行には Bob の API キーが必要です(アプリのサインインは使われません):`interlatch config set-key bob` または
54
+ **Status › Analysis** の IBM Bob タブ。`BOB_API_KEY` も使えますが、それを持つターミナルからだけです。バックグラウンドの
55
+ 分析とダッシュボードはシェルの変数を見ないため、`interlatch install` が保存を提案します。モデルは Bob が選びます。Bob は各分析を自身のタスク一覧に残しますが、Interlatch はそれをセッションとして
55
56
  取り込みません。
56
57
 
57
58
  - **モデルプロバイダーの API**(`backend = "anthropic"`、`"bedrock"`、`"openai"`、`"azure"`、`"openrouter"`、
@@ -18,6 +18,19 @@ from .process import run_sleep_aware
18
18
  BOB_TOOL_GROUPS = ("artifact", "browser", "edit", "execute", "mcp", "mode", "plan", "read", "skill", "subagent",
19
19
  "subtask", "todo")
20
20
  BOB_MODE = "interlatch-analyst"
21
+ # Bob reads tool calls out of the reply's text, so a model can write one with every tool group off; saying so up front
22
+ # makes that rare, and a call that does it anyway is retried once (TOOL_TRIES).
23
+ NO_TOOLS = ("You have no tools here: don't call or write one (no list_files, read_file or the like). Everything you "
24
+ "need is in the message; answer it directly.")
25
+ TOOL_TRIES = 2
26
+
27
+
28
+ class _ToolUsed(LLMError):
29
+ """The model called a tool, so its reply was discarded; `cost_usd` is what the call cost."""
30
+
31
+ def __init__(self, msg: str, cost_usd: float):
32
+ super().__init__(msg)
33
+ self.cost_usd = cost_usd
21
34
 
22
35
 
23
36
  class BobRunner(Runner):
@@ -57,6 +70,23 @@ class BobRunner(Runner):
57
70
 
58
71
  def _call(self, prompt: str, *, system: str, model: str | None, effort: str | None, timeout: int | None,
59
72
  max_budget_usd: float | None) -> tuple[LLMResult, str]:
73
+ spent = 0.0
74
+ for attempt in range(1, TOOL_TRIES + 1):
75
+ try:
76
+ res, text = self._run(prompt, system=f"{system}\n\n{NO_TOOLS}", timeout=timeout,
77
+ max_budget_usd=max_budget_usd)
78
+ except _ToolUsed as exc:
79
+ spent += exc.cost_usd
80
+ if attempt == TOOL_TRIES:
81
+ raise
82
+ continue
83
+ res.cost_usd += spent
84
+ return res, text
85
+ raise AssertionError("unreachable")
86
+
87
+ def _run(self, prompt: str, *, system: str, timeout: int | None,
88
+ max_budget_usd: float | None) -> tuple[LLMResult, str]:
89
+ """One `bob run`."""
60
90
  if not self.bin:
61
91
  raise LLMError("bob executable not found (set analysis.bob_bin in config.toml)")
62
92
  key, _ = self._key()
@@ -103,7 +133,8 @@ class BobRunner(Runner):
103
133
  stats = (result or {}).get("stats") or {}
104
134
  if acted or stats.get("tool_calls"):
105
135
  names = ", ".join(sorted(set(tools))) or f"{stats.get('tool_calls')} call(s)"
106
- raise LLMError(f"bob used a tool ({names}); the reply was discarded")
136
+ raise _ToolUsed(f"bob used a tool ({names}); the reply was discarded",
137
+ float(stats.get("session_costs") or 0.0))
107
138
  text = "".join(parts)
108
139
  if returncode != 0 or not result or result.get("status") != "success" or not text:
109
140
  msg = "; ".join(errors) or str((result or {}).get("error") or "") or html.unescape(stderr).strip()
@@ -89,6 +89,14 @@ def key_source(cfg: Config, provider: str) -> tuple[str | None, str]:
89
89
  return None, ""
90
90
 
91
91
 
92
+ def env_only_key(cfg: Config, provider: str) -> str | None:
93
+ """The environment variable `provider`'s key comes from when it isn't stored. The background jobs and the
94
+ dashboard run from launchd (Task Scheduler on Windows) without the shell's variables, so they don't see it:
95
+ analysis then runs only from a terminal that has it."""
96
+ key, source = key_source(cfg, provider)
97
+ return source[1:] if key and source.startswith("$") else None
98
+
99
+
92
100
  def settings(cfg: Config, provider: str) -> dict:
93
101
  """[providers.<provider>] from config.toml, keeping only known keys of the right type."""
94
102
  raw = cfg.providers.get(provider) or {}
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
  import json
7
7
  import sys
8
8
 
9
- from ..common import analyzer_label, escape_markup, load_cfg, new_console, open_archive
9
+ from ..common import analyzer_label, escape_markup, failed_label, load_cfg, new_console, open_archive
10
10
 
11
11
 
12
12
  def cmd_analyze(args) -> int:
@@ -59,7 +59,7 @@ def cmd_analyze(args) -> int:
59
59
  export=export_markdown, progress=lambda m: print(f" {m}"))
60
60
  print(f"{report.summary()} · analysis cost {human_cost(report.cost_usd)}")
61
61
  for sid, err in report.failed:
62
- print(f" ! {sid[:8]}: {err[:300]}", file=sys.stderr)
62
+ print(f" ! {failed_label(sid)}: {err[:300]}", file=sys.stderr)
63
63
  return 0 if not report.failed else 1
64
64
 
65
65
 
@@ -13,6 +13,7 @@ from ..common import load_cfg, new_console, open_archive, wait_for_port
13
13
  def cmd_status(args) -> int:
14
14
  from ....analysis.queue import PAUSE_KEY, QUEUE_REASON_LABEL, count_pending
15
15
  from ....analysis.runners.factory import make_runner
16
+ from ....analysis.runners.settings import env_only_key
16
17
  from ....core.util import human_cost, local_str
17
18
  from ....runtime.install import UI_LABEL, hooks_installed, launchd_status, mcp_registered, statusline_installed
18
19
  from ....runtime.statusline import WINDOW_LABEL, plan_usage
@@ -53,6 +54,9 @@ def cmd_status(args) -> int:
53
54
  runner = make_runner(cfg)
54
55
  console.print(f" {ok(mcp_registered())} MCP server registered {ok(runner.available())} analysis by {runner.label}: "
55
56
  f"{runner.where() if runner.available() else runner.unavailable_reason()}")
57
+ if runner.available() and (env := env_only_key(cfg, runner.name)):
58
+ console.print(f" [yellow]![/] its key is only in ${env}: background analysis and the dashboard don't see it "
59
+ f"(`interlatch config set-key {runner.name}` saves it)", highlight=False)
56
60
  console.print(f" sessions: {sum(counts.values())} · " + " · ".join(f"{k}: {v}" for k, v in sorted(counts.items())))
57
61
  console.print(f" analysis queue: {pending['ready']} ready now, {pending['queued']} queued in all, {pending['held']} held · "
58
62
  f"model {runner.model_label()} · auto={'on' if cfg.analysis.auto else 'off'} · spent {human_cost(spent)}")
@@ -7,7 +7,7 @@ import argparse
7
7
  import json
8
8
  import sys
9
9
 
10
- from ..common import escape_markup, load_cfg, new_console, open_archive, push_now
10
+ from ..common import escape_markup, failed_label, load_cfg, new_console, open_archive, push_now
11
11
 
12
12
 
13
13
  def cmd_hook(args) -> int:
@@ -91,7 +91,7 @@ def cmd_work(args) -> int:
91
91
  progress=lambda m: print(f" {m}"))
92
92
  print(report.summary())
93
93
  for sid, err in report.failed:
94
- print(f" ! {sid[:8]}: {err[:200]}", file=sys.stderr)
94
+ print(f" ! {failed_label(sid)}: {err[:200]}", file=sys.stderr)
95
95
  return 0 if not report.failed else 1
96
96
 
97
97
 
@@ -8,7 +8,7 @@ import json
8
8
  import sys
9
9
 
10
10
  from ....core.config import set_config_value
11
- from ..common import analyzer_label, load_cfg, new_console, open_archive, wait_for_port
11
+ from ..common import analyzer_label, failed_label, load_cfg, new_console, open_archive, wait_for_port
12
12
 
13
13
 
14
14
  def _ask(question: str, default: bool) -> bool:
@@ -222,7 +222,7 @@ def _analyze_now(cfg, console, ids: list[str], waiting: int, later: str) -> str:
222
222
  f"{terms} glossary term{'s' * (terms != 1)}"
223
223
  + (f", Map themes for {themed} categor{'ies' if themed != 1 else 'y'}" if themed else ""), highlight=False)
224
224
  for target, err in report.failed[:3]:
225
- console.print(f" [red]![/] {target[:40]}: {err[:200]}", highlight=False)
225
+ console.print(f" [red]![/] {failed_label(target)}: {err[:200]}", highlight=False)
226
226
  if report.paused_until:
227
227
  from ....core.util import local_str
228
228
 
@@ -293,6 +293,8 @@ def cmd_install(args) -> int:
293
293
  console.print("[bold]Interlatch setup[/]\n")
294
294
  analyzer = _choose_analyzer(cfg, console, ask=ask, dry_run=args.dry_run, conn=conn, interactive=interactive,
295
295
  choice=args.backend)
296
+ if analyzer:
297
+ _keep_env_key(cfg, console, analyzer, ask=ask, interactive=interactive)
296
298
  if " -m " in exe:
297
299
  console.print(f"[yellow]`interlatch` is not on PATH[/]; hooks will run `{exe}`. "
298
300
  "`uv tool install interlatch` gives it a stable path.\n", highlight=False)
@@ -443,13 +445,37 @@ def _runner_for(cfg, name: str):
443
445
  return make_runner(replace(cfg, analysis=replace(cfg.analysis, backend=name)))
444
446
 
445
447
 
448
+ def _keep_env_key(cfg, console, runner, *, ask, interactive: bool) -> None:
449
+ """Offer to save the analyzer's API key when it is only in this shell's environment, which the background jobs
450
+ and the dashboard don't see. Never saved without asking."""
451
+ import os
452
+
453
+ from ....analysis.runners.settings import env_only_key, set_key
454
+
455
+ env = env_only_key(cfg, runner.name)
456
+ if not env:
457
+ return
458
+ if interactive and ask(f"Your {runner.label} key is in ${env}, which background analysis and the dashboard don't "
459
+ "see. Save it in ~/.interlatch, readable by you only?", True):
460
+ set_key(cfg, runner.name, os.environ[env])
461
+ console.print(f"Saved your {runner.label} key.\n", highlight=False)
462
+ return
463
+ console.print(f"[yellow]Your {runner.label} key is only in ${env}[/]: `interlatch analyze` works from this "
464
+ "terminal, but background analysis and the dashboard don't see it. "
465
+ f"`interlatch config set-key {runner.name}` saves it.\n", highlight=False)
466
+
467
+
446
468
  def _backend_state(runner) -> str:
447
469
  """Whether `runner` can analyze now, in a few words, for the list install shows."""
448
470
  import re
449
471
 
450
472
  from ....analysis.runners.factory import AGENTS
473
+ from ....analysis.runners.settings import env_only_key
451
474
 
452
475
  if runner.available():
476
+ env = env_only_key(runner.cfg, runner.name)
477
+ if env:
478
+ return f"ready in this terminal only · ${env}"
453
479
  if runner.name in AGENTS:
454
480
  return "ready · your Bob API key" if runner.name == "bob" else "ready · your own login and plan"
455
481
  return f"ready · {runner.model_label()}" + (" on this computer" if runner.local() else "")
@@ -772,10 +798,14 @@ def _config_set(cfg, key: str | None, value: str | None) -> int:
772
798
  print(f"[{section}] {name} = {literal}")
773
799
  if key == "analysis.backend":
774
800
  from ....analysis.runners.factory import make_runner
801
+ from ....analysis.runners.settings import env_only_key
775
802
 
776
803
  runner = make_runner(load_config(cfg.home))
777
804
  if not runner.available():
778
805
  print(f"note: {runner.unavailable_reason()}; analysis waits until it is set up")
806
+ elif env := env_only_key(runner.cfg, runner.name):
807
+ print(f"note: the {runner.label} key is only in ${env}, which background analysis and the dashboard don't "
808
+ f"see; `interlatch config set-key {runner.name}` saves it")
779
809
  return 0
780
810
 
781
811
 
@@ -22,6 +22,23 @@ def open_archive(cfg):
22
22
  return connect(cfg.db_path)
23
23
 
24
24
 
25
+ def failed_label(target: str) -> str:
26
+ """What a worker run failed on, as its error lines name it: a session's short id, a project's folder name (from a
27
+ knowledge base, "glossary:<path>" or "themes:<category>"), or the global playbook."""
28
+ from ...knowledge.synthesize import GLOBAL
29
+
30
+ kind, sep, rest = target.partition(":")
31
+ if sep and kind == "glossary":
32
+ return f"glossary {failed_label(rest)}"
33
+ if sep and kind == "themes":
34
+ return f"themes {rest}"
35
+ if target == GLOBAL:
36
+ return "global playbook"
37
+ if "/" in target or "\\" in target:
38
+ return target.replace("\\", "/").rstrip("/").rsplit("/", 1)[-1] or target
39
+ return target[:8]
40
+
41
+
25
42
  def new_console():
26
43
  """A rich console for a command's output."""
27
44
  from rich.console import Console
@@ -135,11 +135,12 @@ def action_team_store_save(ctx: Context, body: dict) -> dict:
135
135
  return {"ok": True, "store": team_store_info(ctx), "status": team_store_status(ctx)}
136
136
 
137
137
 
138
- def people_info(ctx: Context) -> dict:
138
+ def people_info(ctx: Context, session: str | None = None) -> dict:
139
+ """The People page; `session` is the viewer's own browser session, which the page marks as this browser."""
139
140
  from ....sharing import people
140
141
  from ....sharing.projects import hub_projects
141
142
 
142
- return {"people": people.listing(ctx.conn), "shared_token": ctx.cfg.hub_shared_token,
143
+ return {"people": people.listing(ctx.conn, session), "shared_token": ctx.cfg.hub_shared_token,
143
144
  "address": ctx.cfg.hub_address or None, "audit": people.audit_log(ctx.conn, 50),
144
145
  "roles": list(people.ROLES), "projects": hub_projects(ctx.conn)}
145
146
 
@@ -212,8 +213,11 @@ def action_people(ctx: Context, verb: str, body: dict, by: dict | None, address:
212
213
  people.set_projects(ctx.conn, pid, _projects_body(ctx, body), by=by)
213
214
  elif verb == "remove":
214
215
  people.remove(ctx.conn, pid, by=by)
215
- elif verb == "revoke":
216
- if not people.revoke(ctx.conn, pid, str(body.get("token") or ""), by=by):
216
+ elif verb == "revoke": # one token, or several (a browser's older sessions, at once)
217
+ tokens = body.get("tokens") if isinstance(body.get("tokens"), list) else [body.get("token")]
218
+ if not tokens or len(tokens) > 500:
219
+ return {"error": tr("no such computer or browser session")}, 400
220
+ if not sum(people.revoke(ctx.conn, pid, str(x or ""), by=by) for x in tokens):
217
221
  return {"error": tr("no such computer or browser session")}, 400
218
222
  else:
219
223
  return {"error": tr("not found")}, 404
@@ -129,14 +129,17 @@ def make_handler(app: App, port: int):
129
129
  and (self.headers.get("X-Forwarded-Proto") or "").strip().lower() == "https")
130
130
 
131
131
  def _session(self) -> str:
132
+ return next(iter(self._sessions()), "")
133
+
134
+ def _sessions(self) -> tuple[str, ...]:
135
+ """The session cookies this browser sent: the current one first, then one from before the rename."""
132
136
  from http.cookies import CookieError, SimpleCookie
133
137
 
134
138
  try:
135
139
  jar = SimpleCookie(self.headers.get("Cookie") or "")
136
140
  except CookieError:
137
- return ""
138
- morsel = jar.get(SESSION_COOKIE) or jar.get(LEGACY_SESSION_COOKIE)
139
- return morsel.value if morsel else ""
141
+ return ()
142
+ return tuple(m.value for m in (jar.get(SESSION_COOKIE), jar.get(LEGACY_SESSION_COOKIE)) if m and m.value)
140
143
 
141
144
  def _session_cookie(self, value: str, max_age: int) -> str:
142
145
  secure = "; Secure" if self._proxied_https() else ""
@@ -205,7 +208,8 @@ def make_handler(app: App, port: int):
205
208
  try:
206
209
  if wait:
207
210
  raise people.PeopleError(TOO_MANY_CODES, seconds=wait) # the code isn't looked at
208
- _person, session = people.open_browser(app.conn, code, label=self.headers.get("User-Agent"))
211
+ _person, session = people.open_browser(app.conn, code, label=self.headers.get("User-Agent"),
212
+ replaces=self._sessions()) # this browser's old session ends
209
213
  except people.PeopleError as exc:
210
214
  if isinstance(exc, people.UnknownCode):
211
215
  app.code_attempts.wrong(who)
@@ -179,7 +179,7 @@ def _unseen_suggestions(h, ctx, m, q):
179
179
 
180
180
  GET: list[Route] = [
181
181
  Route("/api/me", ANYONE, lambda h, ctx, m, q: h._json(h._me())),
182
- Route("/api/people", ADMIN, lambda h, ctx, m, q: h._json(api.hub_admin.people_info(ctx))),
182
+ Route("/api/people", ADMIN, lambda h, ctx, m, q: h._json(api.hub_admin.people_info(ctx, h._session()))),
183
183
  Route("/api/overview", ANYONE, lambda h, ctx, m, q: h._json(api.status.overview(ctx, q, limited=h.limited))),
184
184
  # a person's own computer only
185
185
  Route("/api/leaderboard", LEADERBOARD, lambda h, ctx, m, q: h._json(api.leaderboard.leaderboard(ctx, q))),
@@ -265,9 +265,11 @@ def _admins(conn: sqlite3.Connection) -> list[int]:
265
265
  return [r[0] for r in conn.execute("SELECT id FROM people WHERE role = 'admin' AND removed_at IS NULL")]
266
266
 
267
267
 
268
- def listing(conn: sqlite3.Connection) -> list[dict]:
269
- """Everyone on the hub, with their computers, open browser sessions and unused invites (no secrets)."""
268
+ def listing(conn: sqlite3.Connection, session: str | None = None) -> list[dict]:
269
+ """Everyone on the hub, with their computers, open browser sessions and unused invites (no secrets). The browser
270
+ session `session` (the viewer's own cookie) is marked `this`."""
270
271
  now = _iso(_now())
272
+ mine = _hash(session) if session else None
271
273
  out = []
272
274
  for p in conn.execute("SELECT * FROM people WHERE removed_at IS NULL ORDER BY role = 'admin' DESC, name"):
273
275
  p = dict(p)
@@ -278,7 +280,8 @@ def listing(conn: sqlite3.Connection) -> list[dict]:
278
280
  "WHERE t.person_id = ? AND t.revoked_at IS NULL ORDER BY t.created_at", (p["id"],))]
279
281
  p["computers"] = [{"id": t["token_hash"][:16], "machine_id": t["machine_id"], "name": t["machine_name"] or t["label"],
280
282
  "since": t["created_at"], "last_used": t["last_used"]} for t in toks if t["kind"] == "computer"]
281
- p["browsers"] = [{"id": t["token_hash"][:16], "label": t["label"], "since": t["created_at"], "last_used": t["last_used"]}
283
+ p["browsers"] = [{"id": t["token_hash"][:16], "label": t["label"], "since": t["created_at"], "last_used": t["last_used"],
284
+ **({"this": True} if t["token_hash"] == mine else {})}
282
285
  for t in toks if t["kind"] == "browser" and not (t["expires_at"] and t["expires_at"] < now)]
283
286
  p["invites"] = [{"expires_at": r[0], "created_at": r[1]} for r in conn.execute(
284
287
  "SELECT expires_at, created_at FROM people_codes WHERE person_id = ? AND kind = 'invite' AND used_at IS NULL "
@@ -416,11 +419,17 @@ def join_computer(conn: sqlite3.Connection, code: str, machine_id: str, machine_
416
419
  return p, token
417
420
 
418
421
 
419
- def open_browser(conn: sqlite3.Connection, code: str, label: str | None = None) -> tuple[dict, str]:
420
- """Redeem an invite or a sign-in code in a browser: (person, session secret for the cookie)."""
422
+ def open_browser(conn: sqlite3.Connection, code: str, label: str | None = None,
423
+ replaces: tuple[str, ...] = ()) -> tuple[dict, str]:
424
+ """Redeem an invite or a sign-in code in a browser: (person, session secret for the cookie). `replaces` are the
425
+ sessions that browser's cookies held: the new cookie takes their place, so they end here rather than linger,
426
+ unused, as more rows on the People page until they expire. Only once the code was good: a wrong one ends nothing."""
421
427
  p = _take_code(conn, code, ("invite", "signin"))
422
428
  session = secrets.token_urlsafe(32)
423
429
  now = _now()
430
+ for old in {s for s in replaces if s}:
431
+ conn.execute("UPDATE people_tokens SET revoked_at = ? WHERE token_hash = ? AND kind = 'browser' AND revoked_at IS NULL",
432
+ (_iso(now), _hash(old)))
424
433
  conn.execute("INSERT INTO people_tokens(token_hash, person_id, kind, label, created_at, expires_at, last_used) "
425
434
  "VALUES (?, ?, 'browser', ?, ?, ?, ?)",
426
435
  (_hash(session), p["id"], (label or "")[:200] or None, _iso(now), _iso(now + timedelta(days=BROWSER_DAYS)), _iso(now)))
@@ -2064,8 +2064,21 @@ a.ar-tile-media.empty:hover { background: var(--accent-wash); }
2064
2064
  .pp-table select { max-width: 150px; }
2065
2065
  .pp-tok { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 1px 0; }
2066
2066
  .pp-tok .icon { width: 13px; height: 13px; color: var(--muted); flex: none; }
2067
- .pp-actions { white-space: nowrap; text-align: right; }
2068
- .pp-actions .btn + .btn { margin-left: 6px; }
2067
+ .pp-actions > div { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; } /* a narrow table stacks them */
2068
+ .pp-actions .btn { white-space: nowrap; }
2069
+ .pp-older { padding-left: 19px; font-size: 12.5px; }
2070
+ /* a narrow card: each person as a block (name and role, then projects, computers and browsers, actions), no sideways scroll */
2071
+ .pp-wrap { container-type: inline-size; }
2072
+ @container (max-width: 720px) {
2073
+ .pp-table thead { display: none; }
2074
+ .pp-table, .pp-table tbody, .pp-table td { display: block; }
2075
+ .pp-table tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 12px; padding: 10px 0; border-bottom: 1px solid var(--grid); }
2076
+ .pp-table tr[hidden] { display: none; }
2077
+ .pp-table tr.pp-edit { display: block; padding-top: 0; }
2078
+ table.data.pp-table td { border: 0; padding: 0; min-width: 0; }
2079
+ .pp-table td:nth-child(n+3) { grid-column: 1 / -1; }
2080
+ .pp-actions > div { justify-content: flex-start; }
2081
+ }
2069
2082
  .link-btn.danger { color: var(--critical-ink); }
2070
2083
  .people-form { margin-bottom: 6px; }
2071
2084
  .pp-scope { border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px 10px; margin: 0 0 12px; min-width: 0; }
@@ -6352,8 +6352,28 @@ function peopleCard(dv) {
6352
6352
  } }, t("Revoke"));
6353
6353
  return h("div", { class: "pp-tok" }, icon(kind === "computer" ? "devices" : "page"),
6354
6354
  h("span", { title: kind === "browser" ? x.label || "" : x.machine_id || "" }, label),
6355
+ x.this ? h("span", { class: "muted" }, t("this browser")) : null,
6355
6356
  h("span", { class: "muted" }, x.last_used ? t("used {ago}", { ago: ago(x.last_used) }) : t("since {ago}", { ago: ago(x.since) })), revoke);
6356
6357
  };
6358
+ // a person's sessions in the same browser: the one in use (or used last) on its own row, the older ones folded into one
6359
+ // row with one Revoke (they pile up from sign-ins before a browser's new session ended its old one)
6360
+ const browserRows = (p, browsers) => {
6361
+ const groups = new Map();
6362
+ for (const b of browsers) groups.set(uaLabel(b.label), [...(groups.get(uaLabel(b.label)) || []), b]);
6363
+ const used = (x) => x.last_used || x.since || "";
6364
+ return [...groups.entries()].flatMap(([label, list]) => {
6365
+ const [kept, ...older] = [...list].sort((a, b) => Number(!!b.this) - Number(!!a.this) || (used(a) < used(b) ? 1 : used(a) > used(b) ? -1 : 0));
6366
+ if (!older.length) return [tokenRow(p, kept, "browser")];
6367
+ const revoke = h("button", { class: "link-btn danger", type: "button", onclick: async () => {
6368
+ if (!confirm(tn(older.length, "End {n} older session of {name} in {browser}? Only the one used last stays; any other browser still using one has to sign in again.",
6369
+ "End {n} older sessions of {name} in {browser}? Only the one used last stays; any other browser still using one has to sign in again.", { name: p.name, browser: label }))) return;
6370
+ revoke.disabled = true;
6371
+ if (await send("/api/people/revoke", { id: p.id, tokens: older.map((x) => x.id) })) { toast(t("Revoked.")); load(); } else revoke.disabled = false;
6372
+ } }, t("Revoke them"));
6373
+ return [tokenRow(p, kept, "browser"), h("div", { class: "pp-tok pp-older" },
6374
+ h("span", { class: "muted" }, tn(older.length, "+ {n} older session, last used {ago}", "+ {n} older sessions, last used {ago}", { ago: ago(used(older[0])) })), revoke)];
6375
+ });
6376
+ };
6357
6377
  const personRow = (p, roles) => {
6358
6378
  const me = ME?.viewer?.id != null && ME.viewer.id === p.id;
6359
6379
  const sel = h("select", { "aria-label": t("Role of {name}", { name: p.name }), onchange: async () => {
@@ -6408,9 +6428,9 @@ function peopleCard(dv) {
6408
6428
  h("td", null, sel),
6409
6429
  scope,
6410
6430
  h("td", null, computers.length || browsers.length
6411
- ? [...computers.map((c) => tokenRow(p, c, "computer")), ...browsers.map((b) => tokenRow(p, b, "browser"))]
6431
+ ? [...computers.map((c) => tokenRow(p, c, "computer")), ...browserRows(p, browsers)]
6412
6432
  : h("span", { class: "muted" }, lastInvite ? t("Not joined yet") : t("No computer or browser yet: make a new invite"))),
6413
- h("td", { class: "pp-actions" }, reinvite, remove)), editRow];
6433
+ h("td", { class: "pp-actions" }, h("div", null, reinvite, remove))), editRow];
6414
6434
  };
6415
6435
 
6416
6436
  const inviteForm = (roles, first) => {
@@ -6478,7 +6498,7 @@ function peopleCard(dv) {
6478
6498
  box.replaceChildren(); append(box, [ // append() flattens the arrays and skips nulls
6479
6499
  cardHead(t("People"), { iconName: "preference", hint: people.length ? tn(people.length, "{n} person", "{n} people") : null }),
6480
6500
  people.length ? [
6481
- h("div", { class: "table-wrap" }, h("table", { class: "data pp-table" },
6501
+ h("div", { class: "table-wrap pp-wrap" }, h("table", { class: "data pp-table" },
6482
6502
  h("thead", null, h("tr", null, ...[t("Person"), t("Role"), t("Projects"), t("Computers and browsers"), ""].map((x) => h("th", null, x)))),
6483
6503
  h("tbody", null, people.map((p) => personRow(p, roles))))),
6484
6504
  h("p", { class: "muted" }, t("Admins invite people and change this hub's settings; members' computers send here and get their teammates' lessons back; read-only people only open this dashboard. Whoever is at this computer is always an admin."))]
@@ -1645,6 +1645,12 @@ window.INTERLATCH_JA = {
1645
1645
  "Revoked.": "取り消しました。",
1646
1646
  "Revoke": "取り消す",
1647
1647
  "used {ago}": "最終使用 {ago}",
1648
+ "this browser": "このブラウザー",
1649
+ "Revoke them": "まとめて取り消す",
1650
+ "+ {n} older session, last used {ago}": "ほかに古いセッション {n} 件(最終使用 {ago})",
1651
+ "+ {n} older sessions, last used {ago}": "ほかに古いセッション {n} 件(最終使用 {ago})",
1652
+ "End {n} older session of {name} in {browser}? Only the one used last stays; any other browser still using one has to sign in again.": "{browser} での {name} さんの古いセッション {n} 件を終了しますか? 最後に使ったものだけが残ります。まだそれを使っているほかのブラウザーでは再びサインインが必要になります。",
1653
+ "End {n} older sessions of {name} in {browser}? Only the one used last stays; any other browser still using one has to sign in again.": "{browser} での {name} さんの古いセッション {n} 件を終了しますか? 最後に使ったものだけが残ります。まだそれを使っているほかのブラウザーでは再びサインインが必要になります。",
1648
1654
  "since {ago}": "{ago}から",
1649
1655
  "Role of {name}": "{name} さんのロール",
1650
1656
  "{name} is now: {role}.": "{name} さんのロールを{role}にしました。",
@@ -12,7 +12,7 @@ import pytest
12
12
  from interlatch.core.config import load_config, set_config_value
13
13
  from interlatch.analysis.runners.base import LLMError, UsageLimitError
14
14
  from interlatch.analysis.runners.factory import make_runner
15
- from interlatch.analysis.runners.bob import BOB_TOOL_GROUPS, BobRunner
15
+ from interlatch.analysis.runners.bob import BOB_TOOL_GROUPS, NO_TOOLS, BobRunner
16
16
  from interlatch.analysis.runners.settings import key_source, set_key
17
17
  from interlatch.application.workflows import run_worker
18
18
 
@@ -28,8 +28,11 @@ modes = json.load(open(os.path.join(ws, ".bob", "custom_modes.yaml")))["customMo
28
28
  mode = next(m for m in modes if m["slug"] == args[args.index("--mode") + 1])
29
29
  with open(os.environ["FAKE_BOB_LOG"], "a") as fh:
30
30
  fh.write(json.dumps({"args": args, "prompt_chars": len(prompt), "groups": mode["groups"], "cwd": os.getcwd(),
31
- "key": os.environ.get("BOB_API_KEY"), "internal": os.environ.get("INTERLATCH_INTERNAL")}) + "\n")
31
+ "key": os.environ.get("BOB_API_KEY"), "internal": os.environ.get("INTERLATCH_INTERNAL"),
32
+ "role": mode["roleDefinition"]}) + "\n")
32
33
  how = os.environ.get("FAKE_BOB_MODE", "ok")
34
+ if how == "tool-once": # only the first call reaches for a tool
35
+ how = "tool" if sum(1 for _ in open(os.environ["FAKE_BOB_LOG"])) == 1 else "ok"
33
36
  if how == "license":
34
37
  print("Error: A license agreement is required. Please accept the license terms before proceeding.", file=sys.stderr)
35
38
  sys.exit(1)
@@ -83,6 +86,7 @@ def test_bob_runs_headless_with_no_tools(bob_env):
83
86
  assert args[args.index("--disable-tool-groups") + 1].split(",") == list(BOB_TOOL_GROUPS)
84
87
  assert args[args.index("--max-cost") + 1] == "3.00"
85
88
  assert call["groups"] == [] and call["key"] == "bob-test-key" and call["internal"] == "1"
89
+ assert call["role"].startswith("Summarize the session.") and call["role"].endswith(NO_TOOLS)
86
90
  workspace = args[args.index("--workspace") + 1]
87
91
  assert cfg.is_internal_path(workspace) and call["cwd"] == workspace
88
92
  assert not (cfg.home / "workdir" / "bob").exists() or not any((cfg.home / "workdir" / "bob").iterdir()) # removed
@@ -92,6 +96,16 @@ def test_bob_reply_after_a_tool_call_is_discarded(bob_env, monkeypatch):
92
96
  monkeypatch.setenv("FAKE_BOB_MODE", "tool")
93
97
  with pytest.raises(LLMError, match=r"used a tool \(execute_command\); the reply was discarded"):
94
98
  make_runner(bob_env["cfg"]).run("x", {"type": "object"}, system="Summarize the session.")
99
+ assert len(bob_calls(bob_env)) == 2 # tried again once
100
+
101
+
102
+ def test_bob_retries_once_after_a_tool_call(bob_env, monkeypatch):
103
+ """Bob reads tool calls out of the reply's text, so the model can write one with every group off: one more try."""
104
+ monkeypatch.setenv("FAKE_BOB_MODE", "tool-once")
105
+ res = make_runner(bob_env["cfg"]).run("<transcript>hello</transcript>", {"type": "object"},
106
+ system="Summarize the session.")
107
+ assert res.data["title"] == "Fixed login token expiry bug"
108
+ assert len(bob_calls(bob_env)) == 2 and res.cost_usd == pytest.approx(0.062) # the discarded call is paid for too
95
109
 
96
110
 
97
111
  def test_bob_license_or_key_problem_pauses(bob_env, monkeypatch):
@@ -176,3 +190,16 @@ def test_dashboard_sets_agent_models(env):
176
190
  assert load_config(env["cfg"].home).analysis.codex_model == ""
177
191
  assert "error" in api.settings.action_agent(app.context(), {"agent": "bob", "settings": {"model": "x"}}) # Bob picks its own model
178
192
  assert "error" in api.settings.action_agent(app.context(), {"agent": "claude", "settings": {"model": "sonnet\nbackend = 'x'"}})
193
+
194
+
195
+ def test_failed_lines_name_projects_not_cut_paths():
196
+ """A worker run's error lines: `! /Users/a: …` named a knowledge base that failed only by its path's first 8
197
+ characters."""
198
+ from interlatch.interfaces.cli.common import failed_label
199
+
200
+ assert failed_label("0eb2ccdb-58fc-420f-b7ac-cbdbabfe1f9a") == "0eb2ccdb"
201
+ assert failed_label("/Users/a/Projects/agents-chronicle") == "agents-chronicle"
202
+ assert failed_label("glossary:/Users/a/Projects/agents-chronicle/") == "glossary agents-chronicle"
203
+ assert failed_label(r"C:\Users\a\Projects\app") == "app"
204
+ assert failed_label("__global__") == "global playbook" and failed_label("themes:Tooling and CI") == "themes Tooling and CI"
205
+ assert failed_label("2026-W41") == "2026-W41"
@@ -74,6 +74,20 @@ def test_browser_sessions_signin_and_removal(conn):
74
74
  assert people.add(conn, "Vic", "vic@example.com", "member")["role"] == "member" # can be added again
75
75
 
76
76
 
77
+ def test_a_browser_signing_in_again_ends_its_old_session(conn):
78
+ vic = people.add(conn, "Vic", "vic@example.com", "readonly")
79
+ _, first = people.open_browser(conn, people.signin_code(conn, vic), "Edge")
80
+ _, other = people.open_browser(conn, people.signin_code(conn, vic), "Safari") # another browser keeps its own
81
+ with pytest.raises(people.PeopleError):
82
+ people.open_browser(conn, "AAAA-BBBB-CCCC", "Edge", replaces=(first,)) # a wrong code ends nothing
83
+ assert people.browser_person(conn, first)
84
+ _, second = people.open_browser(conn, people.signin_code(conn, vic), "Edge", replaces=(first, "", "not-a-session"))
85
+ assert people.browser_person(conn, first) is None and people.browser_person(conn, second) and people.browser_person(conn, other)
86
+ listed = people.listing(conn, second)[0]["browsers"]
87
+ assert [b["label"] for b in listed] == ["Safari", "Edge"] and [b.get("this") for b in listed] == [None, True]
88
+ assert not any(b.get("this") for b in people.listing(conn)[0]["browsers"])
89
+
90
+
77
91
  def test_wrong_codes_make_an_address_wait():
78
92
  t = [1000.0]
79
93
  a = people.CodeAttempts(clock=lambda: t[0])