@adityaaria/agent-os 1.0.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 (129) hide show
  1. package/AGENTS.md +41 -0
  2. package/AGENT_OS_BOOTSTRAP.md +29 -0
  3. package/ENTRYPOINT.md +118 -0
  4. package/LICENSE +21 -0
  5. package/README.md +230 -0
  6. package/adapters/antigravity/README.md +9 -0
  7. package/adapters/antigravity/plugin.json +13 -0
  8. package/adapters/claude/CLAUDE.md +16 -0
  9. package/adapters/claude/README.md +10 -0
  10. package/adapters/codex/README.md +20 -0
  11. package/adapters/cursor/README.md +9 -0
  12. package/adapters/cursor/agent-os.rules.md +18 -0
  13. package/bin/agent-os.mjs +98 -0
  14. package/core/AUTO_PROJECT_DETECTOR.md +99 -0
  15. package/core/COMPLIANCE_GATE.md +19 -0
  16. package/core/FRAMEWORK_AUDITOR.md +35 -0
  17. package/core/KNOWLEDGE_REGENERATOR.md +30 -0
  18. package/core/KNOWLEDGE_VALIDATOR.md +29 -0
  19. package/core/PROJECT_CONTEXT_STANDARD.md +19 -0
  20. package/core/QUALITY_GATES.md +42 -0
  21. package/core/REPORTING_STANDARD.md +47 -0
  22. package/core/SKILL_ORCHESTRATOR.md +63 -0
  23. package/core/TASK_ROUTER.md +33 -0
  24. package/core/TRACEABILITY_STANDARD.md +40 -0
  25. package/package.json +28 -0
  26. package/scripts/install.mjs +690 -0
  27. package/scripts/lib/cli-utils.mjs +148 -0
  28. package/scripts/lib/run-utils.mjs +185 -0
  29. package/scripts/lib/telegram-audit-log.mjs +50 -0
  30. package/scripts/lib/telegram-bot-utils.mjs +413 -0
  31. package/scripts/lib/telegram-utils.mjs +127 -0
  32. package/scripts/orchestrate.mjs +230 -0
  33. package/scripts/quality-static.mjs +19 -0
  34. package/scripts/report-export.mjs +86 -0
  35. package/scripts/report-notify.mjs +169 -0
  36. package/scripts/run-execution.mjs +83 -0
  37. package/scripts/run-list.mjs +44 -0
  38. package/scripts/run-status.mjs +38 -0
  39. package/scripts/run-step.mjs +173 -0
  40. package/scripts/setup-wizard.mjs +116 -0
  41. package/scripts/sync-runtime.mjs +87 -0
  42. package/scripts/telegram-bot.mjs +88 -0
  43. package/scripts/telegram-check.mjs +94 -0
  44. package/scripts/telegram-poll.mjs +176 -0
  45. package/scripts/validate-framework.mjs +290 -0
  46. package/skills/api-contract-audit/SKILL.md +26 -0
  47. package/skills/api-contract-audit/checklist.md +16 -0
  48. package/skills/api-contract-audit/examples.md +3 -0
  49. package/skills/api-contract-audit/report-template.md +39 -0
  50. package/skills/api-contract-audit/workflow.md +15 -0
  51. package/skills/audit/SKILL.md +28 -0
  52. package/skills/audit/checklist.md +18 -0
  53. package/skills/audit/examples.md +26 -0
  54. package/skills/audit/report-template.md +39 -0
  55. package/skills/audit/workflow.md +15 -0
  56. package/skills/bug-fix/SKILL.md +25 -0
  57. package/skills/bug-fix/checklist.md +18 -0
  58. package/skills/bug-fix/examples.md +26 -0
  59. package/skills/bug-fix/report-template.md +39 -0
  60. package/skills/bug-fix/workflow.md +10 -0
  61. package/skills/database-impact-analysis/SKILL.md +27 -0
  62. package/skills/database-impact-analysis/checklist.md +16 -0
  63. package/skills/database-impact-analysis/examples.md +3 -0
  64. package/skills/database-impact-analysis/report-template.md +39 -0
  65. package/skills/database-impact-analysis/workflow.md +14 -0
  66. package/skills/enhancement/SKILL.md +27 -0
  67. package/skills/enhancement/checklist.md +19 -0
  68. package/skills/enhancement/examples.md +26 -0
  69. package/skills/enhancement/report-template.md +39 -0
  70. package/skills/enhancement/workflow.md +10 -0
  71. package/skills/enterprise-certification/SKILL.md +19 -0
  72. package/skills/enterprise-certification/checklist.md +17 -0
  73. package/skills/enterprise-certification/report-template.md +39 -0
  74. package/skills/enterprise-certification/workflow.md +14 -0
  75. package/skills/knowledge-evolution/SKILL.md +23 -0
  76. package/skills/knowledge-evolution/checklist.md +17 -0
  77. package/skills/knowledge-evolution/report-template.md +39 -0
  78. package/skills/knowledge-evolution/workflow.md +14 -0
  79. package/skills/new-page/SKILL.md +26 -0
  80. package/skills/new-page/checklist.md +21 -0
  81. package/skills/new-page/examples.md +26 -0
  82. package/skills/new-page/report-template.md +39 -0
  83. package/skills/new-page/workflow.md +10 -0
  84. package/skills/project-onboarding/SKILL.md +27 -0
  85. package/skills/project-onboarding/checklist.md +18 -0
  86. package/skills/project-onboarding/examples.md +26 -0
  87. package/skills/project-onboarding/report-template.md +39 -0
  88. package/skills/project-onboarding/workflow.md +9 -0
  89. package/skills/release-readiness/SKILL.md +27 -0
  90. package/skills/release-readiness/checklist.md +16 -0
  91. package/skills/release-readiness/examples.md +3 -0
  92. package/skills/release-readiness/report-template.md +39 -0
  93. package/skills/release-readiness/workflow.md +14 -0
  94. package/skills/repository-health/SKILL.md +26 -0
  95. package/skills/repository-health/checklist.md +20 -0
  96. package/skills/repository-health/report-template.md +39 -0
  97. package/skills/repository-health/workflow.md +14 -0
  98. package/skills/selenium-e2e/SKILL.md +83 -0
  99. package/skills/selenium-e2e/checklist.md +29 -0
  100. package/skills/selenium-e2e/examples.md +26 -0
  101. package/skills/selenium-e2e/report-template.md +39 -0
  102. package/skills/selenium-e2e/workflow.md +9 -0
  103. package/skills/self-audit/SKILL.md +19 -0
  104. package/skills/self-audit/checklist.md +14 -0
  105. package/skills/self-audit/report-template.md +39 -0
  106. package/skills/self-audit/workflow.md +11 -0
  107. package/skills/traceability-analysis/SKILL.md +25 -0
  108. package/skills/traceability-analysis/checklist.md +17 -0
  109. package/skills/traceability-analysis/examples.md +3 -0
  110. package/skills/traceability-analysis/report-template.md +39 -0
  111. package/skills/traceability-analysis/workflow.md +17 -0
  112. package/templates/API_RULES.md +20 -0
  113. package/templates/ARCHITECTURE.md +20 -0
  114. package/templates/BUSINESS_FLOW.md +20 -0
  115. package/templates/COMMANDS.md +20 -0
  116. package/templates/DATABASE_RULES.md +20 -0
  117. package/templates/DEPENDENCY_MAP.md +20 -0
  118. package/templates/DEPLOYMENT_RULES.md +20 -0
  119. package/templates/DOMAIN_MODEL.md +20 -0
  120. package/templates/FE_BE_TRACEABILITY.md +20 -0
  121. package/templates/KNOWN_LIMITATIONS.md +20 -0
  122. package/templates/MODULE_CATALOG.md +20 -0
  123. package/templates/PROJECT_PROFILE.md +20 -0
  124. package/templates/RISK_REGISTER.md +20 -0
  125. package/templates/SECURITY_RULES.md +20 -0
  126. package/templates/TECHNICAL_DEBT.md +20 -0
  127. package/templates/TECH_STACK.md +20 -0
  128. package/templates/TESTING_RULES.md +47 -0
  129. package/templates/UI_UX_RULES.md +20 -0
@@ -0,0 +1,413 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { collectRun, collectRuns } from './run-utils.mjs';
6
+ import { escapeTelegramMarkdown } from './telegram-utils.mjs';
7
+ import { appendAuditEntry, readAuditLog } from './telegram-audit-log.mjs';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const repoRoot = path.resolve(path.dirname(__filename), '..', '..');
11
+
12
+ export function readTelegramUpdatesFromFile(filePath, baseDir = process.cwd()) {
13
+ const raw = JSON.parse(fs.readFileSync(path.resolve(baseDir, filePath), 'utf8'));
14
+ return normalizeTelegramUpdates(raw);
15
+ }
16
+
17
+ export function normalizeTelegramUpdates(raw) {
18
+ if (Array.isArray(raw)) return raw;
19
+ if (Array.isArray(raw?.result)) return raw.result;
20
+ return [];
21
+ }
22
+
23
+ export function createAllowedChats(allowedChatId) {
24
+ return new Set(String(allowedChatId ?? '').split(',').map((id) => id.trim()).filter(Boolean));
25
+ }
26
+
27
+ export function createTelegramAccessControl({ allowedChatId, roles }) {
28
+ const allowedChats = createAllowedChats(allowedChatId);
29
+ const roleMap = buildRoleMap(roles);
30
+ return { allowedChats, roleMap, hasConfiguredRoles: roleMap.size > 0 };
31
+ }
32
+
33
+ export function processTelegramUpdates({ updates, allowedChats, accessControl, runRoot, channelConfig, auditRunRoot }) {
34
+ const control = accessControl ?? { allowedChats, roleMap: new Map(), hasConfiguredRoles: false };
35
+ return updates.map((update) => processTelegramUpdate({ update, accessControl: control, runRoot, channelConfig, auditRunRoot })).filter(Boolean);
36
+ }
37
+
38
+ export function writeTelegramBotOutput({ channelContext, channelName, responses, outputOverride, mode = 'dry-run' }) {
39
+ const outputPath = path.resolve(
40
+ channelContext.outputBaseDir,
41
+ outputOverride ?? channelContext.channel.botOutput ?? 'reports/telegram-bot-responses.json'
42
+ );
43
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
44
+ fs.writeFileSync(outputPath, `${JSON.stringify({
45
+ channel: channelName,
46
+ mode,
47
+ responses
48
+ }, null, 2)}\n`);
49
+ return outputPath;
50
+ }
51
+
52
+ function buildRoleMap(roles) {
53
+ const roleMap = new Map();
54
+ for (const [role, chatIds] of Object.entries(roles ?? {})) {
55
+ for (const chatId of Array.isArray(chatIds) ? chatIds : [chatIds]) {
56
+ if (chatId !== null && chatId !== undefined) roleMap.set(String(chatId), role);
57
+ }
58
+ }
59
+ return roleMap;
60
+ }
61
+
62
+ function processTelegramUpdate({ update, accessControl, runRoot, channelConfig, auditRunRoot }) {
63
+ const incoming = extractIncomingCommand(update);
64
+ if (!incoming) return null;
65
+
66
+ const chatId = incoming.chatId;
67
+ const parsed = parseCommand(incoming.text);
68
+ const logEntry = {
69
+ chatId,
70
+ command: parsed.command,
71
+ argument: parsed.argument,
72
+ action: 'command_received'
73
+ };
74
+
75
+ if (!accessControl.allowedChats.has(chatId)) {
76
+ logEntry.action = 'access_denied';
77
+ logEntry.result = 'denied';
78
+ appendAuditEntry({ runRoot: auditRunRoot, entry: logEntry });
79
+ return {
80
+ chatId,
81
+ allowed: false,
82
+ command: parsed.command,
83
+ text: 'This chat is not authorized to control Agent OS.'
84
+ };
85
+ }
86
+
87
+ const role = resolveRole(accessControl, chatId);
88
+ logEntry.role = role;
89
+
90
+ const denied = denyForRole({ parsed, role });
91
+ if (denied) {
92
+ logEntry.action = 'role_denied';
93
+ logEntry.result = 'denied';
94
+ appendAuditEntry({ runRoot: auditRunRoot, entry: logEntry });
95
+ return {
96
+ chatId,
97
+ allowed: true,
98
+ role,
99
+ command: parsed.command,
100
+ text: denied
101
+ };
102
+ }
103
+
104
+ const isCallback = !!incoming.callbackQueryId;
105
+ if (parsed.command === 'run' && isCallback && isApprovalExpired(parsed.expiresAt)) {
106
+ logEntry.action = 'approval_expired';
107
+ logEntry.result = 'expired';
108
+ logEntry.runId = parsed.argument;
109
+ appendAuditEntry({ runRoot: auditRunRoot, entry: logEntry });
110
+ return {
111
+ chatId,
112
+ allowed: true,
113
+ role,
114
+ command: parsed.command,
115
+ callbackQueryId: incoming.callbackQueryId,
116
+ text: '⏰ Approval expired. This approval button is no longer valid.'
117
+ };
118
+ }
119
+
120
+ const response = dispatchCommand({ parsed, runRoot, channelConfig });
121
+ if (parsed.command === 'run') {
122
+ logEntry.action = 'run_executed';
123
+ logEntry.runId = response.runId || parsed.argument;
124
+ logEntry.result = response.text.startsWith('Executed run') ? 'success' : 'error';
125
+ } else if (['bug', 'enhancement', 'audit', 'test', 'analyze_project', 'audit_framework'].includes(parsed.command)) {
126
+ logEntry.action = 'run_created';
127
+ logEntry.runId = response.runId;
128
+ logEntry.result = response.text.startsWith('Created run') ? 'success' : 'error';
129
+ } else {
130
+ logEntry.action = 'command_received';
131
+ logEntry.result = 'success';
132
+ }
133
+
134
+ appendAuditEntry({ runRoot: auditRunRoot, entry: logEntry });
135
+
136
+ return {
137
+ chatId,
138
+ allowed: true,
139
+ role,
140
+ command: parsed.command,
141
+ callbackQueryId: incoming.callbackQueryId,
142
+ ...response
143
+ };
144
+ }
145
+
146
+ export function isApprovalExpired(expiresAt, nowFn = () => Math.floor(Date.now() / 1000)) {
147
+ if (expiresAt === null) return true;
148
+ return nowFn() > expiresAt;
149
+ }
150
+
151
+ function extractIncomingCommand(update) {
152
+ const message = update.message ?? update.edited_message;
153
+ if (message?.chat?.id && typeof message.text === 'string') {
154
+ return {
155
+ chatId: String(message.chat.id),
156
+ text: message.text
157
+ };
158
+ }
159
+
160
+ const callback = update.callback_query;
161
+ if (callback?.message?.chat?.id && typeof callback.data === 'string') {
162
+ return {
163
+ chatId: String(callback.message.chat.id),
164
+ text: callback.data,
165
+ callbackQueryId: callback.id
166
+ };
167
+ }
168
+
169
+ return null;
170
+ }
171
+
172
+ function resolveRole(accessControl, chatId) {
173
+ if (!accessControl.hasConfiguredRoles) return 'admin';
174
+ return accessControl.roleMap.get(chatId) ?? 'viewer';
175
+ }
176
+
177
+ function denyForRole({ parsed, role }) {
178
+ if (role === 'admin') return null;
179
+ const readCommands = new Set(['help', 'start', 'skills', 'runs', 'status']);
180
+ const planCommands = new Set(['bug', 'enhancement', 'audit', 'test', 'analyze_project', 'audit_framework']);
181
+
182
+ if (readCommands.has(parsed.command)) return null;
183
+ if (parsed.command === 'history' && ['admin', 'approver'].includes(role)) return null;
184
+ if (planCommands.has(parsed.command) && ['operator', 'approver'].includes(role)) return null;
185
+ if (parsed.command === 'run' && role === 'approver') return null;
186
+
187
+ return `Role ${escapeTelegramMarkdown(role)} is not permitted to use /${escapeTelegramMarkdown(parsed.command)}.`;
188
+ }
189
+
190
+ function parseCommand(text) {
191
+ const [rawCommand, ...rest] = text.trim().split(/\s+/);
192
+ const command = rawCommand.replace(/^\//, '').split('@')[0].toLowerCase();
193
+
194
+ let argument = rest.join(' ').trim();
195
+ let expiresAt = null;
196
+
197
+ if (command === 'run') {
198
+ const timeMatch = argument.match(/\bt=(\d+)$/);
199
+ if (timeMatch) {
200
+ expiresAt = parseInt(timeMatch[1], 10);
201
+ argument = argument.replace(/\bt=\d+$/, '').trim();
202
+ }
203
+ }
204
+
205
+ return {
206
+ command,
207
+ argument,
208
+ expiresAt
209
+ };
210
+ }
211
+
212
+ function dispatchCommand({ parsed, runRoot, channelConfig }) {
213
+ if (parsed.command === 'help' || parsed.command === 'start') return { text: helpText() };
214
+ if (parsed.command === 'skills') return { text: skillsText() };
215
+ if (parsed.command === 'runs') return { text: runsText(runRoot) };
216
+ if (parsed.command === 'history') return { text: historyText(runRoot, parsed.argument) };
217
+ if (parsed.command === 'status') return { text: statusText(runRoot, parsed.argument) };
218
+ if (parsed.command === 'run') return executeRun(runRoot, parsed.argument);
219
+
220
+ const prompt = promptFromCommand(parsed);
221
+ if (prompt !== null) return createRunFromPrompt({ command: parsed.command, prompt, runRoot, channelConfig });
222
+
223
+ return { text: `Unsupported command: /${escapeTelegramMarkdown(parsed.command)}. Use /help or /skills.` };
224
+ }
225
+
226
+ function helpText() {
227
+ return [
228
+ '*Agent OS Bot Commands*',
229
+ '/skills - list supported skill commands',
230
+ '/runs - list known runs',
231
+ '/history [limit] - view audit log history (admin/approver only)',
232
+ '/status <run-id> - summarize a run',
233
+ '/run <run-id> - execute a reviewed run',
234
+ '/bug <text> - create a bug-fix run plan',
235
+ '/enhancement <text> - create an enhancement run plan',
236
+ '/audit <text> - create an audit run plan',
237
+ '/test <text> - create a test run plan',
238
+ '/analyze_project <text> - create a project-onboarding run plan',
239
+ '/audit_framework - create a framework self-audit run plan'
240
+ ].join('\n');
241
+ }
242
+
243
+ function skillsText() {
244
+ return [
245
+ '*Available Agent OS skills*',
246
+ '/bug - bug-fix',
247
+ '/enhancement - enhancement',
248
+ '/audit - audit',
249
+ '/test - selenium-e2e',
250
+ '/analyze_project - project-onboarding',
251
+ '/audit_framework - self-audit',
252
+ '/status - run status',
253
+ '/history - view audit log',
254
+ '/run - reviewed run execution',
255
+ '/runs - run list'
256
+ ].join('\n');
257
+ }
258
+
259
+ function runsText(runRoot) {
260
+ try {
261
+ const runs = collectRuns(runRoot);
262
+ if (runs.length === 0) return 'No runs found.';
263
+ return runs.map((run) => `${escapeTelegramMarkdown(run.runId)}: ${escapeTelegramMarkdown(run.status)}`).join('\n');
264
+ } catch (err) {
265
+ return `No runs found. ${escapeTelegramMarkdown(err.message)}`;
266
+ }
267
+ }
268
+
269
+ function statusText(runRoot, runId) {
270
+ if (!runId) return 'Usage: /status <run-id>';
271
+ try {
272
+ const run = collectRun(path.join(runRoot, runId));
273
+ return [
274
+ `Run ID: ${escapeTelegramMarkdown(run.runId)}`,
275
+ `Status: ${escapeTelegramMarkdown(run.status)}`,
276
+ `Steps: ${run.counts.passed}/${run.total} passed, ${run.counts.failed} failed, ${run.counts.blocked} blocked`
277
+ ].join('\n');
278
+ } catch (err) {
279
+ return `Run not found: ${escapeTelegramMarkdown(runId)}. ${escapeTelegramMarkdown(err.message)}`;
280
+ }
281
+ }
282
+
283
+ function promptFromCommand(parsed) {
284
+ const argument = parsed.argument;
285
+ const map = {
286
+ bug: `BUG: ${argument}`,
287
+ enhancement: `ENHANCEMENT: ${argument}`,
288
+ audit: `AUDIT: ${argument}`,
289
+ test: `TEST: ${argument}`,
290
+ analyze_project: `ANALYZE PROJECT: ${argument || 'Initialize this repository'}`,
291
+ audit_framework: 'AUDIT FRAMEWORK'
292
+ };
293
+ if (!Object.hasOwn(map, parsed.command)) return null;
294
+ if (!argument && parsed.command !== 'audit_framework' && parsed.command !== 'analyze_project') {
295
+ return '';
296
+ }
297
+ return map[parsed.command];
298
+ }
299
+
300
+ function historyText(runRoot, argument) {
301
+ const logs = readAuditLog({ runRoot });
302
+ if (logs.length === 0) {
303
+ return 'No audit history available.';
304
+ }
305
+
306
+ let limit = 5;
307
+ if (argument) {
308
+ const parsedLimit = parseInt(argument.trim(), 10);
309
+ if (!isNaN(parsedLimit) && parsedLimit > 0) {
310
+ limit = Math.min(parsedLimit, 20); // Cap at 20 to avoid exceeding Telegram message size limits
311
+ }
312
+ }
313
+
314
+ const entries = logs.slice(-limit).reverse(); // Get latest N entries and show newest first
315
+ const lines = [`*Telegram Audit Log (Last ${entries.length} entries)*\n`];
316
+
317
+ for (const entry of entries) {
318
+ const time = new Date(entry.timestamp).toISOString().replace('T', ' ').substring(0, 19);
319
+ const statusIcon = entry.status === 'success' ? '✅' : '❌';
320
+ lines.push(`🕒 \`${time}\` | ${statusIcon}`);
321
+ lines.push(`👤 *User*: \`${escapeTelegramMarkdown(entry.user || 'Unknown')}\``);
322
+ lines.push(`🛠 *Action*: ${escapeTelegramMarkdown(entry.action || 'Unknown')}`);
323
+ if (entry.status === 'denied') {
324
+ lines.push(`🛑 *Reason*: ${escapeTelegramMarkdown(entry.reason || 'Denied')}`);
325
+ }
326
+ lines.push(' ');
327
+ }
328
+
329
+ return lines.join('\n');
330
+ }
331
+
332
+ function createRunFromPrompt({ command, prompt, runRoot, channelConfig }) {
333
+ if (!prompt) {
334
+ return { text: `Usage: /${escapeTelegramMarkdown(command)} <description>` };
335
+ }
336
+
337
+ const runId = `tg-${command}-${new Date().toISOString().replace(/[:.]/g, '-')}`;
338
+ const result = spawnSync(process.execPath, [
339
+ path.join(repoRoot, 'scripts', 'orchestrate.mjs'),
340
+ '--run-root',
341
+ runRoot,
342
+ '--run-id',
343
+ runId,
344
+ prompt
345
+ ], {
346
+ cwd: repoRoot,
347
+ encoding: 'utf8'
348
+ });
349
+
350
+ if (result.status !== 0) {
351
+ return { text: `Failed to create run: ${escapeTelegramMarkdown(result.stderr || result.stdout)}` };
352
+ }
353
+
354
+ const plan = JSON.parse(result.stdout);
355
+ const ttlMinutes = channelConfig?.approvalTtlMinutes ?? 15;
356
+ return {
357
+ runId,
358
+ replyMarkup: approvalReplyMarkup(runId, { ttlMinutes }),
359
+ text: [
360
+ `Created run: ${escapeTelegramMarkdown(runId)}`,
361
+ `Trigger: ${escapeTelegramMarkdown(plan.trigger)}`,
362
+ `Skill chain: ${escapeTelegramMarkdown(plan.skillChain.join(' -> '))}`,
363
+ 'Execution is not automatic. Review the plan before running execution.'
364
+ ].join('\n')
365
+ };
366
+ }
367
+
368
+ function approvalReplyMarkup(runId, { ttlMinutes = 15 } = {}) {
369
+ const expiresAt = Math.floor(Date.now() / 1000) + ttlMinutes * 60;
370
+ return {
371
+ inline_keyboard: [
372
+ [
373
+ {
374
+ text: 'Approve run',
375
+ callback_data: `/run ${runId} t=${expiresAt}`
376
+ }
377
+ ],
378
+ [
379
+ {
380
+ text: 'View status',
381
+ callback_data: `/status ${runId}`
382
+ }
383
+ ]
384
+ ]
385
+ };
386
+ }
387
+
388
+ function executeRun(runRoot, runId) {
389
+ if (!runId) return { text: 'Usage: /run <run-id>' };
390
+
391
+ const result = spawnSync(process.execPath, [
392
+ path.join(repoRoot, 'scripts', 'run-execution.mjs'),
393
+ '--run-dir',
394
+ path.join(runRoot, runId)
395
+ ], {
396
+ cwd: repoRoot,
397
+ encoding: 'utf8'
398
+ });
399
+
400
+ if (result.status !== 0) {
401
+ return { text: `Failed to execute run: ${escapeTelegramMarkdown(result.stderr || result.stdout)}` };
402
+ }
403
+
404
+ const execution = JSON.parse(result.stdout);
405
+ return {
406
+ runId,
407
+ text: [
408
+ `Executed run: ${escapeTelegramMarkdown(runId)}`,
409
+ `Status: ${escapeTelegramMarkdown(execution.status)}`,
410
+ `Steps: ${execution.steps}`
411
+ ].join('\n')
412
+ };
413
+ }
@@ -0,0 +1,127 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export function escapeTelegramMarkdown(value) {
5
+ return String(value ?? '').replace(/([_*[\]()`])/g, '\\$1');
6
+ }
7
+
8
+ function statusCounts(run) {
9
+ return `${run.counts.passed}/${run.total} passed, ${run.counts.failed} failed, ${run.counts.blocked} blocked`;
10
+ }
11
+
12
+ export function renderTelegramRunReport(runs) {
13
+ const rows = Array.isArray(runs) ? runs : [runs];
14
+ const lines = ['*Agent OS Run Report*'];
15
+ for (const run of rows) {
16
+ lines.push(`\nRun ID: ${escapeTelegramMarkdown(run.runId)}`);
17
+ lines.push(`Status: *${escapeTelegramMarkdown(run.status.toUpperCase())}* (${statusCounts(run)})`);
18
+ if (run.plan) {
19
+ lines.push(`Trigger: ${escapeTelegramMarkdown(run.plan.trigger)}`);
20
+ lines.push(`Prompt: ${escapeTelegramMarkdown(run.plan.prompt)}`);
21
+ }
22
+ if (run.steps && run.steps.length > 0) {
23
+ lines.push('\n*Execution Steps:*');
24
+ run.steps.forEach((step) => {
25
+ lines.push(`- Step ${step.step}: ${escapeTelegramMarkdown(step.skill)} (${escapeTelegramMarkdown(step.status)})`);
26
+ });
27
+ }
28
+ if (run.reportDetails?.evidence && run.reportDetails.evidence !== 'N/A. Reason: Skills were not executed by this stub.') {
29
+ lines.push('\n*Test Evidence:*');
30
+ lines.push(escapeTelegramMarkdown(run.reportDetails.evidence));
31
+ }
32
+ }
33
+ return lines.join('\n');
34
+ }
35
+
36
+ export function renderTelegramLifecycleAlert(runs, event) {
37
+ const rows = Array.isArray(runs) ? runs : [runs];
38
+ const run = rows[0];
39
+ const eventLabel = escapeTelegramMarkdown(String(event ?? run.status).toUpperCase());
40
+ const lines = ['*Agent OS Lifecycle Alert*'];
41
+ lines.push(`Event: *${eventLabel}*`);
42
+ lines.push(`Run ID: ${escapeTelegramMarkdown(run.runId)}`);
43
+ lines.push(`Status: *${escapeTelegramMarkdown(run.status.toUpperCase())}* (${statusCounts(run)})`);
44
+ if (run.plan) {
45
+ lines.push(`Trigger: ${escapeTelegramMarkdown(run.plan.trigger)}`);
46
+ lines.push(`Prompt: ${escapeTelegramMarkdown(run.plan.prompt)}`);
47
+ }
48
+
49
+ const relevantSteps = (run.steps ?? []).filter((step) => ['failed', 'blocked'].includes(step.status));
50
+ if (relevantSteps.length > 0) {
51
+ lines.push('\n*Attention Required:*');
52
+ relevantSteps.forEach((step) => {
53
+ lines.push(`- Step ${step.step}: ${escapeTelegramMarkdown(step.skill)} (${escapeTelegramMarkdown(step.status)})`);
54
+ });
55
+ }
56
+
57
+ return lines.join('\n');
58
+ }
59
+
60
+ export function renderTelegramTestMessage(channelName) {
61
+ return [
62
+ '*Agent OS Telegram Test*',
63
+ `Channel: ${escapeTelegramMarkdown(channelName)}`,
64
+ 'Status: configuration reachable'
65
+ ].join('\n');
66
+ }
67
+
68
+ export function createTelegramMessage({ runs, event, test = false, channelName = 'telegram' }) {
69
+ if (test) {
70
+ return {
71
+ parse_mode: 'Markdown',
72
+ text: renderTelegramTestMessage(channelName)
73
+ };
74
+ }
75
+
76
+ return {
77
+ parse_mode: 'Markdown',
78
+ text: event ? renderTelegramLifecycleAlert(runs, event) : renderTelegramRunReport(runs)
79
+ };
80
+ }
81
+
82
+ export function telegramEnv(channel) {
83
+ const botTokenEnv = channel.botTokenEnv ?? 'AGENT_OS_TELEGRAM_BOT_TOKEN';
84
+ const chatIdEnv = channel.chatIdEnv ?? 'AGENT_OS_TELEGRAM_CHAT_ID';
85
+ return {
86
+ botTokenEnv,
87
+ chatIdEnv,
88
+ botToken: process.env[botTokenEnv] ?? null,
89
+ chatId: process.env[chatIdEnv] ?? null
90
+ };
91
+ }
92
+
93
+ export function buildTelegramRequest({ channel, message, botToken, chatId }) {
94
+ const apiBaseUrl = channel.apiBaseUrl ?? 'https://api.telegram.org';
95
+ return {
96
+ method: 'POST',
97
+ url: `${apiBaseUrl}/bot${botToken}/sendMessage`,
98
+ body: {
99
+ chat_id: chatId,
100
+ ...message
101
+ }
102
+ };
103
+ }
104
+
105
+ export async function sendTelegramRequest({ channel, channelContext, message, botToken, chatId }) {
106
+ const telegramRequest = buildTelegramRequest({ channel, message, botToken, chatId });
107
+
108
+ if (channel.mockRequestFile) {
109
+ const mockRequestPath = path.resolve(channelContext.outputBaseDir, channel.mockRequestFile);
110
+ fs.mkdirSync(path.dirname(mockRequestPath), { recursive: true });
111
+ fs.writeFileSync(mockRequestPath, `${JSON.stringify(telegramRequest, null, 2)}\n`);
112
+ return { mocked: true };
113
+ }
114
+
115
+ const response = await fetch(telegramRequest.url, {
116
+ method: telegramRequest.method,
117
+ headers: { 'content-type': 'application/json' },
118
+ body: JSON.stringify(telegramRequest.body)
119
+ });
120
+
121
+ if (!response.ok) {
122
+ const responseText = await response.text();
123
+ throw new Error(`Telegram send failed (${response.status}): ${responseText}`);
124
+ }
125
+
126
+ return { mocked: false };
127
+ }