@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.
- package/README.md +2 -2
- package/dist/journeys/customer-life.json +21 -21
- package/dist/journeys/customer-life.outline.md +17 -17
- package/dist/journeys/customer-life.outline.verdict.json +17 -52
- package/dist/journeys/customer-life.verdict.json +24 -98
- package/dist/journeys/decisions.json +49 -13
- package/dist/journeys/demand.json +0 -91
- package/dist/journeys/first-use.json +21 -0
- package/dist/journeys/unreachable.json +1 -1
- package/dist/journeys/vendor-examples.json +14 -6
- package/dist/spec/SOURCE.md +5 -6
- package/dist/spec/patches.json +20 -20
- package/dist/spec/sources.json +15 -0
- package/dist/src/cli.js +1 -1
- package/dist/src/engine/media.d.ts +6 -12
- package/dist/src/engine/media.js +28 -66
- package/dist/src/engine/text.d.ts +0 -7
- package/dist/src/engine/text.js +2 -2
- package/dist/src/index.js +2 -1
- package/dist/src/manifest.js +7 -6
- package/dist/src/semantics/available-methods.js +50 -40
- package/dist/src/semantics/doors.d.ts +0 -1
- package/dist/src/semantics/doors.js +82 -5
- package/dist/src/semantics/shared.d.ts +10 -6
- package/dist/src/semantics/shared.js +29 -8
- package/dist/src/server.js +2 -19
- package/generated/pack-facts.json +10 -0
- package/journeys/customer-life.json +21 -21
- package/journeys/customer-life.outline.md +17 -17
- package/journeys/customer-life.outline.verdict.json +17 -52
- package/journeys/customer-life.verdict.json +24 -98
- package/journeys/decisions.json +49 -13
- package/journeys/demand.json +0 -91
- package/journeys/first-use.json +21 -0
- package/journeys/unreachable.json +1 -1
- package/journeys/vendor-examples.json +14 -6
- package/package.json +12 -9
- package/spec/SOURCE.md +5 -6
- package/spec/patches.json +20 -20
- package/spec/sources.json +15 -0
- package/src/cli.ts +1 -1
- package/src/engine/media.ts +21 -58
- package/src/engine/text.ts +2 -2
- package/src/index.ts +2 -1
- package/src/manifest.ts +7 -6
- package/src/semantics/available-methods.ts +55 -41
- package/src/semantics/doors.ts +74 -8
- package/src/semantics/shared.ts +27 -8
- package/src/server.ts +2 -11
- package/dist/generated/pack-facts.json +0 -53
- package/dist/spec/page-to-openapi.ts +0 -258
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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."
|
package/dist/src/server.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
982
|
+
"path": "result.0.photo.2.width",
|
|
983
983
|
"equals": 480
|
|
984
984
|
},
|
|
985
985
|
{
|
|
986
|
-
"path": "result.2.photo.
|
|
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": "
|
|
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&
|
|
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": "
|
|
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": "
|
|
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.
|
|
1111
|
+
"path": "result.photo.2.width",
|
|
1112
1112
|
"equals": 640
|
|
1113
1113
|
},
|
|
1114
1114
|
{
|
|
1115
|
-
"path": "result.photo.
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
6
|
-
2026: two bots made, the agent at work in a private chat and a group, a news channel connected 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
|
|
25
|
-
the one bot its deployment configures, its TELEGRAM_TOKEN
|
|
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
|
|
58
|
-
local storage) she sets TELEGRAM_TOKEN to token N, and then chooses to add Telegram;
|
|
59
|
-
shows the command to post, "/connect" and a word, and she posts it in the channel (door).
|
|
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.
|
|
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.**
|
|
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.**
|
|
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
|
|
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.
|
|
85
|
-
("Bad Request: message caption is too long");
|
|
86
|
-
to a paragraph and
|
|
87
|
-
10. **Monday 19 January, 11:00. A demo and a paper.**
|
|
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
|
|
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
|
-
(
|
|
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
|
|
4
|
-
"name": "
|
|
3
|
+
"agent": "Claude journey judge (Opus)",
|
|
4
|
+
"name": "Independent journey judge"
|
|
5
5
|
},
|
|
6
6
|
"authors": [
|
|
7
|
-
"
|
|
7
|
+
"yueranyuan",
|
|
8
|
+
"Aaron (A-001)",
|
|
9
|
+
"Claude Opus 5.5",
|
|
10
|
+
"Codex"
|
|
8
11
|
],
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"judgedAt": "2026-10-02",
|
|
12
|
+
"sha256": "4186f9087ab35bb7639a6d5aaa3471c51695ed2be4c086e417a7e2ebd2310d19",
|
|
13
|
+
"judgedAt": "2026-10-15",
|
|
12
14
|
"plausible": true,
|
|
13
15
|
"rulings": [
|
|
14
16
|
{
|
|
15
|
-
"id": "
|
|
17
|
+
"id": "reply-to-message-id-era",
|
|
16
18
|
"steps": [
|
|
17
|
-
"act
|
|
18
|
-
"act 4",
|
|
19
|
-
"act 7",
|
|
20
|
-
"act 8",
|
|
21
|
-
"act 9",
|
|
22
|
-
"act 10"
|
|
19
|
+
"act 7"
|
|
23
20
|
],
|
|
24
|
-
"barItem": "
|
|
25
|
-
"reason": "Act
|
|
26
|
-
"correction": "
|
|
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": "
|
|
27
|
+
"from": "4bdc79fa09692fb332a4eac8080c9a10e5d7b2234435712284ef4e599d37d4eb",
|
|
41
28
|
"wasPlausible": false,
|
|
42
29
|
"steps": [
|
|
43
|
-
"
|
|
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
|
-
"
|
|
68
|
-
"act 5"
|
|
35
|
+
"act 7"
|
|
69
36
|
],
|
|
70
|
-
"
|
|
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
|
}
|