heinzel-slack 0.1.0__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 (123) hide show
  1. heinzel_slack-0.1.0/.copier-answers.yml +5 -0
  2. heinzel_slack-0.1.0/.gitignore +10 -0
  3. heinzel_slack-0.1.0/AGENTS.md +39 -0
  4. heinzel_slack-0.1.0/CHANGELOG.md +9 -0
  5. heinzel_slack-0.1.0/CLAUDE.md +1 -0
  6. heinzel_slack-0.1.0/LICENSE +21 -0
  7. heinzel_slack-0.1.0/Makefile +19 -0
  8. heinzel_slack-0.1.0/PKG-INFO +215 -0
  9. heinzel_slack-0.1.0/README.md +203 -0
  10. heinzel_slack-0.1.0/docs/SCREENSHOT_STORY.md +38 -0
  11. heinzel_slack-0.1.0/docs/VENDORING.md +86 -0
  12. heinzel_slack-0.1.0/docs/assets/screenshots/capture.json +84 -0
  13. heinzel_slack-0.1.0/docs/assets/screenshots/index.html +1 -0
  14. heinzel_slack-0.1.0/docs/assets/screenshots/review-slack-notice.png +0 -0
  15. heinzel_slack-0.1.0/docs/assets/screenshots/review-slack-notice.txt +73 -0
  16. heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-cli.png +0 -0
  17. heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-cli.txt +15 -0
  18. heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-decision-cli.png +0 -0
  19. heinzel_slack-0.1.0/docs/assets/screenshots/slack-draft-decision-cli.txt +12 -0
  20. heinzel_slack-0.1.0/docs/assets/screenshots/slack-notice-delivered.png +0 -0
  21. heinzel_slack-0.1.0/docs/assets/screenshots/slack-notice-delivered.txt +30 -0
  22. heinzel_slack-0.1.0/docs/assets/screenshots/source-story.md +38 -0
  23. heinzel_slack-0.1.0/docs/screenshot-definition.json +7 -0
  24. heinzel_slack-0.1.0/docs/screenshot_fixture.py +487 -0
  25. heinzel_slack-0.1.0/docs/screenshots.toml +51 -0
  26. heinzel_slack-0.1.0/plugin-lock.json +41 -0
  27. heinzel_slack-0.1.0/pyproject.toml +86 -0
  28. heinzel_slack-0.1.0/requirements.lock +1175 -0
  29. heinzel_slack-0.1.0/src/heinzel_slack/__init__.py +181 -0
  30. heinzel_slack-0.1.0/src/heinzel_slack/_blocks.py +125 -0
  31. heinzel_slack-0.1.0/src/heinzel_slack/_channel.py +67 -0
  32. heinzel_slack-0.1.0/src/heinzel_slack/_decide.py +108 -0
  33. heinzel_slack-0.1.0/src/heinzel_slack/_digest.py +40 -0
  34. heinzel_slack-0.1.0/src/heinzel_slack/_models.py +34 -0
  35. heinzel_slack-0.1.0/src/heinzel_slack/_module.py +5 -0
  36. heinzel_slack-0.1.0/src/heinzel_slack/_sdk.py +64 -0
  37. heinzel_slack-0.1.0/src/heinzel_slack/_service.py +455 -0
  38. heinzel_slack-0.1.0/src/heinzel_slack/_settings.py +63 -0
  39. heinzel_slack-0.1.0/src/heinzel_slack/_slack.py +297 -0
  40. heinzel_slack-0.1.0/src/heinzel_slack/_store.py +333 -0
  41. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/__init__.py +1 -0
  42. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/LICENSE +21 -0
  43. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/__init__.py +54 -0
  44. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/errors/__init__.py +56 -0
  45. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/__init__.py +48 -0
  46. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/builtin_handlers.py +110 -0
  47. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/builtin_interval_calculators.py +45 -0
  48. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/handler.py +82 -0
  49. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/interval_calculator.py +13 -0
  50. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/jitter.py +25 -0
  51. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/request.py +36 -0
  52. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/response.py +23 -0
  53. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/http_retry/state.py +21 -0
  54. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/__init__.py +57 -0
  55. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/attachments/__init__.py +590 -0
  56. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/basic_objects.py +132 -0
  57. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/__init__.py +162 -0
  58. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/basic_components.py +742 -0
  59. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/block_elements.py +2335 -0
  60. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/blocks/blocks.py +1302 -0
  61. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/messages/__init__.py +91 -0
  62. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/messages/chunk.py +159 -0
  63. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/metadata/__init__.py +1255 -0
  64. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/models/views/__init__.py +232 -0
  65. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/proxy_env_variable_loader.py +25 -0
  66. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/py.typed +0 -0
  67. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/__init__.py +13 -0
  68. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/__init__.py +5 -0
  69. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/client.py +294 -0
  70. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/connection.py +449 -0
  71. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/frame_header.py +47 -0
  72. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/builtin/internals.py +413 -0
  73. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/client.py +159 -0
  74. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/interval_runner.py +33 -0
  75. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/listeners.py +17 -0
  76. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/logger/__init__.py +0 -0
  77. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/logger/messages.py +6 -0
  78. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/request.py +57 -0
  79. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/socket_mode/response.py +28 -0
  80. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/version.py +3 -0
  81. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/__init__.py +12 -0
  82. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/base_client.py +640 -0
  83. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/chat_stream.py +243 -0
  84. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/client.py +6649 -0
  85. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/deprecation.py +52 -0
  86. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/file_upload_v2_result.py +7 -0
  87. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/internal_utils.py +440 -0
  88. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/web/slack_response.py +196 -0
  89. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/__init__.py +10 -0
  90. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/client.py +279 -0
  91. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/internal_utils.py +45 -0
  92. heinzel_slack-0.1.0/src/heinzel_slack/_vendor/slack_sdk/webhook/webhook_response.py +16 -0
  93. heinzel_slack-0.1.0/src/heinzel_slack/links/__init__.py +15 -0
  94. heinzel_slack-0.1.0/src/heinzel_slack/links/_cli_process.py +61 -0
  95. heinzel_slack-0.1.0/src/heinzel_slack/links/_reviewed_link.py +366 -0
  96. heinzel_slack-0.1.0/src/heinzel_slack/manifest.json +25 -0
  97. heinzel_slack-0.1.0/src/heinzel_slack/py.typed +0 -0
  98. heinzel_slack-0.1.0/tests/acceptance/channel_host_test.py +98 -0
  99. heinzel_slack-0.1.0/tests/acceptance/install_test.py +51 -0
  100. heinzel_slack-0.1.0/tests/acceptance/links_registration_test.py +63 -0
  101. heinzel_slack-0.1.0/tests/conftest.py +165 -0
  102. heinzel_slack-0.1.0/tests/e2e/live_test.py +83 -0
  103. heinzel_slack-0.1.0/tests/fixtures/captured_writer/pyproject.toml +18 -0
  104. heinzel_slack-0.1.0/tests/fixtures/captured_writer/src/fixture_captured_writer/__init__.py +66 -0
  105. heinzel_slack-0.1.0/tests/fixtures/channel_host_probe.py +246 -0
  106. heinzel_slack-0.1.0/tests/integration/approval_test.py +352 -0
  107. heinzel_slack-0.1.0/tests/integration/cli_process_test.py +31 -0
  108. heinzel_slack-0.1.0/tests/integration/manifest_test.py +25 -0
  109. heinzel_slack-0.1.0/tests/integration/notify_test.py +57 -0
  110. heinzel_slack-0.1.0/tests/integration/operations_test.py +179 -0
  111. heinzel_slack-0.1.0/tests/integration/optional_bot_test.py +124 -0
  112. heinzel_slack-0.1.0/tests/integration/reviewed_link_test.py +273 -0
  113. heinzel_slack-0.1.0/tests/integration/reviewed_target_test.py +108 -0
  114. heinzel_slack-0.1.0/tests/integration/sdk_boundary_test.py +196 -0
  115. heinzel_slack-0.1.0/tests/integration/socket_mask_test.py +76 -0
  116. heinzel_slack-0.1.0/tests/integration/socket_transport_test.py +252 -0
  117. heinzel_slack-0.1.0/tests/integration/store_security_test.py +161 -0
  118. heinzel_slack-0.1.0/tests/integration/vendor_typing_test.py +73 -0
  119. heinzel_slack-0.1.0/tests/integration/web_api_test.py +221 -0
  120. heinzel_slack-0.1.0/tests/unit/blocks_test.py +47 -0
  121. heinzel_slack-0.1.0/tests/unit/digest_test.py +26 -0
  122. heinzel_slack-0.1.0/vendor-manifest.json +326 -0
  123. heinzel_slack-0.1.0/vendor-provenance/slack_sdk-3.46.0-py2.py3-none-any.whl +0 -0
@@ -0,0 +1,5 @@
1
+ # Generated by Copier. Keep this file so the template can be updated.
2
+ _commit: v0.9.8
3
+ _src_path: gh:helmut-hoffer-von-ankershoffen/heinzel
4
+ plugin_name: slack
5
+ plugin_summary: Tell the owner what needs them, and ask for approval of an exact draft, in their own Slack DM.
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ .acceptance/
3
+ .preview/
4
+ dist/
5
+ release-dist/
6
+ __pycache__/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .coverage*
10
+ *.egg-info/
@@ -0,0 +1,39 @@
1
+ # heinzel-slack
2
+
3
+ Read [README.md](README.md). Edit the feature in `src/heinzel_slack/` and its tests; use the public `heinzel`
4
+ and `heinzel.kernel` API. One `@module.operation` supplies CLI, MCP and REST. Add a service or model when the
5
+ feature needs it; neither is required for a small operation. Never import a plugin implementation or core internals.
6
+
7
+ Run from this plugin's directory:
8
+ ```sh
9
+ make setup
10
+ make check-fast
11
+ make check
12
+ make build
13
+ make acceptance
14
+ make preview
15
+ make lock # after changing dependencies
16
+ make release # preview only
17
+ ```
18
+
19
+ Keep all feature changes in this directory. Setup never initializes Git for a collection plugin. The owning
20
+ Makefile works even when the parent repository builds something else. A parent that provides collection commands
21
+ also accepts `NAME=heinzel-slack` from its root. Publication uses the parent's generic plugin release workflow;
22
+ do not copy a nested `.github/` or edit a parent registry.
23
+
24
+
25
+ Each pytest file declares exactly one level: `unit`, `integration` or `e2e`. Write a behavior test for a change.
26
+ Run the fast loop while editing and the owning full/installed-wheel checks before release. These commands install
27
+ a compatible core wheel; they never run the core suite. `CORE=<wheel>` explicitly selects a candidate SDK.
28
+
29
+ The source operation in the starter shows the minimum abstraction. Use the contributions preset or
30
+ [extension guide](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/PLUGINS.md) for views,
31
+ priorities, Today widgets, notifications and shared contracts. Ordinary dependencies belong in this project's
32
+ `pyproject.toml`; regenerate its own lock. Use conventional commits and review nontrivial changes.
33
+
34
+ Publishing requires a merged commit, this plugin's version and configured GitHub/PyPI publishing. Local
35
+ `release` only checks/builds and previews; `publish` dispatches the protected workflow. Never upload a different
36
+ rebuild under an existing version. See the README's release steps.
37
+ Private sources use private GitHub releases and the installing person's `gh` login. PyPI is public; do not publish
38
+ private sources there. The [custom plugin guide](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/PLUGIN_AUTHORING.md)
39
+ documents separate repositories, catalog discovery and both release paths.
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0
6
+
7
+ First version: notices and shared drafts in the owner's own Slack direct message, through the owner's private Slack
8
+ app (official Web API and Socket Mode). Approve and Decline are bound to the exact draft bytes and to the owner;
9
+ a reply, a reaction, another person, a replaced or expired draft and a replayed click never approve.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Plugin authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ .DEFAULT_GOAL := help
2
+ TOOLING ?= heinzel-plugin-tools @ https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/releases/download/tools-v0.3.7/heinzel_plugin_tools-0.3.7-py3-none-any.whl\#sha256=ac959c164e3726f86311c86fa9d3b4a3cbc82a6b4f81fae8bf462ee8103bd359
3
+ unexport TOOLING CORE TAG REGISTRY
4
+ export HZ_PLUGIN_TOOLING := $(value TOOLING)
5
+ export PLUGIN_CORE := $(value CORE)
6
+ export PLUGIN_TAG := $(value TAG)
7
+ export PLUGIN_REGISTRY := $(value REGISTRY)
8
+ MAKEOVERRIDES =
9
+ PLUGIN := uv run --quiet --no-project --with "$$HZ_PLUGIN_TOOLING" hz-plugin
10
+
11
+ .PHONY: help setup fix check-fast check test lock preview build acceptance audit release preflight publish
12
+ help:
13
+ @echo 'make setup; make check-fast; make check; make preview; make build; make acceptance'
14
+ @echo 'make lock refreshes this plugin dependency lock; CORE=<wheel> selects a candidate SDK.'
15
+ @echo 'make release previews; make publish dispatches the protected release workflow.'
16
+ setup fix check-fast check test lock preview build acceptance audit release preflight:
17
+ @$(PLUGIN) $@
18
+ publish:
19
+ @$(PLUGIN) publish
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.5
2
+ Name: heinzel-slack
3
+ Version: 0.1.0
4
+ Summary: Tell the owner what needs them, and ask for approval of an exact draft, in their own Slack DM.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ License-File: src/heinzel_slack/_vendor/slack_sdk/LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: heinzel<0.10,>=0.9.20
10
+ Requires-Dist: slack-sdk==3.46.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ > This plugin is in development. Its offline fixtures use fictional Slack transports and accounts.
14
+ > Installed acceptance against the released SDK and a new gallery review are pending. The existing
15
+ > captures illustrate earlier behavior.
16
+
17
+ # heinzel-slack
18
+
19
+ Slack as an extra notification channel and as a place to share a draft and get a yes or no back: a
20
+ [Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin, released on its own.
21
+
22
+ - **A notice says why.** Every message carries "Why you are seeing this", because a nudge that cannot say why is noise.
23
+ - **A draft is shared with Approve and Decline.** The answer is for the exact text you saw, and only you can give it.
24
+ - **Your DM only.** It writes to your direct message with your own app. Never a channel, never anyone else.
25
+
26
+ ```text
27
+ Lotte has a draft for you
28
+ Weekly report
29
+ Why you are seeing this: It is Friday and you asked for a report.
30
+ | Weekly report ... |
31
+ Nothing goes out until you say so. Fingerprint 3fa9c1d2b4e0 · answer by 16:00 · only your click counts.
32
+ [ Approve ] [ Decline ]
33
+ ```
34
+
35
+ ## Prerequisites
36
+
37
+ The bot channel needs all of these. The separate reviewed-link adapter does not use the bot or app tokens.
38
+ For links-only use, explicitly run `hz system config set slack.bot_enabled false` and configure the SlackCLI
39
+ path/profile described below. The bot's local and live health checks then report it disabled without contacting
40
+ Slack; reviewed links still require their own exact account, target, grant and human approval checks.
41
+
42
+ `slack.bot_enabled true` enables the bot and reports every missing prerequisite. When unset, any configured
43
+ owner/workspace or stored bot/app token retains the existing bot setup and its missing-prerequisite checks,
44
+ even when SlackCLI selectors are also configured. An entirely unconfigured bot is optional and neutral.
45
+ `hz setup slack` explicitly enables the bot. Disabling it prevents bot sends, listening and approval consumption;
46
+ it does not grant reviewed-link delivery or remove its independent safeguards.
47
+
48
+ 1. **Heinzelmännchen 0.9.20 or newer within the 0.9 series.** The development lock uses 0.9.20.
49
+ 2. **A Slack workspace where you may install an app.** You create a *private app that only you use*, in your own
50
+ workspace, from the manifest this plugin ships. If your workspace lets only admins install apps, ask for approval
51
+ of that one app (the manifest lists exactly what it can do) or use a workspace of your own. The plugin does not
52
+ work around that setting.
53
+ 3. **Your Slack member ID**: your profile, the three dots, "Copy member ID" (like `U01ABCDEF`), and your
54
+ **workspace ID** (like `T01ABCDEF`), configured as `slack.owner` and `slack.team`. Both are required.
55
+ 4. **Two tokens from your app**, kept in the OS keychain by `hz`, never in a file or a repository:
56
+ - the **bot token** (`xoxb-...`, on *Install App*), scopes `chat:write`, `im:write`, `users:read`;
57
+ - an **app-level token** (`xapp-...`, on *Basic Information*, scope `connections:write`) for Socket Mode.
58
+ 5. **Python 3.12+** (supplied by uv) and an internet connection to `slack.com`. No public address, no port to open.
59
+
60
+ ## Set up (about three minutes)
61
+
62
+ ```bash
63
+ hz slack manifest # copy the JSON
64
+ # api.slack.com/apps, Create New App, From a manifest, paste it, Create, Install to Workspace.
65
+ # Basic Information, App-Level Tokens, Generate Token with connections:write.
66
+ hz system credential set bot-token --service slack
67
+ hz system credential set app-token --service slack
68
+ hz system config set slack.owner U01ABCDEF # or: hz setup slack
69
+ hz system config set slack.team T01ABCDEF
70
+ hz system doctor # then: hz system connector test slack
71
+ ```
72
+
73
+ The adapter registers notification and approval ports for the core channel consumer. Registration
74
+ alone does not route notices or grant consent. In the installed offline acceptance flow, the source-bound host
75
+ reviews each exact posting target and message through its normal human confirmation surface before sending.
76
+ Consenting to post a draft does not approve that draft: an owner click is a separate claim the host verifies.
77
+ The plugin operations can be used explicitly:
78
+
79
+ ```bash
80
+ hz slack notify "Build done" "You asked to know when it finishes." --helper Otto
81
+ hz slack ask "Send the weekly report" "$(cat report.md)" --helper Lotte
82
+ hz slack wait <request-id> # listens for your click, up to --seconds; or: hz slack status <request-id>
83
+ ```
84
+
85
+ **Clicks are only registered while something listens** (`wait`, `listen`, or the five-minute `listen` job while a draft is waiting). A button pressed with
86
+ nobody listening shows Slack's "not responding" notice and the draft stays waiting; press it again once `hz` listens.
87
+ The message says nothing false in the meantime: it still shows the buttons because it is still unanswered.
88
+
89
+ ## What approval means here
90
+
91
+ Approval follows a typed code path (T3). `DraftStanding.approves(draft)` checks an approved state and the exact
92
+ draft digest. A host consuming `ChannelApprovalReply` must separately validate the request, actor, argument digest
93
+ and expiry before acting. The plugin records a decision only if all of this held:
94
+
95
+ | Held at click time | How |
96
+ | --- | --- |
97
+ | It is a **button** press on our message | Replies, reactions and slash commands are not even subscribed to, and any such payload is refused |
98
+ | The person is **you** | The clicker's Slack ID equals the owner recorded *before* posting; a project `hz.toml` cannot set it |
99
+ | Right **workspace, conversation and message** | Compared with what was recorded when the message was posted |
100
+ | The **exact bytes** | The button carries the host argument digest; the stored preview is hashed again under the lock; `DraftStanding.approves(draft)` hashes what you pass |
101
+ | Still **fresh** | Pending, not expired (default 4 hours), click time plausible; replacing a draft requires an explicit host `settle(reference, "superseded")` call |
102
+ | **Once** | The move from pending to decided is a single atomic update, so a second click, a Slack retry or a replay is refused |
103
+
104
+ Everything else from Slack is data. Only identifiers are read; message text, names and replies are never parsed for
105
+ meaning. A draft containing control, zero-width or bidirectional characters is refused (it could not be reviewed as
106
+ it is), and a draft over 4,000 characters is refused (you could not read it all in Slack). The draft is shown in a
107
+ literal preformatted block, so mentions and links in it are inert. Refused clicks are recorded in the audit trail
108
+ (`approvals.sqlite3` in the state folder, readable by you only) with a code of ours, never Slack's text.
109
+
110
+ What this does **not** prove: that a human, rather than software running as you in Slack, pressed the button. It is an
111
+ application-level decision by the owner's Slack account, as in `hz` core's own confirmations. Anyone who controls
112
+ your Slack account or the machine controls your approvals.
113
+
114
+ ## Why your own app and not a browser session or the Slack CLI
115
+
116
+ | Approach | Verdict |
117
+ | --- | --- |
118
+ | **Browser session tokens** (community CLIs such as `slackcli` with `xoxc`/`xoxd`) | Acts as you, nothing to install, but it is an unofficial use of your login that Slack can break or forbid at any time, tokens die when you sign out, and it cannot receive button clicks. Approval would be a reaction or a typed reply, which cannot be bound to the exact bytes. Not a dependency for a product. |
119
+ | **The official Slack CLI** (`slack`) | Builds and runs apps on Slack's platform and manages manifests; it has no way to post a message or receive a click for another tool. Useful for creating and installing the app during development. |
120
+ | **A private Slack app** (this plugin): bot token for the Web API, Socket Mode for clicks, official `slack-sdk` | Supported by Slack, least privilege (three bot scopes, no user scope, no event subscriptions), signed identity of the clicker in every click, and no inbound address, so nothing is hosted. **Chosen.** |
121
+
122
+ Tenets weighed: P1 ("your identity over a bot identity") and ADR 0035 ("only what an ordinary employee may do, no admin-registered
123
+ apps") against T3 and P2 (approval must be bound and verifiable) and distributability. The app here is yours alone, in
124
+ your workspace, created by you, and it only ever writes to your own DM as a mailbox: it never posts as you and never to
125
+ a shared channel. Anything published under your name later is a separate step with your identity. If you cannot install an
126
+ app, the plugin reports so; it does not fall back to a browser session.
127
+
128
+ ## Ports
129
+
130
+ The plugin registers one service, `SlackChannel`, a `NotificationChannel` and an `ApprovalChannel` as the candidate
131
+ SDK defines them; core imports nothing from this plugin and names no Slack. A channel carries words and never decides:
132
+
133
+ - `delivery_target()` observes a `ChannelTarget`: authenticated workspace/bot account, exact owner DM, and
134
+ versioned owner-bound posting scope. This observation posts no message and grants no authority.
135
+ - `deliver_reviewed(notice, target)` and `post_reviewed(request, target)` require that exact target. They verify it
136
+ again at mutation using the same immutable credential selection and SDK client that will post, with retries
137
+ disabled. A changed account, DM or scope refuses without a post; legacy methods are never a host fallback.
138
+ - `deliver(notice)` is the explicit plugin operation path and posts at most once per `key`. The receipt says what
139
+ was observed: `delivered`, `refused` (Slack said no, safe to retry) or `unknown` (the answer was lost, so it may have been sent and is never retried by itself).
140
+ - `post(request)` shows the exact preview with Approve and Decline, bound to the request's `arguments_sha256` and expiry,
141
+ idempotent by `request_id`. `replies(references)` returns only clicks that passed every check above, with the owner's
142
+ workspace and member ID as the actor; the host must check that actor, digest and expiry again before it
143
+ consumes any approval.
144
+ - `settle(reference, outcome)` is an explicit host call to close the message once the host has acted, expired it or
145
+ replaced it. Posting another request never supersedes an earlier one automatically. Every message update first
146
+ verifies that current credentials address the recorded workspace and the owner's recorded DM.
147
+
148
+ Credential selection is pinned for each conditional call. Rotating credentials after the client is acquired cannot
149
+ redirect that call; it does not transactionally cancel an already pinned, correctly reviewed operation. Revocation
150
+ can make Slack refuse it, while a lost acknowledgement remains `unknown` and is never retried automatically.
151
+
152
+ For development against a different compatible core artifact, select its wheel with `CORE=<wheel>`.
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ make check-fast
158
+ ```
159
+
160
+ The closed SDK subset, upstream wheel pin, license and regeneration policy are described in
161
+ [the vendoring notes](docs/VENDORING.md). Vendor-aware tooling verifies exact upstream and projected bytes before
162
+ formatting, checks or builds, and verifies the built wheel again. Only verified upstream implementation files receive
163
+ dependency treatment; first-party lint, strict typing and the 90% coverage floor remain unchanged.
164
+
165
+ The in-memory Slack in `tests/conftest.py` is a labelled fake. `tests/e2e/live_test.py` talks to a real workspace when
166
+ `SLACK_PROOF=1`; the tokens come from the environment (the official `slack run` injects them without printing them).
167
+
168
+ <!-- screenshots:start -->
169
+ ## Screenshots
170
+
171
+ <img src="docs/assets/screenshots/review-slack-notice.png" alt="Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery." width="960">
172
+
173
+ Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery.
174
+
175
+ <img src="docs/assets/screenshots/slack-notice-delivered.png" alt="Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write." width="960">
176
+
177
+ Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write.
178
+
179
+ <details>
180
+ <summary>Command line</summary>
181
+
182
+ <img src="docs/assets/screenshots/slack-draft-cli.png" alt="Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks." width="960">
183
+
184
+ Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks.
185
+
186
+ <img src="docs/assets/screenshots/slack-draft-decision-cli.png" alt="Demo data: fictional clicks exercise the real decision code; Mira&#x27;s exact approval records one reply while invalid attempts are refused." width="960">
187
+
188
+ Demo data: fictional clicks exercise the real decision code; Mira&#x27;s exact approval records one reply while invalid attempts are refused.
189
+
190
+ </details>
191
+
192
+ [Open the screenshot gallery](docs/assets/screenshots/index.html).
193
+ <!-- screenshots:end -->
194
+
195
+ ### Approved document links through SlackCLI
196
+
197
+ The separate source-only `SlackReviewedLinks` companion uses an existing SlackCLI 0.18.0 browser profile.
198
+ Its workflow channel name is `slack-reviewed-links`, registered in the separate `slack.links` module record.
199
+ The existing `slack` bot channel retains its own unique ordinary notification and approval service. Select
200
+ an absolute native executable with `slack.cli_path` and the exact stored profile key with `slack.cli_profile`.
201
+ These owner settings are ignored in project configuration. It resolves the authenticated user's existing self DM
202
+ before the host asks for approval, and binds that account, destination and captured source into the reviewed target.
203
+ It sends only the exact approved Google Doc URL. Private review notices use the host's ordinary app notifications.
204
+
205
+ No bot or app token is required for this companion. File-backed browser profiles are privately pinned along with the
206
+ native binary; keychain-backed profiles and standard-token SDK profiles refuse. The standard SDK transport may retry
207
+ uncertain writes, so that mode cannot satisfy this companion's acknowledgement contract. Missing or ambiguous self
208
+ DMs refuse without creating a conversation. The exact selected CLI source and destination are checked again after
209
+ the durable delivery intent. Timeout, malformed acknowledgement and replay retain uncertainty and never resend.
210
+ The existing bot-backed notice/approval channel and its verified SDK projection remain separate.
211
+
212
+ The reviewed CLI behavior comes from [SlackCLI v0.18.0](https://github.com/shaharia-lab/slackcli/tree/v0.18.0),
213
+ including its fixed home-relative profile store, explicit workspace selector, browser-write retry policy and structured
214
+ message acknowledgement. Local contract tests use fictional profiles and provider replies. Live account delivery
215
+ and installed acceptance against the released SDK still need verification.
@@ -0,0 +1,203 @@
1
+ > This plugin is in development. Its offline fixtures use fictional Slack transports and accounts.
2
+ > Installed acceptance against the released SDK and a new gallery review are pending. The existing
3
+ > captures illustrate earlier behavior.
4
+
5
+ # heinzel-slack
6
+
7
+ Slack as an extra notification channel and as a place to share a draft and get a yes or no back: a
8
+ [Heinzelmännchen](https://github.com/helmut-hoffer-von-ankershoffen/heinzel) plugin, released on its own.
9
+
10
+ - **A notice says why.** Every message carries "Why you are seeing this", because a nudge that cannot say why is noise.
11
+ - **A draft is shared with Approve and Decline.** The answer is for the exact text you saw, and only you can give it.
12
+ - **Your DM only.** It writes to your direct message with your own app. Never a channel, never anyone else.
13
+
14
+ ```text
15
+ Lotte has a draft for you
16
+ Weekly report
17
+ Why you are seeing this: It is Friday and you asked for a report.
18
+ | Weekly report ... |
19
+ Nothing goes out until you say so. Fingerprint 3fa9c1d2b4e0 · answer by 16:00 · only your click counts.
20
+ [ Approve ] [ Decline ]
21
+ ```
22
+
23
+ ## Prerequisites
24
+
25
+ The bot channel needs all of these. The separate reviewed-link adapter does not use the bot or app tokens.
26
+ For links-only use, explicitly run `hz system config set slack.bot_enabled false` and configure the SlackCLI
27
+ path/profile described below. The bot's local and live health checks then report it disabled without contacting
28
+ Slack; reviewed links still require their own exact account, target, grant and human approval checks.
29
+
30
+ `slack.bot_enabled true` enables the bot and reports every missing prerequisite. When unset, any configured
31
+ owner/workspace or stored bot/app token retains the existing bot setup and its missing-prerequisite checks,
32
+ even when SlackCLI selectors are also configured. An entirely unconfigured bot is optional and neutral.
33
+ `hz setup slack` explicitly enables the bot. Disabling it prevents bot sends, listening and approval consumption;
34
+ it does not grant reviewed-link delivery or remove its independent safeguards.
35
+
36
+ 1. **Heinzelmännchen 0.9.20 or newer within the 0.9 series.** The development lock uses 0.9.20.
37
+ 2. **A Slack workspace where you may install an app.** You create a *private app that only you use*, in your own
38
+ workspace, from the manifest this plugin ships. If your workspace lets only admins install apps, ask for approval
39
+ of that one app (the manifest lists exactly what it can do) or use a workspace of your own. The plugin does not
40
+ work around that setting.
41
+ 3. **Your Slack member ID**: your profile, the three dots, "Copy member ID" (like `U01ABCDEF`), and your
42
+ **workspace ID** (like `T01ABCDEF`), configured as `slack.owner` and `slack.team`. Both are required.
43
+ 4. **Two tokens from your app**, kept in the OS keychain by `hz`, never in a file or a repository:
44
+ - the **bot token** (`xoxb-...`, on *Install App*), scopes `chat:write`, `im:write`, `users:read`;
45
+ - an **app-level token** (`xapp-...`, on *Basic Information*, scope `connections:write`) for Socket Mode.
46
+ 5. **Python 3.12+** (supplied by uv) and an internet connection to `slack.com`. No public address, no port to open.
47
+
48
+ ## Set up (about three minutes)
49
+
50
+ ```bash
51
+ hz slack manifest # copy the JSON
52
+ # api.slack.com/apps, Create New App, From a manifest, paste it, Create, Install to Workspace.
53
+ # Basic Information, App-Level Tokens, Generate Token with connections:write.
54
+ hz system credential set bot-token --service slack
55
+ hz system credential set app-token --service slack
56
+ hz system config set slack.owner U01ABCDEF # or: hz setup slack
57
+ hz system config set slack.team T01ABCDEF
58
+ hz system doctor # then: hz system connector test slack
59
+ ```
60
+
61
+ The adapter registers notification and approval ports for the core channel consumer. Registration
62
+ alone does not route notices or grant consent. In the installed offline acceptance flow, the source-bound host
63
+ reviews each exact posting target and message through its normal human confirmation surface before sending.
64
+ Consenting to post a draft does not approve that draft: an owner click is a separate claim the host verifies.
65
+ The plugin operations can be used explicitly:
66
+
67
+ ```bash
68
+ hz slack notify "Build done" "You asked to know when it finishes." --helper Otto
69
+ hz slack ask "Send the weekly report" "$(cat report.md)" --helper Lotte
70
+ hz slack wait <request-id> # listens for your click, up to --seconds; or: hz slack status <request-id>
71
+ ```
72
+
73
+ **Clicks are only registered while something listens** (`wait`, `listen`, or the five-minute `listen` job while a draft is waiting). A button pressed with
74
+ nobody listening shows Slack's "not responding" notice and the draft stays waiting; press it again once `hz` listens.
75
+ The message says nothing false in the meantime: it still shows the buttons because it is still unanswered.
76
+
77
+ ## What approval means here
78
+
79
+ Approval follows a typed code path (T3). `DraftStanding.approves(draft)` checks an approved state and the exact
80
+ draft digest. A host consuming `ChannelApprovalReply` must separately validate the request, actor, argument digest
81
+ and expiry before acting. The plugin records a decision only if all of this held:
82
+
83
+ | Held at click time | How |
84
+ | --- | --- |
85
+ | It is a **button** press on our message | Replies, reactions and slash commands are not even subscribed to, and any such payload is refused |
86
+ | The person is **you** | The clicker's Slack ID equals the owner recorded *before* posting; a project `hz.toml` cannot set it |
87
+ | Right **workspace, conversation and message** | Compared with what was recorded when the message was posted |
88
+ | The **exact bytes** | The button carries the host argument digest; the stored preview is hashed again under the lock; `DraftStanding.approves(draft)` hashes what you pass |
89
+ | Still **fresh** | Pending, not expired (default 4 hours), click time plausible; replacing a draft requires an explicit host `settle(reference, "superseded")` call |
90
+ | **Once** | The move from pending to decided is a single atomic update, so a second click, a Slack retry or a replay is refused |
91
+
92
+ Everything else from Slack is data. Only identifiers are read; message text, names and replies are never parsed for
93
+ meaning. A draft containing control, zero-width or bidirectional characters is refused (it could not be reviewed as
94
+ it is), and a draft over 4,000 characters is refused (you could not read it all in Slack). The draft is shown in a
95
+ literal preformatted block, so mentions and links in it are inert. Refused clicks are recorded in the audit trail
96
+ (`approvals.sqlite3` in the state folder, readable by you only) with a code of ours, never Slack's text.
97
+
98
+ What this does **not** prove: that a human, rather than software running as you in Slack, pressed the button. It is an
99
+ application-level decision by the owner's Slack account, as in `hz` core's own confirmations. Anyone who controls
100
+ your Slack account or the machine controls your approvals.
101
+
102
+ ## Why your own app and not a browser session or the Slack CLI
103
+
104
+ | Approach | Verdict |
105
+ | --- | --- |
106
+ | **Browser session tokens** (community CLIs such as `slackcli` with `xoxc`/`xoxd`) | Acts as you, nothing to install, but it is an unofficial use of your login that Slack can break or forbid at any time, tokens die when you sign out, and it cannot receive button clicks. Approval would be a reaction or a typed reply, which cannot be bound to the exact bytes. Not a dependency for a product. |
107
+ | **The official Slack CLI** (`slack`) | Builds and runs apps on Slack's platform and manages manifests; it has no way to post a message or receive a click for another tool. Useful for creating and installing the app during development. |
108
+ | **A private Slack app** (this plugin): bot token for the Web API, Socket Mode for clicks, official `slack-sdk` | Supported by Slack, least privilege (three bot scopes, no user scope, no event subscriptions), signed identity of the clicker in every click, and no inbound address, so nothing is hosted. **Chosen.** |
109
+
110
+ Tenets weighed: P1 ("your identity over a bot identity") and ADR 0035 ("only what an ordinary employee may do, no admin-registered
111
+ apps") against T3 and P2 (approval must be bound and verifiable) and distributability. The app here is yours alone, in
112
+ your workspace, created by you, and it only ever writes to your own DM as a mailbox: it never posts as you and never to
113
+ a shared channel. Anything published under your name later is a separate step with your identity. If you cannot install an
114
+ app, the plugin reports so; it does not fall back to a browser session.
115
+
116
+ ## Ports
117
+
118
+ The plugin registers one service, `SlackChannel`, a `NotificationChannel` and an `ApprovalChannel` as the candidate
119
+ SDK defines them; core imports nothing from this plugin and names no Slack. A channel carries words and never decides:
120
+
121
+ - `delivery_target()` observes a `ChannelTarget`: authenticated workspace/bot account, exact owner DM, and
122
+ versioned owner-bound posting scope. This observation posts no message and grants no authority.
123
+ - `deliver_reviewed(notice, target)` and `post_reviewed(request, target)` require that exact target. They verify it
124
+ again at mutation using the same immutable credential selection and SDK client that will post, with retries
125
+ disabled. A changed account, DM or scope refuses without a post; legacy methods are never a host fallback.
126
+ - `deliver(notice)` is the explicit plugin operation path and posts at most once per `key`. The receipt says what
127
+ was observed: `delivered`, `refused` (Slack said no, safe to retry) or `unknown` (the answer was lost, so it may have been sent and is never retried by itself).
128
+ - `post(request)` shows the exact preview with Approve and Decline, bound to the request's `arguments_sha256` and expiry,
129
+ idempotent by `request_id`. `replies(references)` returns only clicks that passed every check above, with the owner's
130
+ workspace and member ID as the actor; the host must check that actor, digest and expiry again before it
131
+ consumes any approval.
132
+ - `settle(reference, outcome)` is an explicit host call to close the message once the host has acted, expired it or
133
+ replaced it. Posting another request never supersedes an earlier one automatically. Every message update first
134
+ verifies that current credentials address the recorded workspace and the owner's recorded DM.
135
+
136
+ Credential selection is pinned for each conditional call. Rotating credentials after the client is acquired cannot
137
+ redirect that call; it does not transactionally cancel an already pinned, correctly reviewed operation. Revocation
138
+ can make Slack refuse it, while a lost acknowledgement remains `unknown` and is never retried automatically.
139
+
140
+ For development against a different compatible core artifact, select its wheel with `CORE=<wheel>`.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ make check-fast
146
+ ```
147
+
148
+ The closed SDK subset, upstream wheel pin, license and regeneration policy are described in
149
+ [the vendoring notes](docs/VENDORING.md). Vendor-aware tooling verifies exact upstream and projected bytes before
150
+ formatting, checks or builds, and verifies the built wheel again. Only verified upstream implementation files receive
151
+ dependency treatment; first-party lint, strict typing and the 90% coverage floor remain unchanged.
152
+
153
+ The in-memory Slack in `tests/conftest.py` is a labelled fake. `tests/e2e/live_test.py` talks to a real workspace when
154
+ `SLACK_PROOF=1`; the tokens come from the environment (the official `slack run` injects them without printing them).
155
+
156
+ <!-- screenshots:start -->
157
+ ## Screenshots
158
+
159
+ <img src="docs/assets/screenshots/review-slack-notice.png" alt="Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery." width="960">
160
+
161
+ Demo data: Mira reviews the fictional Slack account, destination, scope and complete weekly-report notice before delivery.
162
+
163
+ <img src="docs/assets/screenshots/slack-notice-delivered.png" alt="Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write." width="960">
164
+
165
+ Demo data: Core reports the fictional notice delivered to the local stand-in after Mira confirms; this does not approve a captured write.
166
+
167
+ <details>
168
+ <summary>Command line</summary>
169
+
170
+ <img src="docs/assets/screenshots/slack-draft-cli.png" alt="Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks." width="960">
171
+
172
+ Demo data: the fictional weekly-report draft and printed button labels, rendered in the terminal from Slack message blocks.
173
+
174
+ <img src="docs/assets/screenshots/slack-draft-decision-cli.png" alt="Demo data: fictional clicks exercise the real decision code; Mira&#x27;s exact approval records one reply while invalid attempts are refused." width="960">
175
+
176
+ Demo data: fictional clicks exercise the real decision code; Mira&#x27;s exact approval records one reply while invalid attempts are refused.
177
+
178
+ </details>
179
+
180
+ [Open the screenshot gallery](docs/assets/screenshots/index.html).
181
+ <!-- screenshots:end -->
182
+
183
+ ### Approved document links through SlackCLI
184
+
185
+ The separate source-only `SlackReviewedLinks` companion uses an existing SlackCLI 0.18.0 browser profile.
186
+ Its workflow channel name is `slack-reviewed-links`, registered in the separate `slack.links` module record.
187
+ The existing `slack` bot channel retains its own unique ordinary notification and approval service. Select
188
+ an absolute native executable with `slack.cli_path` and the exact stored profile key with `slack.cli_profile`.
189
+ These owner settings are ignored in project configuration. It resolves the authenticated user's existing self DM
190
+ before the host asks for approval, and binds that account, destination and captured source into the reviewed target.
191
+ It sends only the exact approved Google Doc URL. Private review notices use the host's ordinary app notifications.
192
+
193
+ No bot or app token is required for this companion. File-backed browser profiles are privately pinned along with the
194
+ native binary; keychain-backed profiles and standard-token SDK profiles refuse. The standard SDK transport may retry
195
+ uncertain writes, so that mode cannot satisfy this companion's acknowledgement contract. Missing or ambiguous self
196
+ DMs refuse without creating a conversation. The exact selected CLI source and destination are checked again after
197
+ the durable delivery intent. Timeout, malformed acknowledgement and replay retain uncertainty and never resend.
198
+ The existing bot-backed notice/approval channel and its verified SDK projection remain separate.
199
+
200
+ The reviewed CLI behavior comes from [SlackCLI v0.18.0](https://github.com/shaharia-lab/slackcli/tree/v0.18.0),
201
+ including its fixed home-relative profile store, explicit workspace selector, browser-write retry policy and structured
202
+ message acknowledgement. Local contract tests use fictional profiles and provider replies. Live account delivery
203
+ and installed acceptance against the released SDK still need verification.
@@ -0,0 +1,38 @@
1
+ ## Audience
2
+
3
+ Someone considering a private Slack notification and draft-approval channel for Heinzelmännchen, who wants to understand what is reviewed, where decisions happen, and what setup requires.
4
+
5
+ ## Outcome
6
+
7
+ Show a fictional weekly-report notice at the point where the owner can inspect its exact posting account, destination, scope, and message, then show the Core receipt after confirmation. Complement that real Core flow with terminal demonstrations of the Slack draft blocks and decision checks. The receipt establishes delivery to the fixture's local stand-in, not delivery to a real Slack workspace or approval of a separate captured write.
8
+
9
+ ## Scenes
10
+
11
+ 1. **Review the notice — light UI.** Mira sees the pending Core confirmation with the fictional account, owner DM, scope, title, and full reason. The existing fixture stages this through the channel proposal and Core chat APIs (`docs/screenshot_fixture.py`, `stage_notice`).
12
+ 2. **See the result — dark UI.** After confirmation, Core reports that the channel delivered the notice to the local stand-in. This is a meaningful later state of the same flow, not a duplicate image or a claim that a draft was approved (`docs/screenshot_fixture.py`, `StandIn` and `RECEIPT`).
13
+ 3. **Inspect a draft — CLI.** The fixture's `showcase share` renders the actual posted Slack message blocks as a terminal illustration, including the fictional draft and button labels. It is not a screenshot of Slack or an interactive feature UI (`docs/screenshot_fixture.py`, `share`, `draw`).
14
+ 4. **Inspect decisions — CLI.** `showcase answers` exercises the real decision code with fictional owner, stranger, stale draft, replay, and expiry cases, and reports the host's replies. These are fixture operations, not declared production `hz slack` commands (`docs/screenshot_fixture.py`, `answers`).
15
+
16
+ ## Fictional fixtures
17
+
18
+ Use the existing `docs/screenshot_fixture.py` bootstrap and its private synthetic profile. Lotte, Mira, Jonas, the workspace and DM identifiers, and the reading-list report are fictional. The UI fixture binds a loopback Slack stand-in and stages one notice; the CLI fixture records posts in memory. No live Slack delivery or installed acceptance is established by this plan. The README explicitly says released-SDK acceptance and a new gallery review are pending (`README.md`, opening notice).
19
+
20
+ ## Source-bound UI surface inventory
21
+
22
+ - **Core feature UI:** The actual Core chat confirmation and delivery result are evidenced by the fixture's proposal operations, pending action, review terms, confirmation, and receipt (`docs/screenshot_fixture.py`, `stage_notice`). The plan uses `/`, an existing generic Core route. This is sufficient evidence for two useful feature UI states even though this plugin declares no dedicated Page or View.
23
+ - **External provider UI:** Slack itself displays the posted notice or draft and its Approve and Decline buttons. The plugin constructs message blocks and handles button decisions (`src/heinzel_slack/_blocks.py`, `src/heinzel_slack/_service.py`, `src/heinzel_slack/_decide.py`). The fixture's terminal rendering is an illustration of those blocks, not genuine Slack UI. No provider screenshot is claimed.
24
+ - **Operations and generic Core contributions:** Both declared entry points in `pyproject.toml`, `heinzel.modules:slack` and `heinzel.connected_sources:slack`, target `heinzel_slack`. Its `module` is declared in `src/heinzel_slack/_module.py`; `src/heinzel_slack/__init__.py` registers settings, services, `info`, `manifest`, `notify`, `ask`, `status`, `wait`, and `listen`, plus a scheduled listener, local/live health, optional setup, and system-info metadata. These can contribute to generic Core operation, setup, health, and information surfaces; their precise rendered screens are not supplied here and are not invented in the plan. Settings are declared in `src/heinzel_slack/_settings.py`.
25
+ - **Channel and approval surfaces:** `src/heinzel_slack/_channel.py` exposes the notification and approval channel, while Core owns the demonstrated review and confirmation. The separate `Module("links", ...)` conditionally registers `SlackReviewedLinks` only when the captured-source SDK contracts are available (`src/heinzel_slack/links/__init__.py`); its reviewed Google Doc link flow and self-DM target are described in `README.md` and `src/heinzel_slack/links/_reviewed_link.py`. This fixture does not demonstrate that flow. Its module record is described by the README, but no separate packaging entry point for it is declared in the supplied `pyproject.toml`; how Core discovers it remains unresolved from these sources.
26
+ - **Terminal and unresolved evidence:** The fixture's two `showcase` operations are documentation-only native terminal results. Production operations are declared in source, but absence of a dedicated view does not make them terminal-only. No plugin navigation or footer declaration is evident in the inspected first-party declarations; this does not rule out generic Core rendering. The manifest operation reads `src/heinzel_slack/manifest.json`, which is not present in the provided file inventory, so its actual output cannot be verified here. The README's development-status warning qualifies any existing screenshot claims.
27
+
28
+ ## Privacy pitfalls
29
+
30
+ Normal presentation fictionalization is explicitly limited: verified owning-interpreter paths become `/demo-profile/.venv`, and only the verified whole hostname detail in the genuine Doctor row becomes `demo-host`. These mappings change no SDK health status or remaining detail and establish no privacy acceptance. Unknown identities still fail, and independent review must inspect every image and transcript. Inspect account IDs, paths, timestamps, tokens, hostnames, terminal history, and any unexpected Core content; do not publish if their provenance is unknown. The local stand-in and fictional account identifiers must remain visibly distinguishable from real delivery.
31
+
32
+ ## Acceptance criteria
33
+
34
+ - Capture the existing four distinct scenes with applied light and dark appearances, using the declared bootstrap and demo data. Confirm the review contains the complete terms and the later state shows the actual Core receipt.
35
+ - Treat the two terminal outputs as labeled fixture demonstrations, never as Slack UI or production commands. Reject duplicate captures, caption-only changes, terminal output presented as feature UI, and forged HTML.
36
+ - Independently inspect every image and transcript for privacy and truthfulness, including the narrowly permitted path and hostname mappings. Unknown identities fail review.
37
+ - Verify that the local stand-in received only the intended notice and that the receipt does not imply draft approval or live Slack acceptance. Preserve any failure or changed SDK health status rather than masking it.
38
+ - The existing plan is preserved because it already demonstrates the useful Core review and delivery states plus two distinct decision-related CLI results. Its TOML structure was reviewed against the supplied schema requirements without executing code or performing a capture; runtime behavior and accessibility labels remain to be verified during explicit capture.