sandesh-relay 0.2.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 (144) hide show
  1. sandesh_relay-0.2.2/.codacy.yaml +9 -0
  2. sandesh_relay-0.2.2/.github/workflows/publish-npm.yml +124 -0
  3. sandesh_relay-0.2.2/.github/workflows/publish-pypi.yml +124 -0
  4. sandesh_relay-0.2.2/.gitignore +16 -0
  5. sandesh_relay-0.2.2/CLAUDE.md +319 -0
  6. sandesh_relay-0.2.2/LICENSE +675 -0
  7. sandesh_relay-0.2.2/PKG-INFO +292 -0
  8. sandesh_relay-0.2.2/README.md +266 -0
  9. sandesh_relay-0.2.2/RELEASING.md +254 -0
  10. sandesh_relay-0.2.2/bin/sandesh +9 -0
  11. sandesh_relay-0.2.2/docs/changes/CR-SAN-001-mcp-server.md +114 -0
  12. sandesh_relay-0.2.2/docs/changes/CR-SAN-002-read-tools.md +82 -0
  13. sandesh_relay-0.2.2/docs/changes/CR-SAN-003-mutating-tools.md +102 -0
  14. sandesh_relay-0.2.2/docs/changes/CR-SAN-004-e2e-smoke.md +111 -0
  15. sandesh_relay-0.2.2/docs/changes/CR-SAN-005-retire-status.md +98 -0
  16. sandesh_relay-0.2.2/docs/changes/CR-SAN-006-docstrings-instructions.md +156 -0
  17. sandesh_relay-0.2.2/docs/changes/CR-SAN-008-packaging.md +236 -0
  18. sandesh_relay-0.2.2/docs/changes/CR-SAN-009-aur-pkgbuild.md +143 -0
  19. sandesh_relay-0.2.2/docs/changes/CR-SAN-010-pypi-publish.md +181 -0
  20. sandesh_relay-0.2.2/docs/changes/CR-SAN-011-mcp-registry.md +137 -0
  21. sandesh_relay-0.2.2/docs/changes/CR-SAN-013-pi-verbs-extension.md +191 -0
  22. sandesh_relay-0.2.2/docs/changes/CR-SAN-014-pi-native-wake.md +148 -0
  23. sandesh_relay-0.2.2/docs/changes/CR-SAN-015-pi-package-listing.md +138 -0
  24. sandesh_relay-0.2.2/docs/changes/CR-SAN-016-pi-error-handling-promptsnippet.md +133 -0
  25. sandesh_relay-0.2.2/docs/changes/CR-SAN-017-migration-engine.md +301 -0
  26. sandesh_relay-0.2.2/docs/changes/CR-SAN-018-migration-installer-integration.md +177 -0
  27. sandesh_relay-0.2.2/docs/changes/CR-SAN-019-pi-tombstone-and-smoke.md +157 -0
  28. sandesh_relay-0.2.2/docs/changes/CR-SAN-020-pypi-packaging-hardening.md +116 -0
  29. sandesh_relay-0.2.2/docs/changes/CR-SAN-021-npm-pi-packaging-release-integrity.md +196 -0
  30. sandesh_relay-0.2.2/docs/changes/CR-SAN-022-global-db-tracker-consolidation.md +137 -0
  31. sandesh_relay-0.2.2/docs/changes/CR-SAN-023-cross-project-messaging.md +139 -0
  32. sandesh_relay-0.2.2/docs/changes/CR-SAN-024-project-lifecycle-verbs.md +131 -0
  33. sandesh_relay-0.2.2/docs/changes/CR-SAN-025-mcp-surface-update.md +77 -0
  34. sandesh_relay-0.2.2/docs/changes/CR-SAN-026-inbox-filters.md +75 -0
  35. sandesh_relay-0.2.2/docs/changes/CR-SAN-027-fts-search-engine.md +105 -0
  36. sandesh_relay-0.2.2/docs/changes/CR-SAN-028-mcp-search-surface.md +69 -0
  37. sandesh_relay-0.2.2/docs/changes/CR-SAN-029-projects-listing-tombstoned.md +51 -0
  38. sandesh_relay-0.2.2/docs/changes/CR-SAN-030-pre-release-cleanup-sweep.md +87 -0
  39. sandesh_relay-0.2.2/docs/changes/CR-SAN-031-pi-wake-followup.md +53 -0
  40. sandesh_relay-0.2.2/docs/changes/CR-SAN-032-pi-surface-catchup.md +99 -0
  41. sandesh_relay-0.2.2/docs/changes/CR-SAN-034-release-pipeline-branch-gated.md +160 -0
  42. sandesh_relay-0.2.2/docs/changes/README.md +73 -0
  43. sandesh_relay-0.2.2/docs/research/DN-pi-wake.md +159 -0
  44. sandesh_relay-0.2.2/docs/research/DN-windows-notifier.md +63 -0
  45. sandesh_relay-0.2.2/docs/research/PRD-db-migration.md +154 -0
  46. sandesh_relay-0.2.2/docs/research/PRD-distribution.md +143 -0
  47. sandesh_relay-0.2.2/docs/research/PRD-global-store.md +242 -0
  48. sandesh_relay-0.2.2/docs/research/PRD-inbox-search.md +126 -0
  49. sandesh_relay-0.2.2/docs/research/PRD-mcp-server.md +293 -0
  50. sandesh_relay-0.2.2/docs/research/PRD-pi-extension.md +168 -0
  51. sandesh_relay-0.2.2/docs/research/PRD-project-lifecycle.md +153 -0
  52. sandesh_relay-0.2.2/install.sh +128 -0
  53. sandesh_relay-0.2.2/integrations/pi/.gitignore +4 -0
  54. sandesh_relay-0.2.2/integrations/pi/LICENSE +675 -0
  55. sandesh_relay-0.2.2/integrations/pi/README.md +97 -0
  56. sandesh_relay-0.2.2/integrations/pi/bun.lock +316 -0
  57. sandesh_relay-0.2.2/integrations/pi/package.json +40 -0
  58. sandesh_relay-0.2.2/integrations/pi/src/execute.test.ts +873 -0
  59. sandesh_relay-0.2.2/integrations/pi/src/index.test.ts +261 -0
  60. sandesh_relay-0.2.2/integrations/pi/src/index.ts +803 -0
  61. sandesh_relay-0.2.2/integrations/pi/src/package.test.ts +263 -0
  62. sandesh_relay-0.2.2/integrations/pi/src/prereq.test.ts +410 -0
  63. sandesh_relay-0.2.2/integrations/pi/src/smoke.test.ts +204 -0
  64. sandesh_relay-0.2.2/integrations/pi/src/surface_catchup.test.ts +696 -0
  65. sandesh_relay-0.2.2/integrations/pi/src/unregister.test.ts +366 -0
  66. sandesh_relay-0.2.2/integrations/pi/src/version_gate.test.ts +868 -0
  67. sandesh_relay-0.2.2/integrations/pi/src/version_sync.test.ts +65 -0
  68. sandesh_relay-0.2.2/integrations/pi/src/wake.test.ts +873 -0
  69. sandesh_relay-0.2.2/integrations/pi/src/wake_lifecycle.test.ts +715 -0
  70. sandesh_relay-0.2.2/integrations/pi/src/workflow.test.ts +398 -0
  71. sandesh_relay-0.2.2/integrations/pi/tsconfig.json +15 -0
  72. sandesh_relay-0.2.2/packaging/aur/.SRCINFO +17 -0
  73. sandesh_relay-0.2.2/packaging/aur/PKGBUILD +27 -0
  74. sandesh_relay-0.2.2/pyproject.toml +46 -0
  75. sandesh_relay-0.2.2/sandesh/__init__.py +9 -0
  76. sandesh_relay-0.2.2/sandesh/cli.py +621 -0
  77. sandesh_relay-0.2.2/sandesh/data/usage-scenarios.md +347 -0
  78. sandesh_relay-0.2.2/sandesh/mcp_server.py +726 -0
  79. sandesh_relay-0.2.2/sandesh/migrate.py +463 -0
  80. sandesh_relay-0.2.2/sandesh/migrations/.gitkeep +0 -0
  81. sandesh_relay-0.2.2/sandesh/migrations/0001-baseline.sql +46 -0
  82. sandesh_relay-0.2.2/sandesh/migrations/0002-drop-message-status.rollback.sql +6 -0
  83. sandesh_relay-0.2.2/sandesh/migrations/0002-drop-message-status.sql +25 -0
  84. sandesh_relay-0.2.2/sandesh/migrations/0003-project-tracker.rollback.sql +19 -0
  85. sandesh_relay-0.2.2/sandesh/migrations/0003-project-tracker.sql +45 -0
  86. sandesh_relay-0.2.2/sandesh/migrations/0004-xproj-grant.rollback.sql +18 -0
  87. sandesh_relay-0.2.2/sandesh/migrations/0004-xproj-grant.sql +41 -0
  88. sandesh_relay-0.2.2/sandesh/migrations/0005-message-fts.rollback.sql +5 -0
  89. sandesh_relay-0.2.2/sandesh/migrations/0005-message-fts.sql +20 -0
  90. sandesh_relay-0.2.2/sandesh/notify.py +103 -0
  91. sandesh_relay-0.2.2/sandesh/sandesh_db.py +1193 -0
  92. sandesh_relay-0.2.2/sandesh/schema/.gitkeep +0 -0
  93. sandesh_relay-0.2.2/sandesh/schema/current-schema.json +238 -0
  94. sandesh_relay-0.2.2/sandesh/schema/schema.meta.json +45 -0
  95. sandesh_relay-0.2.2/scripts/release.sh +244 -0
  96. sandesh_relay-0.2.2/server.json +34 -0
  97. sandesh_relay-0.2.2/tests/_migrate_helpers.py +29 -0
  98. sandesh_relay-0.2.2/tests/test_admin_grant.py +1476 -0
  99. sandesh_relay-0.2.2/tests/test_ci_migration_gate.py +536 -0
  100. sandesh_relay-0.2.2/tests/test_cleanup_sweep.py +852 -0
  101. sandesh_relay-0.2.2/tests/test_consolidate_skip.py +656 -0
  102. sandesh_relay-0.2.2/tests/test_consolidation.py +1218 -0
  103. sandesh_relay-0.2.2/tests/test_fts_cli.py +986 -0
  104. sandesh_relay-0.2.2/tests/test_fts_index.py +1123 -0
  105. sandesh_relay-0.2.2/tests/test_fts_search.py +1066 -0
  106. sandesh_relay-0.2.2/tests/test_global_store.py +954 -0
  107. sandesh_relay-0.2.2/tests/test_inbox_filters.py +932 -0
  108. sandesh_relay-0.2.2/tests/test_inbox_filters_cli.py +503 -0
  109. sandesh_relay-0.2.2/tests/test_install.py +1226 -0
  110. sandesh_relay-0.2.2/tests/test_lifecycle_archive.py +1214 -0
  111. sandesh_relay-0.2.2/tests/test_lifecycle_cli.py +1637 -0
  112. sandesh_relay-0.2.2/tests/test_lifecycle_e2e.py +444 -0
  113. sandesh_relay-0.2.2/tests/test_lifecycle_tombstone.py +1371 -0
  114. sandesh_relay-0.2.2/tests/test_mcp_e2e.py +1128 -0
  115. sandesh_relay-0.2.2/tests/test_mcp_lifecycle_tools.py +591 -0
  116. sandesh_relay-0.2.2/tests/test_mcp_missing_extra.py +254 -0
  117. sandesh_relay-0.2.2/tests/test_mcp_mutating_tools.py +725 -0
  118. sandesh_relay-0.2.2/tests/test_mcp_project_derivation.py +720 -0
  119. sandesh_relay-0.2.2/tests/test_mcp_read_tools.py +319 -0
  120. sandesh_relay-0.2.2/tests/test_mcp_search_surface.py +1064 -0
  121. sandesh_relay-0.2.2/tests/test_mcp_server.py +107 -0
  122. sandesh_relay-0.2.2/tests/test_mcp_surface.py +467 -0
  123. sandesh_relay-0.2.2/tests/test_migrate.py +5677 -0
  124. sandesh_relay-0.2.2/tests/test_migrate_global.py +730 -0
  125. sandesh_relay-0.2.2/tests/test_migration_0003.py +889 -0
  126. sandesh_relay-0.2.2/tests/test_migration_0004.py +1126 -0
  127. sandesh_relay-0.2.2/tests/test_migration_docs_boundary.py +489 -0
  128. sandesh_relay-0.2.2/tests/test_package.py +85 -0
  129. sandesh_relay-0.2.2/tests/test_pkgbuild.py +410 -0
  130. sandesh_relay-0.2.2/tests/test_project_scoping.py +779 -0
  131. sandesh_relay-0.2.2/tests/test_projects_listing_cli.py +479 -0
  132. sandesh_relay-0.2.2/tests/test_publish_npm_workflow.py +298 -0
  133. sandesh_relay-0.2.2/tests/test_publish_pypi_workflow.py +458 -0
  134. sandesh_relay-0.2.2/tests/test_publish_workflow.py +433 -0
  135. sandesh_relay-0.2.2/tests/test_pyproject.py +441 -0
  136. sandesh_relay-0.2.2/tests/test_release_script.py +929 -0
  137. sandesh_relay-0.2.2/tests/test_releasing_doc.py +237 -0
  138. sandesh_relay-0.2.2/tests/test_sandesh.py +225 -0
  139. sandesh_relay-0.2.2/tests/test_server_json.py +439 -0
  140. sandesh_relay-0.2.2/tests/test_tombstone_read_rules.py +728 -0
  141. sandesh_relay-0.2.2/tests/test_usage_resource_packaging.py +194 -0
  142. sandesh_relay-0.2.2/tests/test_version_scheme.py +326 -0
  143. sandesh_relay-0.2.2/tests/test_xproj_send.py +850 -0
  144. sandesh_relay-0.2.2/tests/test_xproj_visibility_wake.py +495 -0
@@ -0,0 +1,9 @@
1
+ ---
2
+ # Codacy analysis config — paths to exclude (Java glob syntax).
3
+ # Keeps static analysis focused on Sandesh's own code, not dev/test scaffolding
4
+ # or vendored dependencies. (The .codacy/ CLI cache itself is gitignored.)
5
+ exclude_paths:
6
+ - ".venv/**"
7
+ - "test-reports/**"
8
+ - "**/__pycache__/**"
9
+ - ".codacy/**"
@@ -0,0 +1,124 @@
1
+ name: Publish Pi extension to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch: # manual dry-run path
7
+ pull_request: # build-check only (no publish)
8
+ push:
9
+ branches: [develop]
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build-check:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: oven-sh/setup-bun@v2
20
+ - uses: actions/setup-node@v4
21
+ with:
22
+ node-version: '22'
23
+ registry-url: 'https://registry.npmjs.org'
24
+ - name: Install dependencies
25
+ working-directory: integrations/pi
26
+ run: bun install
27
+ - name: Type-check
28
+ working-directory: integrations/pi
29
+ run: bunx tsc --noEmit
30
+ - name: Test
31
+ working-directory: integrations/pi
32
+ run: bun test
33
+ - name: Pack check
34
+ working-directory: integrations/pi
35
+ run: npm pack --dry-run
36
+
37
+ publish-npm:
38
+ needs: build-check
39
+ if: github.event_name == 'release'
40
+ runs-on: ubuntu-latest
41
+ environment: npm
42
+ permissions:
43
+ id-token: write # OIDC trusted publishing
44
+ contents: read
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ with:
48
+ fetch-depth: 0 # guard needs origin/main history for the ancestry check
49
+ - name: Guard — tag ref + on-main ancestry
50
+ run: |
51
+ if ! [[ "$GITHUB_REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
52
+ echo "::error::refusing to publish: $GITHUB_REF is not a clean version tag (^refs/tags/v[0-9]+.[0-9]+.[0-9]+$)"
53
+ exit 1
54
+ fi
55
+ git fetch --no-tags origin main
56
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
57
+ echo "::error::refusing to publish: $GITHUB_SHA is not an ancestor of origin/main"
58
+ exit 1
59
+ fi
60
+ - uses: oven-sh/setup-bun@v2
61
+ - uses: actions/setup-node@v4
62
+ with:
63
+ node-version: '22'
64
+ registry-url: 'https://registry.npmjs.org'
65
+ - name: Install dependencies
66
+ working-directory: integrations/pi
67
+ run: bun install
68
+ - name: Verify release tag matches package versions (Arm B)
69
+ env:
70
+ GITHUB_REF_NAME: ${{ github.ref_name }}
71
+ run: |
72
+ VERSION="${GITHUB_REF_NAME#v}"
73
+ echo "Release tag version: $VERSION"
74
+ PKG_VERSION=$(node -p "require('./integrations/pi/package.json').version")
75
+ SERVER_VERSION=$(node -p "require('./server.json').version")
76
+ SERVER_PKG_VERSION=$(node -p "require('./server.json').packages[0].version")
77
+ echo "package.json: $PKG_VERSION"
78
+ echo "server.json .version: $SERVER_VERSION"
79
+ echo "server.json .packages[0].version: $SERVER_PKG_VERSION"
80
+ if [ "$VERSION" != "$PKG_VERSION" ]; then
81
+ echo "::error::package.json version ($PKG_VERSION) != release tag ($VERSION)"
82
+ exit 1
83
+ fi
84
+ if [ "$VERSION" != "$SERVER_VERSION" ]; then
85
+ echo "::error::server.json .version ($SERVER_VERSION) != release tag ($VERSION)"
86
+ exit 1
87
+ fi
88
+ if [ "$VERSION" != "$SERVER_PKG_VERSION" ]; then
89
+ echo "::error::server.json .packages[0].version ($SERVER_PKG_VERSION) != release tag ($VERSION)"
90
+ exit 1
91
+ fi
92
+ - name: Upgrade npm (>= 11.5.1 for OIDC trusted publishing)
93
+ run: npm install -g npm@latest
94
+ - name: Publish to npm
95
+ working-directory: integrations/pi
96
+ run: npm publish --access public
97
+ # no token secret — OIDC trusted publishing only (provenance auto)
98
+
99
+ dry-run:
100
+ needs: build-check
101
+ if: github.event_name == 'workflow_dispatch'
102
+ runs-on: ubuntu-latest
103
+ steps:
104
+ - uses: actions/checkout@v4
105
+ - uses: oven-sh/setup-bun@v2
106
+ - uses: actions/setup-node@v4
107
+ with:
108
+ node-version: '22'
109
+ registry-url: 'https://registry.npmjs.org'
110
+ - name: Install dependencies
111
+ working-directory: integrations/pi
112
+ run: bun install
113
+ - name: Type-check
114
+ working-directory: integrations/pi
115
+ run: bunx tsc --noEmit
116
+ - name: Test
117
+ working-directory: integrations/pi
118
+ run: bun test
119
+ - name: Pack check
120
+ working-directory: integrations/pi
121
+ run: npm pack --dry-run
122
+ - name: Publish dry-run
123
+ working-directory: integrations/pi
124
+ run: npm publish --dry-run --access public
@@ -0,0 +1,124 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch: # manual TestPyPI dry-run
7
+ pull_request: # build-check only (no publish)
8
+ push:
9
+ branches: [develop, main]
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0 # hatch-vcs needs tags for the version
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.x"
24
+ - name: Build sdist + wheel
25
+ run: |
26
+ python -m pip install --upgrade build twine
27
+ python -m build
28
+ twine check dist/*
29
+ - uses: actions/upload-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+ - name: Schema snapshot-sync gate (migrations ↔ current-schema.json)
34
+ run: |
35
+ python -m pip install --quiet '.[migrate]'
36
+ export XDG_DATA_HOME="$(mktemp -d)"
37
+ sandesh setup --project ci
38
+ sandesh migrate --all
39
+ sandesh migrate --dump-schema > "$XDG_DATA_HOME/dumped.json"
40
+ python -c "import json,sys; a=json.load(open('$XDG_DATA_HOME/dumped.json')); b=json.load(open('sandesh/schema/current-schema.json')); sys.exit(0 if a==b else 1)"
41
+ echo "✓ current-schema.json in sync with migrations"
42
+
43
+ create-release:
44
+ needs: build
45
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+ with:
50
+ fetch-depth: 0 # need full tag history to detect the tag at HEAD
51
+ - name: Create GitHub Release for version tag at HEAD (idempotent)
52
+ env:
53
+ GH_TOKEN: ${{ secrets.RELEASE_PAT }} # PAT so 'release: published' re-fires (default GITHUB_TOKEN would not)
54
+ run: |
55
+ TAG=$(git tag --points-at HEAD | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
56
+ if [ -z "$TAG" ]; then
57
+ echo "::notice::no version tag at HEAD — ordinary main commit, nothing to release"
58
+ exit 0
59
+ fi
60
+ if gh release view "$TAG" >/dev/null 2>&1; then
61
+ echo "release exists for $TAG — nothing to do"
62
+ exit 0
63
+ fi
64
+ gh release create "$TAG" --generate-notes
65
+
66
+ publish-pypi:
67
+ needs: build
68
+ if: github.event_name == 'release'
69
+ runs-on: ubuntu-latest
70
+ environment: pypi
71
+ permissions:
72
+ id-token: write # OIDC trusted publishing
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+ with:
76
+ fetch-depth: 0 # guard needs origin/main history for the ancestry check
77
+ - name: Guard — tag ref + on-main ancestry
78
+ run: |
79
+ if ! [[ "$GITHUB_REF" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
80
+ echo "::error::refusing to publish: $GITHUB_REF is not a clean version tag (^refs/tags/v[0-9]+.[0-9]+.[0-9]+$)"
81
+ exit 1
82
+ fi
83
+ git fetch --no-tags origin main
84
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
85
+ echo "::error::refusing to publish: $GITHUB_SHA is not an ancestor of origin/main"
86
+ exit 1
87
+ fi
88
+ - uses: actions/download-artifact@v4
89
+ with:
90
+ name: dist
91
+ path: dist/
92
+ - uses: pypa/gh-action-pypi-publish@release/v1
93
+ # no password / no token — trusted publishing; attestations default-on
94
+
95
+ publish-testpypi:
96
+ needs: build
97
+ if: github.event_name == 'workflow_dispatch'
98
+ runs-on: ubuntu-latest
99
+ environment: testpypi
100
+ permissions:
101
+ id-token: write
102
+ steps:
103
+ - uses: actions/checkout@v4
104
+ with:
105
+ fetch-depth: 0 # hatch-vcs needs tags to derive the version
106
+ - uses: actions/setup-python@v5
107
+ with:
108
+ python-version: "3.x"
109
+ - name: Version sanity — reject PEP 440 local segment ('+')
110
+ run: |
111
+ python -m pip install --upgrade build
112
+ VER=$(python -m build --sdist --no-isolation 2>/dev/null >/dev/null; ls dist/*.tar.gz 2>/dev/null | sed -E 's#.*/[a-z_-]+-(.+)\.tar\.gz#\1#' | head -1)
113
+ echo "derived version: $VER"
114
+ if echo "$VER" | grep -q '+'; then
115
+ echo "::error::version '$VER' contains a '+' local segment — TestPyPI rejects PEP 440 local versions"
116
+ exit 1
117
+ fi
118
+ - uses: actions/download-artifact@v4
119
+ with:
120
+ name: dist
121
+ path: dist/
122
+ - uses: pypa/gh-action-pypi-publish@release/v1
123
+ with:
124
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ # runtime data lives under $XDG_DATA_HOME/sandesh/, never in the repo
6
+ *.db
7
+ # dev/test tooling — not part of the stdlib-only deliverable
8
+ .venv/
9
+ .env
10
+ test-reports/
11
+ .coverage
12
+ coverage.lcov
13
+ # Codacy CLI scaffolding (local static-analysis tooling, not part of the repo)
14
+ .codacy/
15
+ # Kiro IDE steering / lean-ctx agent tooling (local, not part of the deliverable)
16
+ .kiro/
@@ -0,0 +1,319 @@
1
+ # CLAUDE.md — Sandesh Project Context
2
+
3
+ Read this before changing anything. It captures the architecture, the **locked
4
+ design decisions** (don't re-litigate them without reason), the gotchas, and the
5
+ Wave-2 roadmap. The code is small and stdlib-only — pair this doc with the four
6
+ `.py` files and you have the whole picture.
7
+
8
+ ---
9
+
10
+ ## What Sandesh Is
11
+
12
+ **Sandesh** (संदेश — Sanskrit/Hindi for *message / dispatch*) is a tiny, **standalone,
13
+ multi-project messaging system for cooperating agent/orchestrator sessions**. It is a
14
+ SQLite-backed maildir + a blocking mailbox watcher. **Pure Python stdlib — no
15
+ third-party dependencies.**
16
+
17
+ ### Origin & use case (why it exists)
18
+ It was extracted from a "Model-B" parallel-orchestration workflow: a **Mainline**
19
+ coordinator session plus worker **Track** sessions running in parallel. Those sessions
20
+ **cannot message each other directly**, and — critically — **re-invoking a *sleeping*
21
+ agent turn is only possible via the host's background-task mechanism** (e.g. Claude
22
+ Code's `run_in_background` tool, or Cron). A hook / MCP server / plain daemon *cannot*
23
+ push a turn into an idle session. Sandesh is the relay that works within that limit:
24
+
25
+ - a durable, queryable **mailbox** each session reads on demand, and
26
+ - a blocking **`notify` watcher** a session launches *in the background* so that when
27
+ mail addressed to it arrives, the watcher exits → the host wakes the agent → it
28
+ fetches.
29
+
30
+ Addresses **represent orchestrators** (`'Mainline - <Project>'`, `'Track <N> - <Project>'`),
31
+ but nothing is Claude-specific anymore — it's a general agent-messaging primitive.
32
+
33
+ ---
34
+
35
+ ## Project Classification: Standalone Python CLI tool (+ planned MCP server)
36
+
37
+ - Pure stdlib (`sqlite3`, `argparse`, `os`, `signal`, `uuid`, `socket`). No venv needed.
38
+ - **Source of truth = this repo.** It is *installed* (copied) to the XDG data dir; edits
39
+ here require a re-`./install.sh` to take effect on the installed binary.
40
+ - Provenance note: an earlier, pre-standalone copy lived in the user's Claude dotfiles
41
+ at `~/.claude/scripts/sandesh/` (it imported a Claude-specific `schedule_db`). That
42
+ copy is **superseded** by this repo and should be removed from the dotfiles to avoid
43
+ divergence.
44
+
45
+ ---
46
+
47
+ ## Status & Roadmap
48
+
49
+ - **Waves 1–8 — DONE** (through **v0.2.0**): the standalone CLI + `notify` watcher
50
+ (Wave 1), the MCP server `sandesh-mcp` (Wave 2), packaging/PyPI workflow, the Pi
51
+ extension (Wave 4), the schema-migration subsystem + installer auto-migrate (Wave 5,
52
+ CR-SAN-017/018), **the global store** (Wave 6, CR-SAN-022..025 — one global DB,
53
+ project tracker, cross-project grants, archive→tombstone lifecycle, install-assigned
54
+ super-admin, MCP 9→11 tools), **inbox search** (Wave 7, CR-SAN-026..028 — composable
55
+ filters incl. the `sender_project` proxy stream, FTS5 `search`/`reindex`, MCP
56
+ 11→12 tools), pre-release housekeeping (CR-SAN-029/030), and the **Pi catch-up**
57
+ (Wave 8, CR-SAN-031/032 — wake `deliverAs: followUp` hardening, Pi 9→12-tool parity,
58
+ ≥0.2.0 CLI session gate). Design contracts: `docs/research/PRD-global-store.md`,
59
+ `PRD-inbox-search.md`, `PRD-pi-extension.md` §8 (all AGREED).
60
+ - **Next:** registry publishes (PyPI trusted-publisher registration is a maintainer
61
+ action — RELEASING.md), and adoption by the originating orchestration workflow —
62
+ a separate, deliberate step (seed an addressbook, sessions run `notify`, migrate
63
+ off the old file-note relay).
64
+
65
+ ---
66
+
67
+ ## File Layout
68
+
69
+ ```
70
+ sandesh/ (this repo — source of truth)
71
+ ├── sandesh/ the Python package (dist name: sandesh-relay; version from git tags via hatch-vcs)
72
+ │ ├── sandesh_db.py the library: schema + all operations (no CLI, no I/O loop)
73
+ │ ├── cli.py argparse CLI over the library (one binary, all subcommands)
74
+ │ ├── notify.py the blocking mailbox watcher (run() + a thin main)
75
+ │ ├── mcp_server.py the MCP adapter (12 tools; optional [mcp] extra)
76
+ │ ├── migrate.py the yoyo-backed migration engine (optional [migrate] extra)
77
+ │ ├── migrations/ 0001-baseline … 0005-message-fts (+ rollbacks)
78
+ │ ├── schema/current-schema.json committed snapshot (CI gate: == migrate --dump-schema)
79
+ │ └── data/usage-scenarios.md the sandesh://usage MCP resource content
80
+ ├── integrations/pi/ the Pi extension (bun/TS; npm @anthill-tec/sandesh-pi; 12 tools + native wake)
81
+ ├── install.sh builds a venv at $XDG_DATA_HOME/sandesh/.venv, pip-installs [mcp,migrate],
82
+ │ symlinks launchers, then migrate --all → consolidate → reindex → admin assign
83
+ ├── tests/ 41 test files (run against a temp store; no install needed)
84
+ ├── README.md / RELEASING.md / pyproject.toml
85
+ └── CLAUDE.md (this file)
86
+
87
+ Installed (by install.sh) + runtime data:
88
+ ~/.local/share/sandesh/ ($XDG_DATA_HOME/sandesh)
89
+ ├── .venv/ the installed package + entry points
90
+ ├── sandesh.db the ONE global DB (WAL) — all projects; address, message,
91
+ │ message_recipient, notifier, project, admin (+ message_fts index)
92
+ └── projects/<project_id>/
93
+ ├── messages/msg-<id>.md message bodies (full absolute paths stored in the DB)
94
+ └── sandesh.db.pre-global legacy per-project DB, kept as backup after consolidation
95
+ ~/.local/bin/sandesh symlink → ~/.local/share/sandesh/.venv/bin/sandesh (PATH entry)
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Architecture
101
+
102
+ ### The store — XDG, one global DB, projectid-scoped
103
+ Every operation carries a **`project_id`**, but all projects share **ONE global
104
+ database** at `<data_home>/sandesh/sandesh.db` (WAL mode; `data_home` =
105
+ `$XDG_DATA_HOME` or `~/.local/share`). The `project` tracker table enrolls each
106
+ project; per-project body files live under
107
+ `<data_home>/sandesh/projects/<project_id>/messages/`. There is **no git/CWD
108
+ inference** — projectid is explicit (an MCP daemon has no CWD). The CLI accepts
109
+ `--project` (before *or* after the subcommand) or `$SANDESH_PROJECT`.
110
+ `sandesh_db.db_path()`/`connect()` open the global DB;
111
+ `sandesh_db.store_dir(project_id)` builds the body-folder path;
112
+ `sandesh_db.setup(project_id)` enrolls + provisions (idempotent; refuses a
113
+ tombstoned id).
114
+
115
+ ### Five tables (`sandesh_db._SCHEMA`)
116
+ | table | holds |
117
+ |---|---|
118
+ | `address` | the **addressbook** — durable identities, PK `address` (rejects dupes), `active` soft-delete, `project` (the address's `<Project>` part — the exact-match scoping key) |
119
+ | `message` | the **envelope** — `subject` (NOT NULL), `kind`, `in_reply_to`, `body_path` (NULL = subject-only; else a FULL absolute path) |
120
+ | `message_recipient` | **per-message addressees** — (`message_id`, `recipient`, `role` to/cc, `read_at`); PK `(message_id, recipient)` |
121
+ | `notifier` | **per-session watcher liveness** — PK `recipient`, `pid`, `token` (uuid/launch), `heartbeat_at`, `tombstone` |
122
+ | `project` | the **project tracker** — PK `project_id`, `state` (CHECK `active`\|`archived`\|`tombstoned`), `created_at`/`archived_at`/`tombstoned_at` |
123
+
124
+ ### Modules
125
+ - **`sandesh_db.py`** — the entire model + operations. Stateless functions taking a
126
+ `con` (sqlite connection) and, where bodies are involved, a `store` dir. No printing,
127
+ no loops. This is what the CLI and (future) MCP server both call.
128
+ - **`cli.py`** — argparse front end. `_ctx(args)` → `(project, store, con)`. Address
129
+ defaults: `--from`/`--to` → `$SANDESH_ADDRESS` → `$WF_TRACK`.
130
+ - **`notify.py`** — `run(project_id, address, timeout)` is the blocking poll loop;
131
+ `main()` is the `sandesh notify` entry. Interval from `$SANDESH_POLL_SECONDS`
132
+ (default 10, floor 3).
133
+
134
+ ---
135
+
136
+ ## Locked semantics (the design — change only with reason)
137
+
138
+ 1. **To wakes / Cc silent.** Both `to` and `cc` get `message_recipient` rows and are
139
+ read by `fetch`; the difference is the **wake**: `notify` polls
140
+ `unread_to()` = `role='to' AND read_at IS NULL`. Cc is delivered + readable but
141
+ never wakes — it's swept up on the recipient's next `fetch`. Conserves agent turns.
142
+ 2. **`all-tracks` broadcast — sender-project-scoped.** A reserved recipient keyword;
143
+ `send` expands it to all **active** addresses **in the sender's project**, minus the
144
+ sender. Per-recipient rows mean every recipient's watcher fires on its own row (the
145
+ sender gets none). *Re-opened by design (CR-SAN-022):* the original wording relied on
146
+ per-project stores for isolation; `docs/research/PRD-global-store.md` (AGREED
147
+ 2026-06-11) replaced those with the single global DB, so the scoping is now explicit
148
+ via `address.project` — and **cross-project messaging requires the admin's
149
+ per-project grant** (CR-SAN-023): `sandesh grant --cross-project --project <id>
150
+ --by <admin>` — one-time, inherited by every participant of the granted project,
151
+ revoked project-wide (`revoke --cross-project`). Without it, `send` to a foreign
152
+ project fails with `cross-project sending not approved for project '<id>' — ask the
153
+ Sandesh admin`. The `all-tracks` broadcast stays sender-project-scoped regardless of
154
+ grant.
155
+ 3. **Per-recipient read.** `read_at` lives on `message_recipient`, not `message` — a
156
+ broadcast/cc stays unread for the others after one reads it.
157
+ 4. **Subject-only ⟷ file-body.** `subject` is mandatory (the minimal content). No
158
+ `--body`/`--body-file` ⇒ `body_path` NULL, **no file written**. With a body, it's an
159
+ md file under `messages/`, and the DB stores its **full absolute path**.
160
+ 5. **Keep history; read=seen is the only signal.** Nothing is deleted. `read_at`
161
+ (per recipient, on `message_recipient`) is the sole "seen" signal — there is no
162
+ `message.status` disposition machine (no open/actioned/closed). A request stays
163
+ visible in history; whether it has been *acted on* is conveyed by replies, not a
164
+ status column. (CR-SAN-017 0002 dropped `message.status`; new≡migrated stores have
165
+ no status column.) **Lifecycle exception (CR-SAN-024):** `archive` still deletes
166
+ nothing (read-only, reversible), but `tombstone` is the deliberate, admin-only
167
+ exception — it purges the project's *internal* messages + recipient rows and
168
+ deletes its body folder (cross-project envelopes survive; their bodies are lost).
169
+ Standard reads then hide the tombstoned project's traffic: `inbox`/`fetch` filter
170
+ it out, and `thread` renders the exact warning `incomplete chain — message(s)
171
+ removed (project tombstoned)` where a chain passes through purged nodes.
172
+ 6. **Reply threading.** `message.in_reply_to` links a reply to its parent; `reply`
173
+ defaults `to`=parent's sender and subject=`Re: …` (no `Re: Re:`). `thread` walks the
174
+ chain. `fetch` shows `↳ re #N "<parent subject>"`. (`reply` has no `--resolves`
175
+ flag — see #5.)
176
+ 7. **Crash-safe liveness.** `notifier_live()` treats a row as dead if its `pid` is gone
177
+ OR `heartbeat_at` is older than `HEARTBEAT_STALE_SECS` (60). A clean exit removes the
178
+ row (token-guarded); a SIGKILL leaves a stale row the next `notifier_acquire()` reaps.
179
+ This is the self-heal that compensates for unreliable shutdown hooks.
180
+ 8. **Cooperative tombstone eviction.** You can't cross-session-kill another's watcher.
181
+ `notifier_tombstone(recipient)` sets a flag the watcher sees on its next poll →
182
+ it self-terminates (exit 3). `unregister` of a *live* address tombstones first,
183
+ returns `('tombstoned', pid)`; the caller retries once it's offline, then soft-deletes.
184
+ **Terminology (two concepts, one word):** the `notifier.tombstone` column here is the
185
+ *per-watcher cooperative-shutdown flag*; the project lifecycle state `tombstoned`
186
+ (CR-SAN-024, see #5) is the *permanent retirement of a whole project*. They are
187
+ unrelated mechanisms — `archive`/`tombstone` merely *use* the per-watcher flag to
188
+ evict live watchers before changing the project state.
189
+ 9. **Removal authorization.** `Mainline` may unregister anyone; any address may
190
+ unregister itself. (Honor-system; all-local cooperative orchestrators.)
191
+ 10. **Validated address format** — `'<Orchestrator> - <Project>'`, regex
192
+ `^(Mainline|Track \d+) - [A-Za-z][A-Za-z0-9_]*$`, and the `<Project>` part must equal
193
+ the `project_id`. Caught at `register` and `send`. (Extend the orchestrator
194
+ alternation in `ADDRESS_RE` if new roles appear.)
195
+
196
+ ---
197
+
198
+ ## The wake mechanism (the most important constraint)
199
+
200
+ Re-invoking a **sleeping** agent turn is **exclusive to the host's background-task
201
+ mechanism** (Claude Code's `run_in_background` tool; or Cron/scheduled wakeups). Verified
202
+ against the Claude Code hooks docs:
203
+ - `SessionStart`/`SessionEnd` hooks exist, but a hook-spawned process **cannot wake the
204
+ agent**, and `SessionEnd` is unreliable on crash/SIGKILL.
205
+ - An MCP server **cannot** push a turn into an idle session either.
206
+
207
+ **Therefore:** the `notify` watcher must be launched by the agent via its background-task
208
+ tool (which is what re-invokes it on exit). MCP (Wave 2) replaces the *verbs* (send/
209
+ fetch/…), **not** the wake. Keep `notify` as a standalone process. This is *why* the
210
+ liveness table is crash-safe rather than relying on a shutdown hook.
211
+
212
+ ---
213
+
214
+ ## How to run
215
+
216
+ ```bash
217
+ # tests (no install needed — run against a temp store; per-file, discovery is broken)
218
+ PYTHONPATH=. .venv/bin/python tests/<test_file>.py # dev venv has [mcp,migrate]
219
+
220
+ # install / re-install (venv at ~/.local/share/sandesh/.venv + migrate/consolidate/reindex)
221
+ SANDESH_ADMIN=<name> ./install.sh
222
+
223
+ # use (installed launcher; ~/.local/bin must be on PATH, else call by full path)
224
+ sandesh setup --project Demo
225
+ sandesh --project Demo register --address "Mainline - Demo" --kind mainline
226
+ sandesh --project Demo send --from "Track 1 - Demo" --to "Mainline - Demo" --subject "ping"
227
+ sandesh --project Demo notify --to "Mainline - Demo" # blocks; run in background
228
+ sandesh --project Demo fetch --to "Mainline - Demo"
229
+
230
+ # project lifecycle (CR-SAN-024) — two-tier authz, all three accept --dry-run
231
+ sandesh archive --project Demo --by "Mainline - Demo" # read-only, reversible (own Mainline)
232
+ sandesh unarchive --project Demo --by "Mainline - Demo" # back to active (own Mainline)
233
+ sandesh tombstone --project Demo --by <admin> --yes # PERMANENT; archived-only;
234
+ # ONLY the install-assigned super-admin;
235
+ # interactive confirm unless --yes
236
+ sandesh tombstone --project Demo --by <admin> --dry-run # purge counts, writes nothing
237
+ ```
238
+ Env: `$SANDESH_PROJECT`, `$SANDESH_ADDRESS` (caller's own address),
239
+ `$SANDESH_POLL_SECONDS`. `notify` exit codes: `0` mail / `2` timeout / `3` tombstoned /
240
+ `4` evicted / `5` dedup / `1` error.
241
+
242
+ ### Model-B usage pattern (how an orchestrator uses it)
243
+ Each session, at start: ensure its address is registered, then launch
244
+ `sandesh --project <P> notify --to "<self>"` **via the host's run_in_background tool**.
245
+ On wake (exit 0) → `sandesh fetch --to "<self>"` → act → relaunch `notify`. Send with
246
+ `send`/`reply`. On exit-3 (tombstoned) do **not** relaunch.
247
+
248
+ ---
249
+
250
+ ## Gotchas (learned while building — don't rediscover them)
251
+
252
+ - **argparse `--project` in both positions:** the shared `--project` (on the parent and
253
+ every subparser via `parents=[common]`) uses `default=argparse.SUPPRESS` so an absent
254
+ one doesn't clobber the value given in the other position. Removing SUPPRESS breaks
255
+ `sandesh --project X <cmd>`.
256
+ - **SQLite has no real BOOLEAN.** `active`/`tombstone` are declared `BOOLEAN` (readable)
257
+ but stored as integers `0`/`1`; `sqlite3` returns ints, so compare truthily
258
+ (`if row["tombstone"]:`), never `is True`. (`TRUE`/`FALSE` literals work since 3.23.)
259
+ - **Body paths are absolute.** `send` stores the full path; `fetch` opens it directly
260
+ (with a legacy relative-path fallback under `store`).
261
+ - **`store` vs `con`.** Functions that touch body files take a `store` dir; pure-DB
262
+ functions take only `con`. Keep that split.
263
+ - **`sqlite_sequence`** appears once any AUTOINCREMENT table exists — it's sqlite-internal,
264
+ harmless.
265
+ - **The launcher** resolves its own real path via `readlink -f`, so it works through the
266
+ `~/.local/bin/sandesh` symlink. Don't hardcode the install path.
267
+ - **Schema changes ship via the migration subsystem.** Don't hand-evolve a store's schema —
268
+ add a migration. The `sandesh migrate` CLI command (gated behind the optional **`[migrate]`**
269
+ extra — `yoyo` + `jsonschema`) applies pending migrations; `--status`/`--rollback`/`--check`
270
+ inspect and reverse them. On update the installer **auto-migrates** the global DB
271
+ (`install.sh` runs `sandesh migrate --all`), and the committed
272
+ `sandesh/schema/current-schema.json` snapshot must stay in sync with `migrations/` — the CI
273
+ gate in `publish-pypi.yml` asserts `migrate --dump-schema` equals that snapshot.
274
+ - **`migrate` no longer accepts `--project`** (CR-SAN-022): the global DB is the single
275
+ migration target, so the subcommand has no per-project routing — `sandesh migrate
276
+ --project X` is a CLI error.
277
+ - **`setup` of a tombstoned project refuses** (PRD O1): the tracker row is terminal; the
278
+ raised error message contains `retired (tombstoned)` and the row is left unchanged.
279
+ - **Legacy per-project stores are auto-consolidated by the installer.** `install.sh` runs
280
+ `sandesh consolidate` (stdlib-only, idempotent) after the migrate block: it imports each
281
+ `projects/<id>/sandesh.db` into the global DB (ids remapped, reply chains relinked,
282
+ body files unmoved), enrolls the project, and keeps the legacy file as
283
+ `sandesh.db.pre-global`.
284
+ - **The super-admin is NOT an address.** A single-row `admin` table (`CHECK (id = 1)`)
285
+ holds the Sandesh admin's name — never messageable, registrable, or listable. It is
286
+ assigned ONLY at install time via `$SANDESH_ADMIN` (an inline venv-python call in
287
+ `install.sh`); there is deliberately NO CLI or MCP surface to create/change it, and a
288
+ different-name re-assign is refused (`refusing to silently re-assign`).
289
+ - **`register` requires enrollment.** Registering into a project with no tracker row
290
+ fails with `unknown project '<id>'` — run `setup` first (it enrolls the project).
291
+
292
+ ---
293
+
294
+ ## The MCP server (shipped — Wave 2 + 6 + 7)
295
+
296
+ `sandesh/mcp_server.py` (FastMCP, stdio; the optional **`[mcp]`** extra) exposes **12 tools**:
297
+ setup, register, unregister, addressbook, send, reply, inbox, fetch, thread (Wave 2),
298
+ archive, unarchive (Wave 6 — tombstone/grant/revoke/admin are NEVER exposed), and search
299
+ (Wave 7). Inbox/fetch carry the six filter params; `project_id` is optional everywhere it
300
+ can be derived (and accepted-but-unused on the recipient-keyed tools). Errors map
301
+ `ValueError`/`PermissionError` → `ToolError`. **The wake is NOT in MCP** — `notify` stays a
302
+ background process (the agent's host re-invokes it; see the wake section above). The Pi
303
+ extension (`integrations/pi/`) mirrors the same 12-tool surface over the CLI, with a native
304
+ wake loop (`sendUserMessage(…, {deliverAs:"followUp"})`) and a ≥0.2.0 CLI session gate.
305
+
306
+ ---
307
+
308
+ ## Conventions
309
+
310
+ - **Git:** the user works git-flow style (feature branches off `develop`).
311
+ **Commit/push only when asked; branch before committing on the default branch.**
312
+ - **Commit messages: NEVER add Claude attribution** ("Generated with Claude",
313
+ "Co-Authored-By: Claude"). Clean, technical messages only.
314
+ - **New dependency?** The core's whole virtue is stdlib-only — runtime deps live ONLY
315
+ behind the optional extras (`[mcp]` = the MCP SDK; `[migrate]` = yoyo + jsonschema);
316
+ adding one is a deliberate decision; read the real upstream API first.
317
+ - Keep `sandesh_db.py` pure (model + ops, no printing/looping); presentation in `cli.py`,
318
+ the loop in `notify.py`, the MCP protocol in `mcp_server.py`.
319
+ ```