instar 0.28.72 → 0.28.74
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +51 -23
- package/dist/commands/server.js.map +1 -1
- package/dist/core/CapabilityMapper.d.ts.map +1 -1
- package/dist/core/CapabilityMapper.js +8 -2
- package/dist/core/CapabilityMapper.js.map +1 -1
- package/dist/core/SessionManager.d.ts +4 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +15 -4
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts +8 -0
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +31 -1
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +28 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +79 -1
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/TelegramMarkdownFormatter.d.ts +76 -0
- package/dist/messaging/TelegramMarkdownFormatter.d.ts.map +1 -0
- package/dist/messaging/TelegramMarkdownFormatter.js +493 -0
- package/dist/messaging/TelegramMarkdownFormatter.js.map +1 -0
- package/dist/messaging/imessage/IMessageAdapter.d.ts +3 -6
- package/dist/messaging/imessage/IMessageAdapter.d.ts.map +1 -1
- package/dist/messaging/imessage/IMessageAdapter.js +20 -21
- package/dist/messaging/imessage/IMessageAdapter.js.map +1 -1
- package/dist/messaging/imessage/NativeBackend.d.ts +26 -0
- package/dist/messaging/imessage/NativeBackend.d.ts.map +1 -1
- package/dist/messaging/imessage/NativeBackend.js +133 -0
- package/dist/messaging/imessage/NativeBackend.js.map +1 -1
- package/dist/messaging/telegramFormatMetrics.d.ts +22 -0
- package/dist/messaging/telegramFormatMetrics.d.ts.map +1 -0
- package/dist/messaging/telegramFormatMetrics.js +38 -0
- package/dist/messaging/telegramFormatMetrics.js.map +1 -0
- package/dist/messaging/types.d.ts +7 -0
- package/dist/messaging/types.d.ts.map +1 -1
- package/dist/messaging/types.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +5 -5
- package/src/templates/scripts/telegram-reply.sh +52 -8
- package/upgrades/0.28.73.md +33 -0
- package/upgrades/0.28.74.md +48 -0
- package/upgrades/side-effects/0.28.73.md +80 -0
- package/upgrades/side-effects/telegram-markdown-renderer-pr1.md +124 -0
- /package/upgrades/side-effects/{tunnel-retry-exhaustion-notify.md → 0.28.72.md} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-04-
|
|
5
|
-
"instarVersion": "0.28.
|
|
4
|
+
"generatedAt": "2026-04-25T00:12:18.203Z",
|
|
5
|
+
"instarVersion": "0.28.74",
|
|
6
6
|
"entryCount": 186,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -1112,7 +1112,7 @@
|
|
|
1112
1112
|
"type": "template",
|
|
1113
1113
|
"domain": "operations",
|
|
1114
1114
|
"sourcePath": "src/templates/scripts/telegram-reply.sh",
|
|
1115
|
-
"contentHash": "
|
|
1115
|
+
"contentHash": "3d08c63c6280d0a7ba94a345c259673a461ee5c1d116cb47c95c7626c67cee23",
|
|
1116
1116
|
"since": "2025-01-01"
|
|
1117
1117
|
},
|
|
1118
1118
|
"template:whatsapp-reply.sh": {
|
|
@@ -1408,7 +1408,7 @@
|
|
|
1408
1408
|
"type": "subsystem",
|
|
1409
1409
|
"domain": "sessions",
|
|
1410
1410
|
"sourcePath": "src/core/SessionManager.ts",
|
|
1411
|
-
"contentHash": "
|
|
1411
|
+
"contentHash": "b47fdc0040c512bada61574b4fd1505da3c9bbe2adc793915d6f5bd479faad06",
|
|
1412
1412
|
"since": "2025-01-01"
|
|
1413
1413
|
},
|
|
1414
1414
|
"subsystem:auto-updater": {
|
|
@@ -1464,7 +1464,7 @@
|
|
|
1464
1464
|
"type": "subsystem",
|
|
1465
1465
|
"domain": "communication",
|
|
1466
1466
|
"sourcePath": "src/lifeline/TelegramLifeline.ts",
|
|
1467
|
-
"contentHash": "
|
|
1467
|
+
"contentHash": "22d7d3827ba85d345862e55457fd2cca83e4f38201da61e20a7f0e539052322b",
|
|
1468
1468
|
"since": "2025-01-01"
|
|
1469
1469
|
},
|
|
1470
1470
|
"subsystem:orphan-process-reaper": {
|
|
@@ -3,18 +3,52 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
5
|
# ./telegram-reply.sh TOPIC_ID "message text"
|
|
6
|
+
# ./telegram-reply.sh --format markdown TOPIC_ID "**bold**"
|
|
6
7
|
# echo "message text" | ./telegram-reply.sh TOPIC_ID
|
|
7
8
|
# cat <<'EOF' | ./telegram-reply.sh TOPIC_ID
|
|
8
9
|
# Multi-line message here
|
|
9
10
|
# EOF
|
|
10
11
|
#
|
|
12
|
+
# Flags:
|
|
13
|
+
# --format <mode> Override server-side format mode for this send.
|
|
14
|
+
# Valid: plain, code, markdown, legacy-passthrough
|
|
15
|
+
# ('html' is reserved for trusted internal callers.)
|
|
16
|
+
# When absent, the server's configured default applies.
|
|
17
|
+
#
|
|
11
18
|
# Reads INSTAR_PORT from environment (default: 4040).
|
|
12
19
|
|
|
20
|
+
FORMAT=""
|
|
21
|
+
|
|
22
|
+
# Parse leading flags before positional args.
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--format)
|
|
26
|
+
FORMAT="$2"
|
|
27
|
+
shift 2
|
|
28
|
+
;;
|
|
29
|
+
--format=*)
|
|
30
|
+
FORMAT="${1#--format=}"
|
|
31
|
+
shift
|
|
32
|
+
;;
|
|
33
|
+
--)
|
|
34
|
+
shift
|
|
35
|
+
break
|
|
36
|
+
;;
|
|
37
|
+
-*)
|
|
38
|
+
echo "Unknown flag: $1" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
;;
|
|
41
|
+
*)
|
|
42
|
+
break
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
|
|
13
47
|
TOPIC_ID="$1"
|
|
14
48
|
shift
|
|
15
49
|
|
|
16
50
|
if [ -z "$TOPIC_ID" ]; then
|
|
17
|
-
echo "Usage: telegram-reply.sh TOPIC_ID [message]" >&2
|
|
51
|
+
echo "Usage: telegram-reply.sh [--format MODE] TOPIC_ID [message]" >&2
|
|
18
52
|
exit 1
|
|
19
53
|
fi
|
|
20
54
|
|
|
@@ -38,22 +72,32 @@ if [ -f ".instar/config.json" ]; then
|
|
|
38
72
|
AUTH_TOKEN=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null)
|
|
39
73
|
fi
|
|
40
74
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
75
|
+
# Build JSON body (text + optional format).
|
|
76
|
+
JSON_BODY=$(python3 -c '
|
|
77
|
+
import sys, json
|
|
78
|
+
msg = sys.argv[1]
|
|
79
|
+
fmt = sys.argv[2]
|
|
80
|
+
body = {"text": msg}
|
|
81
|
+
if fmt:
|
|
82
|
+
body["format"] = fmt
|
|
83
|
+
print(json.dumps(body))
|
|
84
|
+
' "$MSG" "$FORMAT" 2>/dev/null)
|
|
85
|
+
|
|
86
|
+
if [ -z "$JSON_BODY" ]; then
|
|
87
|
+
# Fallback if python3 not available: basic escape, no format override.
|
|
88
|
+
ESCAPED=$(printf '%s' "$MSG" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g')
|
|
89
|
+
JSON_BODY="{\"text\":\"${ESCAPED}\"}"
|
|
46
90
|
fi
|
|
47
91
|
|
|
48
92
|
if [ -n "$AUTH_TOKEN" ]; then
|
|
49
93
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/telegram/reply/${TOPIC_ID}" \
|
|
50
94
|
-H 'Content-Type: application/json' \
|
|
51
95
|
-H "Authorization: Bearer ${AUTH_TOKEN}" \
|
|
52
|
-
-d "
|
|
96
|
+
-d "$JSON_BODY")
|
|
53
97
|
else
|
|
54
98
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/telegram/reply/${TOPIC_ID}" \
|
|
55
99
|
-H 'Content-Type: application/json' \
|
|
56
|
-
-d "
|
|
100
|
+
-d "$JSON_BODY")
|
|
57
101
|
fi
|
|
58
102
|
|
|
59
103
|
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Upgrade Guide — v0.28.73
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
`CapabilityMapper.classify()` now correctly labels agent-local capabilities (skills, scripts, jobs, context segments, custom hooks) that are NOT in the shipped builtin manifest and NOT linked to an evolution proposal as `user` provenance, instead of leaving them as `unknown`. The `Provenance` type has always declared `user` as a first-class value, but the classifier never assigned it — so every agent's drift report over-counted "unmapped" capabilities and `userConfigured` was always zero in the summary.
|
|
8
|
+
|
|
9
|
+
Concretely, in `classify()` the final fallback checks `cap.provenance === 'unknown'` and, if so, returns the capability with `provenance: 'user'`. Non-`unknown` pre-classify provenance (e.g. the `hooks/instar/` subdir which hardcodes `instar`) is preserved untouched. The persisted `capability-manifest.json` now records `classificationReason: 'agent-local config directory'` for these entries.
|
|
10
|
+
|
|
11
|
+
On the first scan after upgrading, each agent's `drift.changed` list will show a one-time provenance transition from `unknown` → `user` for every affected capability. That's expected — it's the signal that the fix took effect. `drift.unmapped` shrinks accordingly; `summary.userConfigured` populates correctly.
|
|
12
|
+
|
|
13
|
+
No schema change, no API surface change, no migration required.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
- **Cleaner drift reports**: "I stopped flagging my own config as mystery items. The capability map now recognizes what you and I set up together as user-authored."
|
|
18
|
+
|
|
19
|
+
## Summary of New Capabilities
|
|
20
|
+
|
|
21
|
+
| Capability | How to Use |
|
|
22
|
+
|-----------|-----------|
|
|
23
|
+
| Accurate user-provenance classification | Automatic on next `capability-map` scan |
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
|
|
27
|
+
Reproduction: on an agent running pre-fix, call the capability-map drift endpoint. The `unmapped` list contains every agent-local skill/script/job/context segment that isn't in the builtin manifest (~100+ entries on a mature agent). `summary.userConfigured` reads 0 even when the agent has many user-authored capabilities.
|
|
28
|
+
|
|
29
|
+
After upgrading and running one scan: those entries move out of `unmapped` and into the `user`-provenance tally. `drift.changed` shows the one-time transition for each affected capability.
|
|
30
|
+
|
|
31
|
+
Unit-level verification: `tests/unit/capability-mapper-advanced.test.ts` "classifies unmatched agent-local capabilities as user (not unmapped)" asserts both that a mystery skill is NOT in `drift.unmapped` and that its `provenance` on the capability map equals `'user'`. All 208 tests across the three capability-mapper test files pass.
|
|
32
|
+
|
|
33
|
+
Reporter: `cluster-capability-map-has-104-unmapped-capabilities` (2 reports, governance=implement).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Upgrade Guide — v0.28.74
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
The scope-coherence stop hook (`scope-coherence-checkpoint.js`) previously
|
|
8
|
+
suggested running `/grounding` as the only recovery path when the
|
|
9
|
+
implementation-depth threshold was crossed. `/grounding` is a Claude-Code-side
|
|
10
|
+
skill convention; Instar does not ship it, so agents whose harness lacks that
|
|
11
|
+
skill saw a dead-end suggestion with no working manual reset path.
|
|
12
|
+
|
|
13
|
+
The checkpoint message now lists three real options: read the relevant
|
|
14
|
+
spec/proposal, invoke `/grounding` if the harness has it, or reset directly via
|
|
15
|
+
the local Instar server's reset endpoint. The endpoint already exists and is
|
|
16
|
+
what `session-start.sh` invokes automatically — this release just surfaces it
|
|
17
|
+
as a manual fallback in the message agents see when they hit the checkpoint.
|
|
18
|
+
|
|
19
|
+
No code paths changed beyond the human-readable message string. The reset
|
|
20
|
+
endpoint and the depth-accumulation logic are unchanged.
|
|
21
|
+
|
|
22
|
+
## What to Tell Your User
|
|
23
|
+
|
|
24
|
+
- The scope-coherence checkpoint now points to a manual reset path that always works, so if the suggested skill is not installed in your setup you still have a one-line recovery I can run for you.
|
|
25
|
+
|
|
26
|
+
## Summary of New Capabilities
|
|
27
|
+
|
|
28
|
+
| Capability | How to Use |
|
|
29
|
+
|-----------|-----------|
|
|
30
|
+
| Manual scope-coherence reset surfaced in checkpoint message | `curl -X POST http://localhost:4040/scope-coherence/reset` |
|
|
31
|
+
|
|
32
|
+
## Evidence
|
|
33
|
+
|
|
34
|
+
Cluster `cluster-scope-coherence-hook-counter-never-resets-across-sessions`
|
|
35
|
+
flagged that `/grounding` is not a registered skill or command in Instar and
|
|
36
|
+
that there is no user-facing reset path shown in the checkpoint message.
|
|
37
|
+
|
|
38
|
+
Verification: `ls skills/` confirms no `grounding` skill ships with Instar.
|
|
39
|
+
The reset endpoint is implemented at `src/server/routes.ts:2925` and is
|
|
40
|
+
invoked by `.instar/hooks/instar/session-start.sh` when the local server is
|
|
41
|
+
healthy — so the underlying counter reset already works on every session
|
|
42
|
+
start; this release makes the manual fallback path discoverable from the
|
|
43
|
+
hook's own message.
|
|
44
|
+
|
|
45
|
+
Reproduction: trigger the checkpoint (implementation depth ≥ 20, cooldown
|
|
46
|
+
elapsed, server running) and inspect the `reason` field in the hook's
|
|
47
|
+
output. Before: ends with `or /grounding`. After: ends with the three-option
|
|
48
|
+
block including the explicit curl command for manual reset.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Side-Effects Review — Capability Map user-provenance fallback
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `capability-map-user-provenance-fallback`
|
|
4
|
+
**Date:** `2026-04-22`
|
|
5
|
+
**Author:** Dawn (instar-bug-fix autonomous job, AUT-6010-wo)
|
|
6
|
+
**Second-pass reviewer:** not-required (LOW-risk classifier fallback; no public API, schema, or adapter surface touched)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
In `src/core/CapabilityMapper.ts`, the `classify()` method now assigns `provenance: 'user'` to any capability that falls through all three existing classification rules (builtin-manifest, evolution linkage, custom hook dir) AND still carries the pre-classify default `'unknown'`. Capabilities whose scanners pre-assign a non-`unknown` provenance (e.g. the `hooks/instar/` subdir) are preserved untouched.
|
|
11
|
+
|
|
12
|
+
The persisted manifest's `classificationReason` gains a matching case: `'agent-local config directory'` for `user`-classified entries.
|
|
13
|
+
|
|
14
|
+
Files touched:
|
|
15
|
+
- `src/core/CapabilityMapper.ts` — two edits in the `classify()` and `persistManifest()` methods.
|
|
16
|
+
- `tests/unit/capability-mapper-advanced.test.ts` — one existing test renamed and updated to assert the new behavior (mystery skill classified as `user`, not left in `drift.unmapped`).
|
|
17
|
+
|
|
18
|
+
Feedback cluster addressed: `cluster-capability-map-has-104-unmapped-capabilities`. Reporter wanted the drift endpoint to stop flagging fully-functional agent-local capabilities as "unmapped." This ships exactly that: agent-local skills/scripts/jobs/context that aren't shipped-builtin and aren't evolution-linked are now correctly labeled `user`-provenance.
|
|
19
|
+
|
|
20
|
+
## Decision-point inventory
|
|
21
|
+
|
|
22
|
+
- Final fallback branch in `classify()` — **modify** — converts `unknown` → `user` when nothing else matched; preserves non-`unknown` pre-set provenance.
|
|
23
|
+
- `classificationReason` ternary in `persistManifest()` — **extend** — new case `user ? 'agent-local config directory'`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. Over-block
|
|
28
|
+
|
|
29
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
30
|
+
|
|
31
|
+
None. The change adds a positive classification where one was missing. It does not gate, reject, or short-circuit any path. The only capabilities affected are those that previously would have been labeled `unknown`; they now get a more specific `user` label.
|
|
32
|
+
|
|
33
|
+
Hooks already pre-assigned `instar` by the scanner (`hooks/instar/` subdir) would be overwritten by a naive fallback, so the guard `cap.provenance === 'unknown'` explicitly preserves them.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Under-block
|
|
38
|
+
|
|
39
|
+
**What failure modes does this still miss?**
|
|
40
|
+
|
|
41
|
+
- Capabilities legitimately sourced from neither the bundle, an evolution proposal, nor the agent's own config — e.g. a third-party drop-in dir if one ever exists — would also be labeled `user`. Today no such source exists; the scanners only reach paths under `projectDir/.claude/` and `stateDir/hooks/`, both agent-owned. If a future scanner adds a truly "external" source, it must pre-assign a non-`unknown` provenance to avoid mislabeling.
|
|
42
|
+
- Already-persisted `unknown` entries in each agent's on-disk `capability-manifest.json` will continue to show `unknown` until the next `refresh()` runs. That's fine: drift detection already re-classifies on scan; no migration is required.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Level-of-abstraction fit
|
|
47
|
+
|
|
48
|
+
**Is this at the right layer?**
|
|
49
|
+
|
|
50
|
+
Yes. The classifier is the correct seam — it's the single chokepoint every capability passes through, and the `Provenance` type already names `'user'` as a first-class value. The alternative (adding `user` at each scanner) would duplicate the rule across six scanners and drift over time. The alternative (post-hoc in `buildMap`) would scatter classification across multiple layers.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 4. Signal vs authority compliance
|
|
55
|
+
|
|
56
|
+
**Required reference:** `docs/signal-vs-authority.md`
|
|
57
|
+
|
|
58
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
59
|
+
|
|
60
|
+
- [x] No. This change has zero blocking authority. It produces a *label* on a read-only classification path. No gating, no rejection, no downstream authority decision hangs off the new label; the drift endpoint and summary table consume the label for display only.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 5. Interactions
|
|
65
|
+
|
|
66
|
+
- **Manifest persistence** — the new label is written to `capability-manifest.json`. Persisted entries that previously read `provenance: 'unknown'` will, on the next scan, persist as `provenance: 'user'`. The drift detector's `changed` list will surface these as a one-time provenance transition per agent. That's expected and visible in the drift report; it's the signal that the fix took effect.
|
|
67
|
+
- **Summary counters** — `buildMap()` already counts `user`-provenance capabilities into `userConfigured`. That counter was 0 for all agents pre-fix; it now reflects reality. `unmapped` drops correspondingly.
|
|
68
|
+
- **Test coupling** — one existing test (`reports unmapped capabilities (unknown provenance)`) asserted the old behavior and has been renamed and updated. All 208 tests across the three capability-mapper test files still pass.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 6. Revert cost
|
|
73
|
+
|
|
74
|
+
Single-commit revert. Two tiny edits in `CapabilityMapper.ts` plus one test update. No schema, migration, config, or API contract changed. Persisted manifests with `user` provenance would be re-labeled `unknown` on the next post-revert scan — fully reversible.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 7. Justification for shipping now (vs. deferring)
|
|
79
|
+
|
|
80
|
+
The cluster has sat with governance=`implement` and concrete research notes pointing at the exact line of code. The fix is two small conditional edits; the risk of shipping is strictly lower than the continued noise of ~100+ agent-local capabilities being flagged as "unmapped" across every drift report on every agent. Deferring would keep the drift signal polluted and continue to show `userConfigured: 0` in summaries that should show a much larger number.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Side-Effects Review — Telegram markdown renderer (PR1: formatter module, disabled)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `telegram-markdown-renderer-pr1`
|
|
4
|
+
**Date:** `2026-04-24`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (no block/allow surface, no wiring into send paths, shipped disabled)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
PR1 of the two-PR plan in `docs/specs/TELEGRAM-MARKDOWN-RENDERER-SPEC.md` (approved 2026-04-24 by Justin via Telegram topic 8183). Adds a new pure-function module `src/messaging/TelegramMarkdownFormatter.ts` that ports Dawn's `telegram_format.py` (the-portal) to TypeScript, plus a Vitest test suite `tests/unit/telegram-markdown-formatter.test.ts` with 105 passing tests.
|
|
11
|
+
|
|
12
|
+
The module exports `formatForTelegram(text, mode)`, `format(text, mode)`, `lintTelegramMarkdown(text)`, and primitives (`escapeHtmlText`, `escapeHtmlAttribute`, `isSafeUrl`). It implements the spec's full 12-step markdown→Telegram-HTML pipeline, a balanced-paren URL scanner (Wikipedia-style parens), WHATWG URL scheme allowlist, distinct HTML-text vs HTML-attribute escapers, NUL + Supplementary-PUA-B stripping for sentinel-collision safety, a 32KB input guard that falls back to plain mode without byte loss (`conversionSkipped: true`), and a `legacy-passthrough` mode that returns input byte-for-byte unchanged with `parseMode: undefined` so callers retain their historical `parse_mode`.
|
|
13
|
+
|
|
14
|
+
**Critically, no send path is wired to this module.** `TelegramAdapter.ts` and `TelegramLifeline.ts` are untouched. The module is dead code at runtime until PR2 wires `apiCall()` behind the `telegramFormatMode` config accessor. PR2 ships with `legacy-passthrough` as the default, so PR1 has zero behavioral effect on any agent even after PR2 merges.
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
This PR contains no runtime decision points — the module is not called by anything. The future decision points it enables (documented here for completeness; all are PR2 scope):
|
|
19
|
+
|
|
20
|
+
- `TelegramAdapter.apiCall` — formatter invocation conditional on `method === 'sendMessage' || method === 'editMessageText'`. **Not in this PR.**
|
|
21
|
+
- `TelegramLifeline.apiCall` — same. **Not in this PR.**
|
|
22
|
+
- Trusted-internal-caller allowlist for `html` mode. **Not in this PR.**
|
|
23
|
+
|
|
24
|
+
Decision points inside the module itself (pure functions, no runtime authority):
|
|
25
|
+
|
|
26
|
+
- `isSafeUrl(raw)` — scheme allowlist (http/https/tg/mailto). Pure function; refuses unsafe schemes by returning `null`. Caller decides what to do with the rejection (emit literal text).
|
|
27
|
+
- `lintTelegramMarkdown(text)` — pure detector returning string array. Advisory-only; no blocking authority in this PR.
|
|
28
|
+
- 32KB length guard in `formatForTelegram` — falls back to plain mode on oversized input. Pure transformation; no network/storage side effect.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
35
|
+
|
|
36
|
+
No block/allow surface in PR1 — module is not wired. Future-facing concerns the module itself introduces:
|
|
37
|
+
|
|
38
|
+
- `legacy-passthrough` mode emits no lint issues (by design — it's the rollback target and must be behaviorally identical to pre-cutover). A future operator might want lint-as-observation in passthrough mode; noted as open question for PR2.
|
|
39
|
+
- `isSafeUrl` rejects `javascript:`, `data:`, `file:`, `vbscript:`, and any non-allowlisted scheme. Rejected links become literal text (`[click](javascript:...)` rendered verbatim) — a legitimate agent-authored `tg://resolve?domain=foo` link is permitted (mode allowed). Wikipedia-style `https://...wiki/X_(y)` URLs parse correctly via the balanced-paren scanner (covered by fixture test).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Under-block
|
|
44
|
+
|
|
45
|
+
**What failure modes does this still miss?**
|
|
46
|
+
|
|
47
|
+
No block/allow surface in PR1. Module-internal known gaps (documented in spec, accepted as v1):
|
|
48
|
+
|
|
49
|
+
- Italic between emoji (`🎉*bold*🎉`) renders literal asterisks because the punctuation-class lookaround doesn't include emoji — accepted v1 per spec iteration-2 L4.
|
|
50
|
+
- `tg://` deep links are permitted (spec allowlists them); future spec may tighten.
|
|
51
|
+
- PUA-B range is stripped from input before sentinels are inserted — this means a user message legitimately containing PUA-B codepoints will lose those bytes. Near-zero real-world usage; accepted trade-off per spec. Not flagged as `truncated` because "byte loss" in the spec is reserved for the oversized-`<pre>` fallback.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. Level-of-abstraction fit
|
|
56
|
+
|
|
57
|
+
**Is this at the right layer?**
|
|
58
|
+
|
|
59
|
+
Yes. The module is a pure string-transform library at the same layer as `MessageFormatter.ts`. It owns no I/O, no config reading, no network, no logging. It does not hold blocking authority. The decision "should this send happen at all?" stays with the adapter/lifeline chokepoints; the formatter only answers "given this text and mode, what bytes go on the wire?"
|
|
60
|
+
|
|
61
|
+
This is consistent with the spec's architectural intent: the formatter is a detector+transformer producing `{ text, parseMode, lintIssues }`, consumed downstream by the adapter which holds the authoritative send/no-send decision.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 4. Signal vs authority compliance
|
|
66
|
+
|
|
67
|
+
Per `docs/signal-vs-authority.md`:
|
|
68
|
+
|
|
69
|
+
- **Lint is a signal, not an authority.** `lintTelegramMarkdown` returns canonical prose-string issues. It does not reject a send. The spec specifies lint-strict mode (which a downstream authority can consume to return 422) as PR2 scope; PR1's lint is purely observational.
|
|
70
|
+
- **Scheme allowlist is a deterministic transform, not a policy decision.** `isSafeUrl` is brittle-by-design (WHATWG URL + scheme set). It does not block a send — it causes a link to render as literal text. The outbound send still happens. This satisfies the "brittle logic must not hold blocking authority" rule: brittle logic (regex/parse) produces a transformation, not a decision to refuse a user action.
|
|
71
|
+
- **No sentinel, gate, watchdog, or sentinel-class name is introduced by this PR.**
|
|
72
|
+
|
|
73
|
+
Compliance: PASS.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 5. Interactions
|
|
78
|
+
|
|
79
|
+
- No shadow/shadowed interactions in PR1 — module is not called by anything. Search confirms no existing import site.
|
|
80
|
+
- Module name collision check: `src/messaging/` contains `MessageFormatter.ts` (different purpose: envelope formatting) and no `TelegramMarkdownFormatter.ts` prior to this PR. No collision.
|
|
81
|
+
- Test file name: `tests/unit/telegram-markdown-formatter.test.ts`. No existing test file of that name.
|
|
82
|
+
- PR2 will introduce interactions with `TelegramAdapter.apiCall`, `TelegramLifeline.apiCall`, `MessageStore` (raw/sent fields), and `GitSyncTransport` (envelope flag). Those interactions get their own artifact at PR2 time.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 6. External surfaces
|
|
87
|
+
|
|
88
|
+
**Does it change anything visible to other agents, other users, other systems?**
|
|
89
|
+
|
|
90
|
+
No. PR1 ships dead code. No route changes, no message format changes on the wire, no config schema changes applied (config fields `telegramFormatMode` / `telegramLintStrict` are spec'd but deliberately not added in PR1 — they land in PR2 alongside the wiring), no database changes, no envelope changes.
|
|
91
|
+
|
|
92
|
+
- Bot API: untouched — adapter still sends exactly the bytes it sends today with exactly the `parse_mode` it uses today.
|
|
93
|
+
- MessageStore: untouched.
|
|
94
|
+
- GitSyncTransport: untouched.
|
|
95
|
+
- Shell-script `.claude/scripts/telegram-reply.sh`: untouched.
|
|
96
|
+
- Agent dashboards / self-knowledge: untouched.
|
|
97
|
+
|
|
98
|
+
No timing dependencies, no conversation state dependencies.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 7. Rollback cost
|
|
103
|
+
|
|
104
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
105
|
+
|
|
106
|
+
Trivial. PR1 is a code-only addition with no runtime invocation. Rollback options in order of cost:
|
|
107
|
+
|
|
108
|
+
1. **Do nothing.** The module is not on any call path; a latent bug is invisible until PR2.
|
|
109
|
+
2. **Revert the merge commit.** Standard `git revert` — affects only two new files. No data migration, no agent state repair, no config flip.
|
|
110
|
+
3. **Fix forward.** The module is pure functions with 105 unit tests; most fixes ship as deltas.
|
|
111
|
+
|
|
112
|
+
PR2 itself is governed by the spec's staged-rollout (ships with `legacy-passthrough` default; canary via config flip). That's PR2's rollback story — out of scope for this artifact.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Second-pass review
|
|
117
|
+
|
|
118
|
+
Not required. PR1 has no block/allow surface, no session-lifecycle surface, no sentinel/gate/watchdog, no coherence/idempotency/trust change, and is shipped disabled. High-risk criteria from Phase 5 are not met.
|
|
119
|
+
|
|
120
|
+
## Spec linkage
|
|
121
|
+
|
|
122
|
+
- Approved spec: `docs/specs/TELEGRAM-MARKDOWN-RENDERER-SPEC.md` (approved 2026-04-24T20:53:47Z by Justin via Telegram topic 8183).
|
|
123
|
+
- Convergence report: `docs/specs/reports/telegram-markdown-renderer-convergence.md`.
|
|
124
|
+
- Dawn reference impl: `the-portal/.claude/scripts/telegram_format.py` (merged 2026-04-24T17:57Z).
|
|
File without changes
|