flow_chat 0.8.2 → 0.10.0

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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/pages.yml +43 -0
  5. data/.github/workflows/release.yml +56 -0
  6. data/.standard.yml +4 -0
  7. data/CHANGELOG.md +48 -0
  8. data/CLAUDE.md +327 -0
  9. data/CONTRIBUTING.md +134 -0
  10. data/Gemfile +1 -0
  11. data/README.md +189 -133
  12. data/Rakefile +17 -2
  13. data/SECURITY.md +42 -349
  14. data/docs/architecture.md +83 -0
  15. data/docs/async-background-processing.md +64 -0
  16. data/docs/configuration.md +110 -287
  17. data/docs/factory-pattern.md +58 -0
  18. data/docs/gateway-context-variables.md +168 -0
  19. data/docs/gateway-development.md +159 -0
  20. data/docs/getting-started.md +90 -0
  21. data/docs/instrumentation.md +95 -175
  22. data/docs/platforms/instagram.md +278 -0
  23. data/docs/platforms/messenger.md +205 -0
  24. data/docs/platforms/telegram.md +109 -0
  25. data/docs/platforms/ussd.md +78 -0
  26. data/docs/platforms/whatsapp.md +147 -0
  27. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  28. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  29. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  30. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  31. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  32. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  33. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  34. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  35. data/docs/testing.md +33 -426
  36. data/examples/custom_session_id_example.rb +119 -0
  37. data/examples/http_controller.rb +22 -20
  38. data/examples/intercom_configuration_example.rb +113 -0
  39. data/examples/intercom_controller.rb +182 -0
  40. data/examples/multi_tenant_whatsapp_controller.rb +63 -168
  41. data/examples/simulator_controller.rb +0 -1
  42. data/examples/ussd_controller.rb +88 -160
  43. data/examples/whatsapp_controller.rb +18 -17
  44. data/examples/whatsapp_media_examples.rb +27 -79
  45. data/flow_chat.gemspec +4 -0
  46. data/lib/flow_chat/app.rb +211 -0
  47. data/lib/flow_chat/async_job.rb +176 -0
  48. data/lib/flow_chat/choice_titles.rb +95 -0
  49. data/lib/flow_chat/config.rb +126 -23
  50. data/lib/flow_chat/delivery_error.rb +9 -0
  51. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  52. data/lib/flow_chat/factory.rb +94 -0
  53. data/lib/flow_chat/gateway_async_support.rb +106 -0
  54. data/lib/flow_chat/generic_async_job.rb +30 -0
  55. data/lib/flow_chat/http/configuration_error.rb +9 -0
  56. data/lib/flow_chat/http/gateway/simple.rb +104 -36
  57. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  58. data/lib/flow_chat/http/renderer.rb +3 -3
  59. data/lib/flow_chat/input.rb +86 -0
  60. data/lib/flow_chat/instagram/client.rb +32 -0
  61. data/lib/flow_chat/instagram/configuration.rb +147 -0
  62. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  63. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  64. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  65. data/lib/flow_chat/instagram/renderer.rb +23 -0
  66. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  67. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  68. data/lib/flow_chat/instrumentation.rb +182 -0
  69. data/lib/flow_chat/intercom/client.rb +161 -0
  70. data/lib/flow_chat/intercom/configuration.rb +102 -0
  71. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  72. data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
  73. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  74. data/lib/flow_chat/intercom/renderer.rb +123 -0
  75. data/lib/flow_chat/media.rb +121 -0
  76. data/lib/flow_chat/messenger/client.rb +264 -0
  77. data/lib/flow_chat/messenger/configuration.rb +103 -0
  78. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  79. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  80. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  81. data/lib/flow_chat/messenger/renderer.rb +150 -0
  82. data/lib/flow_chat/meta/challenge.rb +24 -0
  83. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  84. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  85. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  86. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  87. data/lib/flow_chat/meta/signature.rb +30 -0
  88. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  89. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  90. data/lib/flow_chat/named_configuration.rb +65 -0
  91. data/lib/flow_chat/phone_number_util.rb +37 -35
  92. data/lib/flow_chat/processor.rb +188 -0
  93. data/lib/flow_chat/prompt.rb +13 -16
  94. data/lib/flow_chat/renderers/markdown_support.rb +167 -0
  95. data/lib/flow_chat/security.rb +76 -0
  96. data/lib/flow_chat/session/middleware.rb +36 -11
  97. data/lib/flow_chat/simulator/controller.rb +31 -15
  98. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  99. data/lib/flow_chat/telegram/client.rb +283 -0
  100. data/lib/flow_chat/telegram/configuration.rb +78 -0
  101. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  102. data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
  103. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
  104. data/lib/flow_chat/telegram/renderer.rb +133 -0
  105. data/lib/flow_chat/telegram.rb +7 -0
  106. data/lib/flow_chat/text_truncator.rb +75 -0
  107. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  108. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  109. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  110. data/lib/flow_chat/ussd/renderer.rb +1 -1
  111. data/lib/flow_chat/version.rb +1 -1
  112. data/lib/flow_chat/whatsapp/client.rb +158 -20
  113. data/lib/flow_chat/whatsapp/configuration.rb +13 -52
  114. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  115. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
  116. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
  117. data/lib/flow_chat/whatsapp/renderer.rb +259 -64
  118. data/lib/flow_chat.rb +1 -1
  119. data/lib/tasks/release.rake +165 -0
  120. data/site/.nojekyll +0 -0
  121. data/site/.og-card.html +89 -0
  122. data/site/favicon.svg +6 -0
  123. data/site/index.html +209 -0
  124. data/site/og.png +0 -0
  125. metadata +132 -25
  126. data/docs/flows.md +0 -320
  127. data/docs/http-gateway-protocol.md +0 -432
  128. data/docs/images/simulator.png +0 -0
  129. data/docs/media.md +0 -153
  130. data/docs/sessions.md +0 -433
  131. data/docs/ussd-setup.md +0 -322
  132. data/docs/whatsapp-setup.md +0 -162
  133. data/examples/whatsapp_message_job.rb +0 -113
  134. data/lib/flow_chat/base_app.rb +0 -86
  135. data/lib/flow_chat/base_processor.rb +0 -146
  136. data/lib/flow_chat/http/app.rb +0 -6
  137. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  138. data/lib/flow_chat/http/processor.rb +0 -33
  139. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  140. data/lib/flow_chat/ussd/app.rb +0 -6
  141. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  142. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  143. data/lib/flow_chat/ussd/processor.rb +0 -39
  144. data/lib/flow_chat/whatsapp/app.rb +0 -29
  145. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  146. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  147. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
@@ -0,0 +1,226 @@
1
+ {
2
+ "planPath": "docs/superpowers/plans/2026-08-10-messenger-instagram.md",
3
+ "specPath": "docs/superpowers/specs/2026-08-10-messenger-instagram-design.md",
4
+ "branch": "feat/messenger-instagram",
5
+ "baseBranch": "feat/coexistence-webhooks",
6
+ "phases": [
7
+ {
8
+ "name": "Shared extractions and WhatsApp fixes",
9
+ "tasks": [
10
+ 1,
11
+ 2,
12
+ 3,
13
+ 4,
14
+ 5,
15
+ 6,
16
+ 7
17
+ ]
18
+ },
19
+ {
20
+ "name": "Messenger",
21
+ "tasks": [
22
+ 8,
23
+ 9,
24
+ 10,
25
+ 11,
26
+ 12,
27
+ 13,
28
+ 14
29
+ ]
30
+ },
31
+ {
32
+ "name": "Instagram and close out",
33
+ "tasks": [
34
+ 15,
35
+ 16,
36
+ 17,
37
+ 18,
38
+ 19,
39
+ 20,
40
+ 21
41
+ ]
42
+ }
43
+ ],
44
+ "tasks": [
45
+ {
46
+ "id": 1,
47
+ "subject": "Task 1: Extract Meta signature validation",
48
+ "status": "completed",
49
+ "description": "**Goal:** One implementation of X-Hub-Signature-256 validation, used by the WhatsApp gateway, with each platform keeping its own error class via a configuration_error_class hook.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/meta/signature_validation.rb\", \"lib/flow_chat/whatsapp/gateway/cloud_api.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"shared module provides valid_webhook_signature?\", \"skip_signature_validation short-circuits true\", \"blank app_secret raises the platform's own error class\", \"missing header returns false\", \"whatsapp gateway suite green\"], \"requiresUserVerification\": false}\n```"
50
+ },
51
+ {
52
+ "id": 2,
53
+ "subject": "Task 2: Extract Meta webhook verification",
54
+ "status": "completed",
55
+ "description": "**Goal:** One implementation of the hub.challenge exchange, plus FlowChat::Meta::GatewayIdentity, the identity seam both Meta behavior modules depend on. The drafted verification module called log_tag and platform without defining either, so it would only have worked because SignatureValidation happened to be included alongside it.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/meta/gateway_identity.rb\", \"lib/flow_chat/meta/webhook_verification.rb\", \"lib/flow_chat/meta/signature_validation.rb\", \"lib/flow_chat/whatsapp/gateway/cloud_api.rb\", \"test/unit/meta/webhook_verification_test.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"GatewayIdentity declares platform, platform_label, configuration_error_class as NotImplementedError and log_tag derived\", \"each behavior module works included alone\", \"challenge rendered on token match\", \"blank configured token still forbidden\", \"platform in WEBHOOK_VERIFIED and WEBHOOK_FAILED\", \"handle_verification is private\"], \"requiresUserVerification\": false}\n```"
56
+ },
57
+ {
58
+ "id": 3,
59
+ "subject": "Task 3: Extract the named-configuration registry",
60
+ "status": "completed",
61
+ "description": "**Goal:** One registry with per-class storage, replacing three verbatim copies. A shared @@configurations would merge every platform's registry, which is why storage is a class-level ivar.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/named_configuration.rb\", \"test/unit/named_configuration_test.rb\", \"lib/flow_chat/whatsapp/configuration.rb\", \"lib/flow_chat/telegram/configuration.rb\", \"lib/flow_chat/intercom/configuration.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"per-class storage\", \"exact not-found messages preserved for all three platforms\", \"Whatsapp overrides configuration_label to WhatsApp\"], \"requiresUserVerification\": false}\n```"
62
+ },
63
+ {
64
+ "id": 4,
65
+ "subject": "Task 4: Move IdGenerator up, make its cap configurable",
66
+ "status": "completed",
67
+ "description": "**Goal:** FlowChat::IdGenerator usable by any platform, max length passed in rather than fixed at WhatsApp's 256.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/id_generator.rb\", \"test/unit/id_generator_test.rb\", \"lib/flow_chat/whatsapp/middleware/choice_mapper.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"max_length configurable\", \"default 256 unchanged\", \"hash suffix still fits the cap\", \"FlowChat::Whatsapp::IdGenerator gone\"], \"requiresUserVerification\": false}\n```"
68
+ },
69
+ {
70
+ "id": 5,
71
+ "subject": "Task 5: Add plain-text markdown conversion",
72
+ "status": "completed",
73
+ "description": "**Goal:** to_plain_text on Renderers::MarkdownSupport, for the two platforms that support no rich text at all.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/renderers/markdown_support.rb\", \"test/unit/renderers/plain_text_support_test.rb\"], \"verifyCommand\": \"bundle exec ruby -Itest test/unit/renderers/plain_text_support_test.rb\", \"acceptanceCriteria\": [\"emphasis stripped\", \"ul bulleted and ol numbered\", \"link as text (url)\", \"entities decoded\", \"nil returns empty string\"], \"requiresUserVerification\": false}\n```"
74
+ },
75
+ {
76
+ "id": 6,
77
+ "subject": "Task 6: Fix WhatsApp lists above 10 choices",
78
+ "status": "completed",
79
+ "blockedBy": [
80
+ 4
81
+ ],
82
+ "description": "**Goal:** Stop building list payloads Meta rejects. Meta allows 10 rows for ALL sections combined, so above 10 choices fall back to a numbered body.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/whatsapp/renderer.rb\", \"lib/flow_chat/whatsapp/middleware/choice_mapper.rb\", \"test/unit/whatsapp/renderer_test.rb\", \"test/unit/whatsapp/middleware/choice_mapper_test.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"3 or fewer use buttons\", \"4 to 10 use one section\", \"11 or more use a numbered body\", \"no list exceeds 10 rows\", \"typed number resolves\", \"ids resolve before positions\"], \"requiresUserVerification\": false}\n```"
83
+ },
84
+ {
85
+ "id": 7,
86
+ "subject": "Task 7: Say who sent a WhatsApp echo",
87
+ "status": "completed",
88
+ "description": "**Goal:** Coexistence echoes report whether our app, another app, or a human in the business inbox sent the message.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/whatsapp/gateway/cloud_api.rb\", \"test/unit/whatsapp/gateway/cloud_api_test.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"echo_origin on WEBHOOK_RECEIVED for echo fields\", \"self when app_id matches\", \"other_app when different\", \"human_agent when absent\", \"non-echo fields unchanged\"], \"requiresUserVerification\": false}\n```"
89
+ },
90
+ {
91
+ "id": 8,
92
+ "subject": "Task 8: Messenger configuration",
93
+ "status": "completed",
94
+ "blockedBy": [
95
+ 3
96
+ ],
97
+ "description": "**Goal:** Credentials for Messenger, loadable from Rails credentials or env, with the platform's limits as named constants on FlowChat::Config.messenger.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/messenger/configuration.rb\", \"lib/flow_chat/config.rb\", \"test/unit/messenger/configuration_test.rb\"], \"verifyCommand\": \"bundle exec ruby -Itest test/unit/messenger/configuration_test.rb\", \"acceptanceCriteria\": [\"credentials and MESSENGER_* env loading\", \"valid? requires token, page_id, verify_token\", \"messages_url uses page_id\", \"account_id returns page_id\", \"limits exposed as constants\"], \"requiresUserVerification\": false}\n```"
98
+ },
99
+ {
100
+ "id": 9,
101
+ "subject": "Task 9: The choice ladder helper",
102
+ "status": "completed",
103
+ "blockedBy": [
104
+ 8
105
+ ],
106
+ "description": "**Goal:** One place that decides which rung renders a given choice count, so the renderer and the choice mapper cannot disagree.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/meta/choice_ladder.rb\", \"test/unit/meta/choice_ladder_test.rb\"], \"verifyCommand\": \"bundle exec ruby -Itest test/unit/meta/choice_ladder_test.rb\", \"acceptanceCriteria\": [\"none, quick_replies, carousel, numbered\", \"13 quick_replies, 14 carousel, 30 carousel, 31 numbered\", \"carousel_capacity is elements times buttons\", \"always_number covers every non-zero rung\"], \"requiresUserVerification\": false}\n```"
107
+ },
108
+ {
109
+ "id": 10,
110
+ "subject": "Task 10: Messenger renderer",
111
+ "status": "completed",
112
+ "blockedBy": [
113
+ 5,
114
+ 9
115
+ ],
116
+ "description": "**Goal:** Turn [prompt, choices, media] into a Send API payload shape, down the ladder, in plain text.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/messenger/renderer.rb\", \"test/unit/messenger/renderer_test.rb\"], \"verifyCommand\": \"bundle exec ruby -Itest test/unit/messenger/renderer_test.rb\", \"acceptanceCriteria\": [\"no choices renders text\", \"1 to 13 quick replies with 20-char titles\", \"14 to 30 carousel packing options across at most 10 elements\", \"above 30 numbered body\", \"markdown flattened\", \"media without choices renders an attachment\"], \"requiresUserVerification\": false}\n```"
117
+ },
118
+ {
119
+ "id": 11,
120
+ "subject": "Task 11: Messenger client",
121
+ "status": "completed",
122
+ "blockedBy": [
123
+ 10
124
+ ],
125
+ "description": "**Goal:** Send to the Send API, splitting text over the platform cap, reporting failures through the existing delivery hooks. Check report_api_error's real signature before wiring it.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/messenger/client.rb\", \"test/unit/messenger/client_test.rb\"], \"verifyCommand\": \"bundle exec ruby -Itest test/unit/messenger/client_test.rb\", \"acceptanceCriteria\": [\"recipient/messaging_type/message payload shape\", \"quick replies on the text message\", \"carousel as generic template attachment\", \"long text split on whitespace, last result returned\", \"non-2xx returns nil and reports API_ERROR\", \"upload_media returns attachment_id\"], \"requiresUserVerification\": false}\n```"
126
+ },
127
+ {
128
+ "id": 12,
129
+ "subject": "Task 12: The shared messaging gateway",
130
+ "status": "completed",
131
+ "blockedBy": [
132
+ 1,
133
+ 2,
134
+ 11
135
+ ],
136
+ "description": "**Goal:** The entry[].messaging[] envelope, dispatch, echo classification, receipts, and context population, implemented once. The test defines its own TestGateway subclass so it does not depend on Task 13.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/meta/messaging_gateway.rb\", \"lib/flow_chat/session/middleware.rb\", \"test/unit/meta/messaging_gateway_test.rb\", \"test/support/test_helpers.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"context values set, msisdn nil\", \"quick_reply and postback payloads become input\", \"echo never drives a flow and carries echo_origin\", \"receipts handled before the flow slot is claimed\", \"one flow per delivery\", \"wrong account rejected forbidden\", \"messenger and instagram default to user_id\"], \"requiresUserVerification\": false}\n```"
137
+ },
138
+ {
139
+ "id": 13,
140
+ "subject": "Task 13: Messenger gateway and choice mapper",
141
+ "status": "completed",
142
+ "blockedBy": [
143
+ 12,
144
+ 4
145
+ ],
146
+ "description": "**Goal:** The Messenger subclass, and the middleware that maps a reply back to the choice key the flow used. Two separate session maps, ids resolved before positions.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/messenger/gateway/send_api.rb\", \"lib/flow_chat/messenger/middleware/choice_mapper.rb\", \"test/unit/messenger/middleware/choice_mapper_test.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"six hooks overridden\", \"tapped quick reply resolves\", \"typed number resolves on the numbered rung\", \"ids beat positions\", \"no position map on tappable rungs\"], \"requiresUserVerification\": false}\n```"
147
+ },
148
+ {
149
+ "id": 14,
150
+ "subject": "Task 14: Messenger integration test",
151
+ "status": "completed",
152
+ "blockedBy": [
153
+ 13
154
+ ],
155
+ "description": "**Goal:** A full webhook-to-send cycle through a real flow, session store and middleware stack, plus the async path. Read test/integration/whatsapp_integration_test.rb first and copy its harness.\n\n```json:metadata\n{\"files\": [\"test/integration/messenger_integration_test.rb\", \"test/support/test_helpers.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"three-turn conversation\", \"session state carried across turns\", \"terminal screen sends\", \"async enqueues and returns 200\"], \"requiresUserVerification\": false}\n```"
156
+ },
157
+ {
158
+ "id": 15,
159
+ "subject": "Task 15: Instagram configuration",
160
+ "status": "completed",
161
+ "blockedBy": [
162
+ 3
163
+ ],
164
+ "description": "**Goal:** Instagram credentials on the Facebook Login path, with Instagram's own limits. account_id returns page_id, since that path keys webhooks on the linked page.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/instagram/configuration.rb\", \"lib/flow_chat/config.rb\", \"test/unit/instagram/configuration_test.rb\"], \"verifyCommand\": \"bundle exec ruby -Itest test/unit/instagram/configuration_test.rb\", \"acceptanceCriteria\": [\"instagram_account_id alongside page_id\", \"INSTAGRAM_* env fallback\", \"max_text_length 1000\", \"account_id is the linked page\"], \"requiresUserVerification\": false}\n```"
165
+ },
166
+ {
167
+ "id": 16,
168
+ "subject": "Task 16: Instagram renderer, client, gateway, choice mapper",
169
+ "status": "completed",
170
+ "blockedBy": [
171
+ 13,
172
+ 15
173
+ ],
174
+ "description": "**Goal:** The Instagram platform. Differs from Messenger in three ways: the body is always numbered (both interactive surfaces are mobile-only), text is measured in bytes, and limits come from Config.instagram. The gateway is a SIBLING of Messenger's, not a subclass; the renderer IS a subclass, deliberately.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/instagram/renderer.rb\", \"lib/flow_chat/instagram/client.rb\", \"lib/flow_chat/instagram/gateway/send_api.rb\", \"lib/flow_chat/instagram/middleware/choice_mapper.rb\", \"test/unit/instagram/renderer_test.rb\", \"test/unit/instagram/client_test.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"quick replies and carousels both number the body\", \"no choices means no numbers\", \"text split by bytes\", \"position map always stored\", \"gateway subclasses Meta::MessagingGateway directly\"], \"requiresUserVerification\": false}\n```"
175
+ },
176
+ {
177
+ "id": 17,
178
+ "subject": "Task 17: Instagram integration test",
179
+ "status": "completed",
180
+ "blockedBy": [
181
+ 16,
182
+ 14
183
+ ],
184
+ "description": "**Goal:** A full webhook-to-send cycle on Instagram, including a typed-number reply proving the desktop path works.\n\n```json:metadata\n{\"files\": [\"test/integration/instagram_integration_test.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"tapped quick reply advances the flow\", \"typed number advances the flow\", \"numbered options present in the body\", \"wrong webhook object ignored with 200\"], \"requiresUserVerification\": false}\n```"
185
+ },
186
+ {
187
+ "id": 18,
188
+ "subject": "Task 18: Simulator support",
189
+ "status": "completed",
190
+ "blockedBy": [
191
+ 13,
192
+ 16
193
+ ],
194
+ "description": "**Goal:** Both platforms selectable in the simulator. The view branches on processor_type in embedded JS at six points and has per-platform screen chrome, so this replaces those comparisons with one isChatPlatform predicate rather than adding two more copies.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/simulator/controller.rb\", \"lib/flow_chat/simulator/views/simulator.html.erb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"five platforms in the selector\", \"correct messaging envelope per platform\", \"both use the chat bubble screen\", \"one predicate replaces six comparisons\", \"cookie gate intact\"], \"requiresUserVerification\": false}\n```"
195
+ },
196
+ {
197
+ "id": 19,
198
+ "subject": "Task 19: Documentation",
199
+ "status": "completed",
200
+ "blockedBy": [
201
+ 16
202
+ ],
203
+ "description": "**Goal:** Both platforms documented to the standard of the existing platform guides. Dense plain register, real limits, no marketing adjectives, no em-dashes.\n\n```json:metadata\n{\"files\": [\"docs/platforms/messenger.md\", \"docs/platforms/instagram.md\", \"README.md\", \"docs/gateway-context-variables.md\"], \"verifyCommand\": \"grep -c '\\u2014' docs/platforms/messenger.md docs/platforms/instagram.md\", \"acceptanceCriteria\": [\"two platform guides covering setup through limits\", \"Instagram desktop caveat stated plainly\", \"README platform and differences tables updated\", \"context variables documented including nil msisdn\", \"no em-dashes\"], \"requiresUserVerification\": false}\n```"
204
+ },
205
+ {
206
+ "id": 20,
207
+ "subject": "Task 20: Verify the two unresolvable facts with the user",
208
+ "status": "pending",
209
+ "blockedBy": [
210
+ 17
211
+ ],
212
+ "description": "**Goal:** Close the two gaps that cannot be settled from this machine: which webhook object Instagram messaging arrives under, and whether the Instagram carousel is legible for a plain option menu.\n\n**User Verification Required:**\nBefore marking this task complete, you MUST call AskUserQuestion:\n```yaml\nAskUserQuestion:\n question: \"Two things I cannot check from here. In your Meta app dashboard, which webhook object are Instagram messaging events subscribed under, and does the Instagram carousel look acceptable for a plain list of options on a real device?\"\n header: \"Verification\"\n options:\n - label: \"object is instagram, carousel is fine\"\n description: \"Keep expected_webhook_object as instagram and keep the carousel rung\"\n - label: \"object is page, carousel is fine\"\n description: \"Change expected_webhook_object to page and keep the carousel rung\"\n - label: \"object is instagram, carousel reads badly\"\n description: \"Keep the object and drop the carousel rung\"\n - label: \"object is page, carousel reads badly\"\n description: \"Change the object to page and drop the carousel rung\"\n```\n\n**If the user selects an option indicating rework:** apply the change, re-run the suite, and re-verify with AskUserQuestion again. Do NOT guess either answer.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/instagram/gateway/send_api.rb\", \"lib/flow_chat/instagram/renderer.rb\", \"docs/superpowers/specs/2026-08-10-messenger-instagram-design.md\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"webhook object confirmed against the dashboard\", \"carousel decision confirmed on a device\", \"spec open items 2 and 3 resolved\", \"suite green after any change\"], \"requiresUserVerification\": true, \"userVerificationPrompt\": \"In your Meta app dashboard, which webhook object are Instagram messaging events subscribed under, and does the Instagram carousel look acceptable for a plain list of options on a real device?\"}\n```",
213
+ "note": "Asked. User has not set up the Instagram webhook yet and has not checked the carousel on a device. Both recorded as open in the spec. Webhook object must be confirmed before Instagram goes live; carousel is cosmetic only."
214
+ },
215
+ {
216
+ "id": 21,
217
+ "subject": "Task 21: Confirm the Messenger text cap and close out",
218
+ "status": "completed",
219
+ "blockedBy": [
220
+ 11
221
+ ],
222
+ "description": "**Goal:** Replace the one assumed constant with a verified one, and confirm the whole branch is green. 2000 was assumed because Meta's Send API reference page would not render during design.\n\n```json:metadata\n{\"files\": [\"lib/flow_chat/config.rb\", \"docs/superpowers/specs/2026-08-10-messenger-instagram-design.md\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"text cap verified against Meta docs\", \"client test fixture still crosses the boundary\", \"spec open item 1 resolved with its source\", \"suite and linter green\", \"branch diff reviewed\"], \"requiresUserVerification\": false}\n```"
223
+ }
224
+ ],
225
+ "lastUpdated": "2026-08-10T17:00:00Z"
226
+ }