@volter/twin-telegram 1.0.0 → 1.0.1

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 (52) hide show
  1. package/README.md +2 -2
  2. package/dist/journeys/customer-life.json +21 -21
  3. package/dist/journeys/customer-life.outline.md +17 -17
  4. package/dist/journeys/customer-life.outline.verdict.json +17 -52
  5. package/dist/journeys/customer-life.verdict.json +24 -98
  6. package/dist/journeys/decisions.json +49 -13
  7. package/dist/journeys/demand.json +0 -91
  8. package/dist/journeys/first-use.json +21 -0
  9. package/dist/journeys/unreachable.json +1 -1
  10. package/dist/journeys/vendor-examples.json +14 -6
  11. package/dist/spec/SOURCE.md +5 -6
  12. package/dist/spec/patches.json +20 -20
  13. package/dist/spec/sources.json +15 -0
  14. package/dist/src/cli.js +1 -1
  15. package/dist/src/engine/media.d.ts +6 -12
  16. package/dist/src/engine/media.js +28 -66
  17. package/dist/src/engine/text.d.ts +0 -7
  18. package/dist/src/engine/text.js +2 -2
  19. package/dist/src/index.js +2 -1
  20. package/dist/src/manifest.js +7 -6
  21. package/dist/src/semantics/available-methods.js +50 -40
  22. package/dist/src/semantics/doors.d.ts +0 -1
  23. package/dist/src/semantics/doors.js +82 -5
  24. package/dist/src/semantics/shared.d.ts +10 -6
  25. package/dist/src/semantics/shared.js +29 -8
  26. package/dist/src/server.js +2 -19
  27. package/generated/pack-facts.json +10 -0
  28. package/journeys/customer-life.json +21 -21
  29. package/journeys/customer-life.outline.md +17 -17
  30. package/journeys/customer-life.outline.verdict.json +17 -52
  31. package/journeys/customer-life.verdict.json +24 -98
  32. package/journeys/decisions.json +49 -13
  33. package/journeys/demand.json +0 -91
  34. package/journeys/first-use.json +21 -0
  35. package/journeys/unreachable.json +1 -1
  36. package/journeys/vendor-examples.json +14 -6
  37. package/package.json +12 -9
  38. package/spec/SOURCE.md +5 -6
  39. package/spec/patches.json +20 -20
  40. package/spec/sources.json +15 -0
  41. package/src/cli.ts +1 -1
  42. package/src/engine/media.ts +21 -58
  43. package/src/engine/text.ts +2 -2
  44. package/src/index.ts +2 -1
  45. package/src/manifest.ts +7 -6
  46. package/src/semantics/available-methods.ts +55 -41
  47. package/src/semantics/doors.ts +74 -8
  48. package/src/semantics/shared.ts +27 -8
  49. package/src/server.ts +2 -11
  50. package/dist/generated/pack-facts.json +0 -53
  51. package/dist/spec/page-to-openapi.ts +0 -258
  52. package/spec/page-to-openapi.ts +0 -258
@@ -37,7 +37,7 @@ export function jsonParam(ctx, name) {
37
37
  }
38
38
  }
39
39
  /** The vendor's own fields of a stored row: the kernel's bookkeeping (`_` fields, a tombstone) left out. */
40
- export function vendorFields(ctx, row) {
40
+ function vendorFields(ctx, row) {
41
41
  return Object.fromEntries(Object.entries(ctx.own(row)).filter(([k]) => !k.startsWith('_') && k !== 'deleted'));
42
42
  }
43
43
  /** The methods this pack serves, as the page names them: a request naming one in another case is routed to it. */
@@ -88,7 +88,7 @@ export function userView(ctx, id) {
88
88
  return Object.fromEntries(Object.entries(v).filter(([k]) => ['id', 'is_bot', 'first_name', 'last_name', 'username', 'language_code', 'is_premium'].includes(k)));
89
89
  }
90
90
  /** A Chat as a message names it: its id, type and names. */
91
- export function chatView(ctx, id) {
91
+ function chatView(ctx, id) {
92
92
  const row = ctx.row('chat', String(id));
93
93
  const v = row ? vendorFields(ctx, row) : { id: Number(id), type: 'private' };
94
94
  return Object.fromEntries(Object.entries(v).filter(([k]) => ['id', 'type', 'title', 'username', 'first_name', 'last_name', 'is_forum', 'is_direct_messages'].includes(k)));
@@ -134,7 +134,7 @@ export function messageView(ctx, row, nested = false) {
134
134
  return out;
135
135
  }
136
136
  /** A ChatFullInfo as getChat answers it, from a chat as chatOf reads it (its own id and type already restored). */
137
- export function chatFullView(_ctx, chat) {
137
+ export function chatFullView(chat) {
138
138
  return Object.fromEntries(Object.entries(chat).filter(([k]) => !k.startsWith('_') && k !== 'deleted'));
139
139
  }
140
140
  // ── a chat's access ─────────────────────────────────────────────────────────────────────────────────────────────
@@ -212,12 +212,15 @@ export function formatted(ctx, text, parseMode, entitiesParam) {
212
212
  if (new TextEncoder().encode(text).length > 1 << 15)
213
213
  return { refusal: refuse(ctx, 400, 'Bad Request: text is too long') };
214
214
  const mode = typeof parseMode === 'string' ? parseMode.toLowerCase() : '';
215
+ // source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "Unsupported parse_mode"
216
+ if (!['', 'none', 'html', 'markdownv2', 'markdown'].includes(mode))
217
+ return { refusal: refuse(ctx, 400, 'Bad Request: unsupported parse_mode') };
215
218
  let parsed;
216
219
  if (mode === '' || mode === 'none') {
217
220
  const given = Array.isArray(entitiesParam) ? entitiesParam : [];
218
221
  parsed = { text, entities: given };
219
222
  }
220
- else if (mode === 'html' || mode === 'markdownv2' || mode === 'markdown') {
223
+ else {
221
224
  // source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "text_parse_mode = make_object<td_api::textParseModeMarkdown>(2);"
222
225
  // source: https://core.telegram.org/bots/api#markdownv2-style "To use this mode, pass MarkdownV2 in the parse_mode field."
223
226
  // source: https://core.telegram.org/bots/api#markdown-style "This is a legacy mode, retained for backward compatibility."
@@ -229,10 +232,6 @@ export function formatted(ctx, text, parseMode, entitiesParam) {
229
232
  return { refusal: refuse(ctx, 400, `Bad Request: can't parse entities: ${r.error}`) };
230
233
  parsed = r;
231
234
  }
232
- else {
233
- // source: https://raw.githubusercontent.com/tdlib/telegram-bot-api/e3e9dd8e5b3d7ab8537cd5a10dc31d5ffa8f82d1/telegram-bot-api/Client.cpp "Unsupported parse_mode"
234
- return { refusal: refuse(ctx, 400, 'Bad Request: unsupported parse_mode') };
235
- }
236
235
  // a bot's text gets the entities the server finds (TDLib's fix_formatted_text: a bot finds new entities)
237
236
  // source: https://raw.githubusercontent.com/tdlib/td/42e6a5259551178d1dab54a22ad96d14bd906e20/td/telegram/MessageEntity.cpp "add_entities(MessageEntity::Type::Mention, find_mentions);"
238
237
  const done = finish(parsed, { find: true, skipBotCommands: false });
@@ -330,6 +329,28 @@ export async function storeFile(ctx, bytes, meta, operation) {
330
329
  // the row as kept, its own `_` fields with it (the create answers the vendor's view)
331
330
  return ctx.row('file', id);
332
331
  }
332
+ /** The smaller sizes Telegram makes of an uploaded photo, smallest first: each size type whose box is smaller than the
333
+ * photo's longer side, the photo scaled to fit it (the rounding is the twin's). A thumbnail is kept by its ids and
334
+ * dimensions only: the twin renders no thumbnail's pixels, so its file is a gap. */
335
+ // source: https://core.telegram.org/api/files "s - Server-side resized image, bounded by 100x100 pixels."
336
+ // source: https://core.telegram.org/api/files "w - Server-side resized image, bounded by 2560x2560 pixels."
337
+ // source: https://raw.githubusercontent.com/tdlib/td/42e6a5259551178d1dab54a22ad96d14bd906e20/td/telegram/PhotoSize.cpp "get_photo_sizes_object"
338
+ export async function photoThumbnails(ctx, original, width, height, operation) {
339
+ const longer = Math.max(width, height), out = [];
340
+ // a photo's sizes are made once: the same photo sent again (by its file_id) answers the sizes it already has
341
+ // source: https://core.telegram.org/bots/api#file "Unique identifier for this file, which is supposed to be the same over time and for different bots."
342
+ const made = ctx.rowsRaw('file').filter((r) => r.deleted !== true && r._thumbnail_of === original.file_unique_id && typeof r._width === 'number');
343
+ if (made.length)
344
+ return made.map((r) => ({ file_id: r.file_id, file_unique_id: r.file_unique_id, width: r._width, height: r._height })).sort((x, y) => x.width - y.width);
345
+ for (const box of [100, 320, 800, 1280, 2560]) {
346
+ if (box >= longer)
347
+ break;
348
+ const scale = box / longer, w = Math.max(1, Math.round(width * scale)), h = Math.max(1, Math.round(height * scale));
349
+ const row = await ctx.create('file', (id) => ({ file_id: `AgACAgIAAxkBAA${id.slice(4)}`, file_unique_id: id, _kind: 'photos', _thumbnail_of: original.file_unique_id, _width: w, _height: h, ...(original._bot !== undefined ? { _bot: original._bot } : {}) }), operation);
350
+ out.push({ file_id: row.file_id, file_unique_id: row.file_unique_id, width: w, height: h });
351
+ }
352
+ return out;
353
+ }
333
354
  /** A file by its file_id, as one bot holds it: a file a bot uploaded is that bot's alone (a chat's photo is every
334
355
  * bot's that reads the chat). */
335
356
  // source: https://core.telegram.org/bots/api#sending-files "file_id is unique for each individual bot and can't be transferred from one bot to another."
@@ -1,24 +1,7 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
1
  // FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
10
- import { grpc, serveHttp } from '@volter/world-core';
2
+ import { serveHttp } from '@volter/world-core';
11
3
  import { createTelegramFetch } from "./fetch.js";
12
- import { manifest } from "./manifest.js";
13
4
  export async function createTelegramTwinServer(options = {}) {
14
- const fetch = createTelegramFetch(options);
15
- // a vendor that speaks gRPC (its manifest's `grpc`): HTTP/2 and HTTP/1 on one port, each rpc through the fetch
16
- if ('grpc' in manifest && manifest.grpc) {
17
- const schemaPath = './generated/proto.gen.json';
18
- const schema = (await import(__rewriteRelativeImportExtension(schemaPath), { with: { type: 'json' } })).default;
19
- return grpc.serveGrpc({ ...(options.port ? { port: options.port } : {}), fetch: async (r) => fetch(r), schema });
20
- }
21
- // the vendor's sockets, when its manifest declares any (the kernel's sockets.ts)
22
- const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch });
5
+ const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch: createTelegramFetch(options) });
23
6
  return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
24
7
  }
@@ -37,6 +37,16 @@
37
37
  "note": "the Chat SDK's Telegram adapter reads it as its API base (@chat-adapter/telegram 4.39.0, dist/index.js:1039); node-telegram-bot-api takes a baseApiUrl option only, and reaches the twin by its host"
38
38
  },
39
39
  "credentialDoor": {
40
+ "account": {
41
+ "field": "account",
42
+ "tenancy": "Telegram person",
43
+ "source": "https://core.telegram.org/bots/features#botfather"
44
+ },
45
+ "deployment": {
46
+ "field": "deployment",
47
+ "object": "Bot",
48
+ "source": "https://core.telegram.org/bots/features#botfather"
49
+ },
40
50
  "path": "/_twin/bots",
41
51
  "body": {},
42
52
  "fill": {
@@ -2,7 +2,7 @@
2
2
  "vendor": "telegram",
3
3
  "author": "Contributor 173",
4
4
  "host": "api.telegram.org",
5
- "about": "Tide Labs, a two-person studio, runs its Volter Harness agent on Telegram (a bot polling for updates, answering in private and in the team group, where privacy mode keeps a plain member from hearing the team until it is made an administrator, posting a scheduled digest, and sending a finished card's release notes as a document) and publishes its news to a Telegram channel through Postiz (connected by a /connect post, then text, an album, a reply, a photo, a video and a PDF), from January to the agent's retirement at the end of February.",
5
+ "about": "Tide Labs, a two-person studio, runs its Volter Harness agent on Telegram (a bot polling for updates, answering in private and in the team group, where privacy mode keeps a plain member from hearing the team until it is made an administrator, posting a scheduled digest, and sending a finished card's release notes as a document) and publishes its news to a Telegram channel through Scheduler (connected by a /connect post, then text, an album, a reply, a photo, a video and a PDF), from January to the agent's retirement at the end of February.",
6
6
  "steps": [
7
7
  {
8
8
  "name": "Monday 5 January, 10:00 in Lisbon",
@@ -530,7 +530,7 @@
530
530
  ]
531
531
  },
532
532
  {
533
- "name": "Ana makes the news bot with BotFather for Postiz, which she runs herself (docker compose, local storage)",
533
+ "name": "Ana makes the news bot with BotFather for Scheduler, which she runs herself (docker compose, local storage)",
534
534
  "method": "POST",
535
535
  "path": "/_twin/bots",
536
536
  "body": {
@@ -571,11 +571,11 @@
571
571
  ]
572
572
  },
573
573
  {
574
- "name": "14:06: Ana sets her Postiz's TELEGRAM_TOKEN to the news bot's token and, in Postiz, chooses to add Telegram",
574
+ "name": "14:06: Ana sets her Scheduler's TELEGRAM_TOKEN to the news bot's token and, in Scheduler, chooses to add Telegram",
575
575
  "wait": "1m"
576
576
  },
577
577
  {
578
- "name": "Postiz's connect screen polls the news bot: only the bot's own promotion is waiting, and it holds no /connect",
578
+ "name": "Scheduler's connect screen polls the news bot: only the bot's own promotion is waiting, and it holds no /connect",
579
579
  "method": "POST",
580
580
  "path": "/bot{news_token}/getUpdates",
581
581
  "headers": {
@@ -602,7 +602,7 @@
602
602
  ]
603
603
  },
604
604
  {
605
- "name": "Ana posts the command Postiz shows her in the channel",
605
+ "name": "Ana posts the command Scheduler shows her in the channel",
606
606
  "method": "POST",
607
607
  "path": "/_twin/messages",
608
608
  "body": {
@@ -613,7 +613,7 @@
613
613
  "status": 200
614
614
  },
615
615
  {
616
- "name": "Postiz polls again from the next update (the last update_id it saw plus one) and finds the /connect post",
616
+ "name": "Scheduler polls again from the next update (the last update_id it saw plus one) and finds the /connect post",
617
617
  "method": "POST",
618
618
  "path": "/bot{news_token}/getUpdates",
619
619
  "headers": {
@@ -637,7 +637,7 @@
637
637
  ]
638
638
  },
639
639
  {
640
- "name": "Postiz reads its bot",
640
+ "name": "Scheduler reads its bot",
641
641
  "method": "POST",
642
642
  "path": "/bot{news_token}/getMe",
643
643
  "headers": {
@@ -709,7 +709,7 @@
709
709
  "wait": "10s"
710
710
  },
711
711
  {
712
- "name": "Postiz deletes its notice",
712
+ "name": "Scheduler deletes its notice",
713
713
  "method": "POST",
714
714
  "path": "/bot{news_token}/deleteMessage",
715
715
  "headers": {
@@ -725,7 +725,7 @@
725
725
  ]
726
726
  },
727
727
  {
728
- "name": "Postiz reads the channel it is connecting",
728
+ "name": "Scheduler reads the channel it is connecting",
729
729
  "method": "POST",
730
730
  "path": "/bot{news_token}/getChat",
731
731
  "headers": {
@@ -783,7 +783,7 @@
783
783
  "wait": "71630s"
784
784
  },
785
785
  {
786
- "name": "Postiz publishes the launch post (Postiz's HTML: a bold title, two paragraphs)",
786
+ "name": "Scheduler publishes the launch post (Scheduler's HTML: a bold title, two paragraphs)",
787
787
  "method": "POST",
788
788
  "path": "/bot{news_token}/sendMessage",
789
789
  "headers": {
@@ -944,7 +944,7 @@
944
944
  "wait": "67h"
945
945
  },
946
946
  {
947
- "name": "Postiz publishes the 0.9 release with three screenshots as one album, the text on the first",
947
+ "name": "Scheduler publishes the 0.9 release with three screenshots as one album, the text on the first",
948
948
  "method": "POST",
949
949
  "path": "/bot{news_token}/sendMediaGroup?chat_id={channel}&media=%5B%7B%22type%22%3A%22photo%22%2C%22media%22%3A%22attach%3A%2F%2F0%22%2C%22caption%22%3A%22%3Cb%3ETide%200.9%3C%2Fb%3E%5CnA%20dashboard%20for%20every%20release%2C%20a%20timeline%20you%20can%20drag%2C%20and%20settings%20in%20one%20place.%22%2C%22parse_mode%22%3A%22HTML%22%7D%2C%7B%22type%22%3A%22photo%22%2C%22media%22%3A%22attach%3A%2F%2F1%22%2C%22parse_mode%22%3A%22HTML%22%7D%2C%7B%22type%22%3A%22photo%22%2C%22media%22%3A%22attach%3A%2F%2F2%22%2C%22parse_mode%22%3A%22HTML%22%7D%5D",
950
950
  "form": {
@@ -979,11 +979,11 @@
979
979
  "equals": "Tide 0.9\nA dashboard for every release, a timeline you can drag, and settings in one place."
980
980
  },
981
981
  {
982
- "path": "result.0.photo.0.width",
982
+ "path": "result.0.photo.2.width",
983
983
  "equals": 480
984
984
  },
985
985
  {
986
- "path": "result.2.photo.0.height",
986
+ "path": "result.2.photo.2.height",
987
987
  "equals": 300
988
988
  },
989
989
  {
@@ -1001,13 +1001,13 @@
1001
1001
  "wait": "1h"
1002
1002
  },
1003
1003
  {
1004
- "name": "Postiz publishes the post's comment, the changelog, as a reply to the album",
1004
+ "name": "Scheduler publishes the post's comment, the changelog, as a reply to the album",
1005
1005
  "method": "POST",
1006
1006
  "path": "/bot{news_token}/sendMessage",
1007
1007
  "headers": {
1008
1008
  "content-type": "application/x-www-form-urlencoded"
1009
1009
  },
1010
- "body": "chat_id={channel}&text=Changelog%3A%20drag%20to%20reschedule%20a%20release%3B%20faster%20dashboard%3B%20fixed%20the%20login%20redirect.%0A&parse_mode=HTML&reply_to_message_id={album}",
1010
+ "body": "chat_id={channel}&text=Changelog%3A%20drag%20to%20reschedule%20a%20release%3B%20faster%20dashboard%3B%20fixed%20the%20login%20redirect.%0A&parse_mode=HTML&reply_parameters=%7B%22message_id%22%3A{album}%7D",
1011
1011
  "status": 200,
1012
1012
  "expect": [
1013
1013
  {
@@ -1062,7 +1062,7 @@
1062
1062
  "wait": "1845m"
1063
1063
  },
1064
1064
  {
1065
- "name": "Postiz sends Ana's product photo with its 1,091-character story, and Telegram refuses the caption",
1065
+ "name": "Scheduler sends Ana's product photo with its 1,091-character story, and Telegram refuses the caption",
1066
1066
  "method": "POST",
1067
1067
  "path": "/bot{news_token}/sendPhoto?chat_id={channel}&caption=Tide%20Desk%20started%20as%20a%20sketch%20on%20a%20caf%C3%A9%20napkin%20in%20Porto%2C%20the%20week%20our%20second%20release%20slipped%20for%20the%20third%20time.%20We%20were%20two%20people%20with%20a%20shared%20spreadsheet%2C%20a%20calendar%20nobody%20trusted%20and%20a%20habit%20of%20shipping%20on%20Fridays%20at%20midnight.%20Ana%20drew%20three%20columns%3A%20what%20is%20ready%2C%20what%20is%20waiting%2C%20what%20is%20promised.%20Bruno%20added%20a%20fourth%3A%20who%20is%20waiting%20for%20it.%20That%20napkin%20became%20the%20first%20board%2C%20and%20the%20board%20became%20the%20planner%20we%20use%20every%20Monday.%20We%20kept%20the%20rule%20from%20that%20night%3A%20nothing%20goes%20on%20the%20timeline%20unless%20someone%20can%20say%20who%20is%20waiting%20for%20it.%20It%20sounds%20small%2C%20but%20it%20changed%20how%20we%20talk%20about%20releases.%20We%20stopped%20arguing%20about%20dates%20and%20started%20asking%20who%20would%20notice.%20The%20desk%20in%20the%20photo%20is%20where%20it%20all%20happens%20now%3A%20one%20screen%20for%20the%20timeline%2C%20one%20for%20the%20changelog%2C%20and%20a%20cup%20that%20says%20Friday.%20Today%20Tide%20plans%20the%20weekly%20releases%20of%20forty%20small%20teams%2C%20from%20a%20bakery%27s%20ordering%20app%20in%20Braga%20to%20a%20school%20timetable%20in%20Coimbra.%20If%20your%20team%20still%20keeps%20its%20releases%20in%20a%20spreadsheet%2C%20we%20were%20you%20two%20years%20ago.%20Come%20and%20try%20the%20board%3B%20the%20napkin%20is%20framed%20on%20the%20wall%20behind%20it.&parse_mode=HTML",
1068
1068
  "form": {
@@ -1092,7 +1092,7 @@
1092
1092
  "wait": "20m"
1093
1093
  },
1094
1094
  {
1095
- "name": "Postiz sends the photo again with the short caption",
1095
+ "name": "Scheduler sends the photo again with the short caption",
1096
1096
  "method": "POST",
1097
1097
  "path": "/bot{news_token}/sendPhoto?chat_id={channel}&caption=Tide%20Desk%20started%20as%20a%20sketch%20on%20a%20caf%C3%A9%20napkin%20in%20Porto%2C%20the%20week%20our%20second%20release%20slipped%20for%20the%20third%20time.%20That%20napkin%20became%20the%20first%20board%2C%20and%20the%20board%20became%20the%20planner%20we%20use%20every%20Monday.%20Today%20Tide%20plans%20the%20weekly%20releases%20of%20forty%20small%20teams.&parse_mode=HTML",
1098
1098
  "form": {
@@ -1108,11 +1108,11 @@
1108
1108
  "status": 200,
1109
1109
  "expect": [
1110
1110
  {
1111
- "path": "result.photo.0.width",
1111
+ "path": "result.photo.2.width",
1112
1112
  "equals": 640
1113
1113
  },
1114
1114
  {
1115
- "path": "result.photo.0.height",
1115
+ "path": "result.photo.2.height",
1116
1116
  "equals": 480
1117
1117
  },
1118
1118
  {
@@ -1126,7 +1126,7 @@
1126
1126
  "wait": "6880m"
1127
1127
  },
1128
1128
  {
1129
- "name": "Postiz publishes the product demo video",
1129
+ "name": "Scheduler publishes the product demo video",
1130
1130
  "method": "POST",
1131
1131
  "path": "/bot{news_token}/sendVideo?chat_id={channel}&caption=Three%20seconds%20of%20Tide%200.9%3A%20a%20release%20dragged%20to%20Friday.&parse_mode=HTML",
1132
1132
  "form": {
@@ -1164,7 +1164,7 @@
1164
1164
  "wait": "4h"
1165
1165
  },
1166
1166
  {
1167
- "name": "Postiz publishes the white paper as a PDF",
1167
+ "name": "Scheduler publishes the white paper as a PDF",
1168
1168
  "method": "POST",
1169
1169
  "path": "/bot{news_token}/sendDocument?chat_id={channel}&caption=How%20a%20two-person%20team%20ships%20every%20week%3A%20our%20notes%2C%20as%20a%20PDF.&parse_mode=HTML",
1170
1170
  "form": {
@@ -2,8 +2,8 @@
2
2
 
3
3
  **The customer.** Tide Labs, a two-person software studio in Lisbon. Ana Ribeiro, its founder, runs an agent on Volter
4
4
  Harness that she and the team talk to on Telegram, and publishes the studio's news to its Telegram channel with
5
- Postiz. Bruno Costa, its engineer, is on the team chat. The story runs from Monday 5 January to Friday 27 February
6
- 2026: two bots made, the agent at work in a private chat and a group, a news channel connected to Postiz and posted to
5
+ Scheduler. Bruno Costa, its engineer, is on the team chat. The story runs from Monday 5 January to Friday 27 February
6
+ 2026: two bots made, the agent at work in a private chat and a group, a news channel connected to Scheduler and posted to
7
7
  for six weeks, the agent's group retired and, at the end, the agent's token revoked. The channel and its bot stay.
8
8
 
9
9
  ## Who acts, and with what
@@ -21,8 +21,8 @@ for six weeks, the agent's group retired and, at the end, the agent's token revo
21
21
  Harness's board, where the agent files work as cards, tells a chat subscribed to a card when it is done, the card's
22
22
  files attached; the Chat SDK sends a message with files as MarkdownV2 (supercode board/dispatch.mjs notifyChats,
23
23
  @chat-adapter/telegram's resolveParseMode).
24
- - **The news bot**, @tidelabs_news_bot, token N: the bot Postiz posts with. Postiz's Telegram integration calls with
25
- the one bot its deployment configures, its TELEGRAM_TOKEN (postiz-app telegram.provider.ts), so Ana runs Postiz
24
+ - **The news bot**, @tidelabs_news_bot, token N: the bot Scheduler posts with. Scheduler's Telegram integration calls with
25
+ the one bot its deployment configures, its TELEGRAM_TOKEN, so Ana runs Scheduler
26
26
  herself (docker compose, local storage) and sets its TELEGRAM_TOKEN to token N. She makes the bot an administrator of
27
27
  the channel allowed to post and to delete messages.
28
28
 
@@ -54,17 +54,17 @@ is a member; a bot deletes another's message in a channel only with the right to
54
54
  exactly those items to the home chat (sendMessage to the group).
55
55
  5. **Wednesday 7 January, 14:00. The news channel.** Ana makes the public channel "Tide Labs News" (@tidelabs_news)
56
56
  with the studio's logo as its photo (door). She makes the news bot with BotFather (token N) and adds it to the
57
- channel as an administrator that may post and delete messages (door). In the Postiz she runs herself (docker compose,
58
- local storage) she sets TELEGRAM_TOKEN to token N, and then chooses to add Telegram; Postiz
59
- shows the command to post, "/connect" and a word, and she posts it in the channel (door). Postiz's connect screen
57
+ channel as an administrator that may post and delete messages (door). In the Scheduler she runs herself (docker compose,
58
+ local storage) she sets TELEGRAM_TOKEN to token N, and then chooses to add Telegram; Scheduler
59
+ shows the command to post, "/connect" and a word, and she posts it in the channel (door). Scheduler's connect screen
60
60
  polls (getUpdates, its library sending allowed_updates as the fields allowed_updates[0]=message and
61
61
  allowed_updates[1]=channel_post, which the Bot API does not read as its JSON-serialized list, so every update type
62
- comes back): the bot's own membership change and her post come back. Postiz reads its bot (getMe), checks the bot
62
+ comes back): the bot's own membership change and her post come back. Scheduler reads its bot (getMe), checks the bot
63
63
  may delete messages there (getChatMember: an administrator with can_delete_messages), deletes her /connect post
64
64
  (deleteMessage), posts "Connection Successful. Message will be deleted in 10 seconds." (sendMessage) and, ten
65
65
  seconds later, deletes that too (deleteMessage). It then reads the channel (getChat), the photo's file (getFile), and copies the photo into its
66
66
  storage from the file's link (the file download).
67
- 6. **Thursday 8 January, 10:00. The launch post, and the release notes.** Postiz publishes Ana's announcement, bold title and two paragraphs
67
+ 6. **Thursday 8 January, 10:00. The launch post, and the release notes.** Scheduler publishes Ana's announcement, bold title and two paragraphs
68
68
  (sendMessage, parse_mode HTML).
69
69
  At 11:00 Ana asks the agent in her private chat for the 0.9 release notes by Friday; Harness's poll receives it,
70
70
  it shows the bot typing, and the agent answers that the work is card t_7c1e9a04 on its board and that this chat
@@ -72,23 +72,23 @@ is a member; a bot deletes another's message in a channel only with the right to
72
72
  January, 15:00**: the card is done with its notes file, and Harness's board tells the chat: the notice "card … done
73
73
  — Draft the 0.9 release notes" and its summary as the caption of the file (sendDocument, the file uploaded, the
74
74
  caption in MarkdownV2 as the Chat SDK escapes it).
75
- 7. **Monday 12 January, 10:00. A release with screenshots.** Postiz publishes the release post with three screenshots
75
+ 7. **Monday 12 January, 10:00. A release with screenshots.** Scheduler publishes the release post with three screenshots
76
76
  as one album, the text on the first (sendMediaGroup, the images uploaded). At 11:00 it publishes the post's comment,
77
- the changelog, as a reply to the album's first message (sendMessage with reply_to_message_id).
77
+ the changelog, as a reply to the album's first message (sendMessage with reply_parameters naming it).
78
78
  8. **Monday 12 January, 18:00. The standups move.** The team moves its standups to a video call and Ana removes the
79
79
  agent bot from "Tide Labs team" (door), but leaves the digest job on. Tuesday 13 January at 09:15 the job posts its
80
80
  digest to the home chat, which says no standup items were posted this week (that morning's standup was on the
81
81
  call), and Telegram refuses: "Forbidden: bot was kicked from the group chat". Ana deletes the job in Harness (no
82
82
  Telegram call).
83
83
  9. **Wednesday 14 January, 16:00. A caption too long.** Ana's next post is one product photo with a 1,091-character
84
- story. Postiz sends it (sendPhoto) and Telegram refuses it: a photo's caption holds at most 1,024 characters
85
- ("Bad Request: message caption is too long"); Postiz shows "Telegram rejected the post". At 16:20 she cuts the story
86
- to a paragraph and Postiz sends it again, which posts.
87
- 10. **Monday 19 January, 11:00. A demo and a paper.** Postiz publishes the product demo video (sendVideo, uploaded) and,
84
+ story. Scheduler sends it (sendPhoto) and Telegram refuses it: a photo's caption holds at most 1,024 characters
85
+ ("Bad Request: message caption is too long"); Scheduler shows "Telegram rejected the post". At 16:20 she cuts the story
86
+ to a paragraph and Scheduler sends it again, which posts.
87
+ 10. **Monday 19 January, 11:00. A demo and a paper.** Scheduler publishes the product demo video (sendVideo, uploaded) and,
88
88
  at 15:00, the white paper as a PDF (sendDocument, uploaded).
89
89
  11. **Friday 27 February, 17:00. The agent retired.** Ana stops using the agent on Telegram and, in BotFather, makes a new
90
90
  token for it (/token), which revokes token A. Harness, still running on her laptop, polls with it and is refused ("Unauthorized"); she stops it. The
91
- news channel, its posts and the news bot stay with Postiz.
91
+ news channel, its posts and the news bot stay with Scheduler.
92
92
 
93
93
  ## What the story reaches
94
94
 
@@ -96,4 +96,4 @@ getMe (both bots), deleteWebhook, getUpdates (held, a long poll; with allowed_up
96
96
  privacy mode (a plain member bot sent a service message and not a plain mention; an administrator bot sent every
97
97
  message), sendChatAction, sendMessage (plain, HTML, a reply; refused for a removed bot), getChatMember, deleteMessage
98
98
  (twice), getChat, getFile, the file download, sendPhoto (refused for its caption, then sent), sendVideo, sendDocument
99
- (Postiz's PDF; Harness's notes with a MarkdownV2 caption), sendMediaGroup; and a revoked token's refusal.
99
+ (Scheduler's PDF; Harness's notes with a MarkdownV2 caption), sendMediaGroup; and a revoked token's refusal.
@@ -1,75 +1,40 @@
1
1
  {
2
2
  "judge": {
3
- "agent": "Claude Code (Claude Agent SDK), model claude-opus-5-5",
4
- "name": "Contributor 106"
3
+ "agent": "Claude journey judge (Opus)",
4
+ "name": "Independent journey judge"
5
5
  },
6
6
  "authors": [
7
- "Contributor 173"
7
+ "yueranyuan",
8
+ "Aaron (A-001)",
9
+ "Claude Opus 5.5",
10
+ "Codex"
8
11
  ],
9
- "bar": "829132787d94a2d4",
10
- "sha256": "267e4ae00d8d6bd435831be1a5b9dc741a7003bd0a43641ea7c4c000ee974e20",
11
- "judgedAt": "2026-10-02",
12
+ "sha256": "4186f9087ab35bb7639a6d5aaa3471c51695ed2be4c086e417a7e2ebd2310d19",
13
+ "judgedAt": "2026-10-15",
12
14
  "plausible": true,
13
15
  "rulings": [
14
16
  {
15
- "id": "three-tuesdays-skipped",
17
+ "id": "reply-to-message-id-era",
16
18
  "steps": [
17
- "act 3",
18
- "act 4",
19
- "act 7",
20
- "act 8",
21
- "act 9",
22
- "act 10"
19
+ "act 7"
23
20
  ],
24
- "barItem": "a stated cause that never happens, and a promise never kept",
25
- "reason": "Act 3 sets the standup weekly on Tuesdays and makes the agent an administrator, which is sent every group message (https://core.telegram.org/bots/features#privacy-mode). Act 4 runs the first standup and digest on 6 January. The standups move to the call only on Monday 2 February (act 10). Tuesdays 13, 20 and 27 January fall within acts 7-9 with no standup, no poll and no digest. Act 10's digest then says that no items were posted \"this week\", as if earlier weeks had them.",
26
- "correction": "Move act 10 to Monday 12 January, 18:00, so that the refused digest is Tuesday 13 January's. Or tell the three January Tuesdays' standups and digests."
27
- },
28
- {
29
- "id": "postiz-never-given-token-n",
30
- "steps": [
31
- "Who acts",
32
- "act 5"
33
- ],
34
- "barItem": "the wrong actor or credential for an act",
35
- "reason": "Postiz posts to Telegram through the one bot its deployment configures, process.env.TELEGRAM_TOKEN (postiz-app e0a08a7, libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts:19). In hosted Postiz, that bot is Postiz's own. The outline calls @tidelabs_news_bot, token N, \"Postiz's Telegram integration\", and in act 5 Ana makes the bot and Postiz then calls with it. The outline never says that she runs Postiz herself or that token N becomes its TELEGRAM_TOKEN.",
36
- "correction": "In 'Who acts' and act 5, say that Ana self-hosts Postiz (docker compose, local storage) and sets its TELEGRAM_TOKEN to token N before she adds Telegram."
21
+ "barItem": "the wrong era: building on an API the vendor had deprecated by the World's date; a call through a door the vendor does not document",
22
+ "reason": "Act 7 has Scheduler publish the changelog comment as a reply 'with reply_to_message_id'. Telegram replaced that parameter in Bot API 7.0, December 29, 2023 (https://core.telegram.org/bots/api-changelog: 'Replaced parameters reply_to_message_id and allow_sending_without_reply in the methods ... with the field reply_parameters'), two years before the story's January 2026, and the reference page the spec is written from (https://core.telegram.org/bots/api#sendmessage, Bot API 10.3) no longer documents it at all, as spec/SOURCE.md itself states; the act builds on a door the vendor withdrew, even though the server still reads it.",
23
+ "correction": "Act 7 sends the reply with reply_parameters (a JSON-serialized ReplyParameters naming the album's first message_id), as https://core.telegram.org/bots/api#sendmessage documents, and the 'What the story reaches' line says so."
37
24
  }
38
25
  ],
39
26
  "applied": {
40
- "from": "7f1802b57b256e32061a52ede0d2972060c725a7550b068ac1ae23f131c63c6c",
27
+ "from": "4bdc79fa09692fb332a4eac8080c9a10e5d7b2234435712284ef4e599d37d4eb",
41
28
  "wasPlausible": false,
42
29
  "steps": [
43
- "0",
44
- "5",
45
- "8",
46
- "9",
47
- "10"
30
+ "act 7"
48
31
  ],
49
32
  "rulings": [
50
33
  {
51
- "id": "three-tuesdays-skipped",
52
- "steps": [
53
- "act 3",
54
- "act 4",
55
- "act 7",
56
- "act 8",
57
- "act 9",
58
- "act 10"
59
- ],
60
- "barItem": "a stated cause that never happens, and a promise never kept",
61
- "reason": "Act 3 sets the standup weekly on Tuesdays and makes the agent an administrator, which is sent every group message (https://core.telegram.org/bots/features#privacy-mode). Act 4 runs the first standup and digest on 6 January. The standups move to the call only on Monday 2 February (act 10). Tuesdays 13, 20 and 27 January fall within acts 7-9 with no standup, no poll and no digest. Act 10's digest then says that no items were posted \"this week\", as if earlier weeks had them.",
62
- "correction": "Move act 10 to Monday 12 January, 18:00, so that the refused digest is Tuesday 13 January's. Or tell the three January Tuesdays' standups and digests."
63
- },
64
- {
65
- "id": "postiz-never-given-token-n",
66
34
  "steps": [
67
- "Who acts",
68
- "act 5"
35
+ "act 7"
69
36
  ],
70
- "barItem": "the wrong actor or credential for an act",
71
- "reason": "Postiz posts to Telegram through the one bot its deployment configures, process.env.TELEGRAM_TOKEN (postiz-app e0a08a7, libraries/nestjs-libraries/src/integrations/social/telegram.provider.ts:19). In hosted Postiz, that bot is Postiz's own. The outline calls @tidelabs_news_bot, token N, \"Postiz's Telegram integration\", and in act 5 Ana makes the bot and Postiz then calls with it. The outline never says that she runs Postiz herself or that token N becomes its TELEGRAM_TOKEN.",
72
- "correction": "In 'Who acts' and act 5, say that Ana self-hosts Postiz (docker compose, local storage) and sets its TELEGRAM_TOKEN to token N before she adds Telegram."
37
+ "why": "Act 7 sends the reply with reply_parameters (a JSON-serialized ReplyParameters naming the album's first message_id), as https://core.telegram.org/bots/api#sendmessage documents, and the 'What the story reaches' line says so."
73
38
  }
74
39
  ]
75
40
  }