switchroom 0.19.19 → 0.19.23
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/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +121 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +2995 -1583
- package/dist/host-control/main.js +122 -2
- package/dist/vault/approvals/kernel-server.js +124 -4
- package/dist/vault/broker/server.js +124 -4
- package/package.json +7 -4
- package/profiles/_base/start.sh.hbs +101 -0
- package/profiles/_shared/agent-self-service.md.hbs +64 -109
- package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
- package/profiles/_shared/dev-protocol.md.hbs +13 -42
- package/profiles/_shared/execution-discipline.md.hbs +7 -14
- package/profiles/coding/CLAUDE.md.hbs +0 -6
- package/profiles/default/CLAUDE.md.hbs +21 -50
- package/skills/dev-protocol/SKILL.md +90 -107
- package/skills/switchroom-release/SKILL.md +103 -20
- package/telegram-plugin/bunfig.toml +10 -0
- package/telegram-plugin/card-format.ts +92 -3
- package/telegram-plugin/dist/gateway/gateway.js +873 -184
- package/telegram-plugin/edit-flood-fuse.ts +477 -0
- package/telegram-plugin/format.ts +19 -7
- package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
- package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
- package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
- package/telegram-plugin/gateway/gateway.ts +75 -63
- package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
- package/telegram-plugin/gateway/narrative-lane.ts +49 -3
- package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
- package/telegram-plugin/gateway/status-pin-api.ts +145 -0
- package/telegram-plugin/gateway/stream-render.ts +6 -0
- package/telegram-plugin/gateway/turn-record-status.ts +19 -0
- package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
- package/telegram-plugin/retry-api-call.ts +15 -2
- package/telegram-plugin/send-gate.ts +1 -1
- package/telegram-plugin/status-no-truncate.ts +64 -1
- package/telegram-plugin/status-pin-driver.ts +50 -27
- package/telegram-plugin/status-pin.ts +43 -5
- package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
- package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
- package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
- package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
- package/telegram-plugin/tests/status-pin.test.ts +106 -5
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -10
- package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
- package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
- package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
- package/telegram-plugin/tool-activity-summary.ts +85 -13
- package/telegram-plugin/worker-activity-feed.ts +5 -1
- package/vendor/hindsight-memory/scripts/drain_pending.py +304 -34
- package/vendor/hindsight-memory/scripts/lib/pending.py +886 -70
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +71 -13
- package/vendor/hindsight-memory/scripts/recall.py +74 -5
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1602 -14
- package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +93 -13
- package/vendor/hindsight-memory/tests/test_drain_pending.py +44 -3
- package/vendor/hindsight-memory/tests/test_pending.py +12 -4
|
@@ -13,51 +13,14 @@ description: >
|
|
|
13
13
|
|
|
14
14
|
# Development Protocol — the playbook
|
|
15
15
|
|
|
16
|
-
The always-loaded CLAUDE.md "Development Protocol" section
|
|
17
|
-
This skill
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **Read the real source.** The repo's source files at the current HEAD — not
|
|
25
|
-
build artifacts (`dist/`, generated files, caches), not your memory of the
|
|
26
|
-
code, not the task description's paraphrase. If a claim matters, open the
|
|
27
|
-
file.
|
|
28
|
-
- **Verify the root cause, not the first plausible cause.** Reproduce or trace
|
|
29
|
-
the failure to a specific mechanism before fixing. "This line looks wrong"
|
|
30
|
-
is a lead; the fix ships only when you can say *why* it produced the
|
|
31
|
-
observed symptom.
|
|
32
|
-
- **Report contradicting evidence.** If what you find contradicts the task
|
|
33
|
-
description, the ticket, or your own working theory — say so explicitly and
|
|
34
|
-
stop to re-plan. Never force-fit evidence to the plan you already had.
|
|
35
|
-
- **Cite everything.** Claims about the codebase carry `file:line`, commit
|
|
36
|
-
hashes, or PR numbers. "The scaffold appends fragments at
|
|
37
|
-
`src/agents/scaffold.ts:4113`" is a claim; "the scaffold appends fragments
|
|
38
|
-
somewhere" is not.
|
|
39
|
-
|
|
40
|
-
## 2. Clarify vs proceed
|
|
41
|
-
|
|
42
|
-
- **Infer first.** Most questions are answerable from the codebase, git
|
|
43
|
-
history, existing tests, or docs. Exhaust those before asking.
|
|
44
|
-
- **One question at a time.** If genuinely unsure after inferring, ask the
|
|
45
|
-
single question whose answer unblocks the most work. Phrase it as a
|
|
46
|
-
decision with a default: state what you found, the 2–3 viable options, which
|
|
47
|
-
you'd pick and why, and ask for confirmation — e.g. *"The config loader
|
|
48
|
-
supports both YAML and JSON overlays; the task says 'config file' without
|
|
49
|
-
specifying. I'd extend the YAML path since all existing overlays are YAML
|
|
50
|
-
(src/config/merge.ts:88) — confirm, or should JSON be covered too?"* Never
|
|
51
|
-
send a questionnaire.
|
|
52
|
-
- **Phase discipline.** Clarify during *planning*. Once the plan is agreed,
|
|
53
|
-
execute autonomously: make the reasonable call on small ambiguities, record
|
|
54
|
-
the assumption in your report, and keep moving. Mid-execution questions are
|
|
55
|
-
reserved for discoveries that invalidate the plan.
|
|
56
|
-
|
|
57
|
-
## 3. Design-align on larger tasks
|
|
58
|
-
|
|
59
|
-
**Classify the task first.** Treat it as "larger" (design-align before
|
|
60
|
-
implementing) when ANY of these hold:
|
|
16
|
+
The always-loaded CLAUDE.md "Development Protocol" section carries the
|
|
17
|
+
judgement criteria. This skill carries the parts that are *this fleet's
|
|
18
|
+
specific opinion* — the ones you would get wrong by defaulting to generic
|
|
19
|
+
good practice, because our answer differs from the obvious one.
|
|
20
|
+
|
|
21
|
+
## 1. Is this a "larger" task? (decides whether you design-align first)
|
|
22
|
+
|
|
23
|
+
Treat it as larger — design report before implementing — when ANY of these hold:
|
|
61
24
|
|
|
62
25
|
- It changes a public interface, schema, config shape, or on-disk format.
|
|
63
26
|
- It cuts across 3+ modules or touches a load-bearing invariant.
|
|
@@ -67,65 +30,85 @@ implementing) when ANY of these hold:
|
|
|
67
30
|
("add flag Y").
|
|
68
31
|
- It will land as more than one PR.
|
|
69
32
|
|
|
70
|
-
Small, single-concern, obvious-approach changes skip straight to
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- **
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
33
|
+
Small, single-concern, obvious-approach changes skip straight to the pipeline.
|
|
34
|
+
Design-aligning a one-liner is its own failure mode.
|
|
35
|
+
|
|
36
|
+
## 2. Design report + red-team (larger tasks only)
|
|
37
|
+
|
|
38
|
+
The report states what exists today **with citations**, what will change, the
|
|
39
|
+
chosen approach, the alternatives you rejected and why, and the PR staging
|
|
40
|
+
plan. Get alignment before implementing.
|
|
41
|
+
|
|
42
|
+
Then red-team your own plan item by item. Each item gets a verdict —
|
|
43
|
+
`SOUND`, `RISK`, or `WRONG` — backed by evidence you can point at (a file you
|
|
44
|
+
read, a test you ran, documented behaviour), not intuition. Fix every `WRONG`
|
|
45
|
+
and address every `RISK` before starting. A red-team that returns all-`SOUND`
|
|
46
|
+
on a non-trivial plan is a red-team you didn't actually do.
|
|
47
|
+
|
|
48
|
+
## 3. Ask one question, as a decision with a default
|
|
49
|
+
|
|
50
|
+
If you're genuinely blocked after inferring from code and history, don't send
|
|
51
|
+
a questionnaire. State what you found, the 2–3 viable options, which you'd
|
|
52
|
+
pick and why, and ask for confirmation:
|
|
53
|
+
|
|
54
|
+
> "The config loader supports both YAML and JSON overlays; the task says
|
|
55
|
+
> 'config file' without specifying. I'd extend the YAML path since all
|
|
56
|
+
> existing overlays are YAML (`src/config/merge.ts:88`) — confirm, or should
|
|
57
|
+
> JSON be covered too?"
|
|
58
|
+
|
|
59
|
+
Clarify during *planning*. Once the plan is agreed, execute autonomously:
|
|
60
|
+
make the reasonable call on small ambiguities, record the assumption in your
|
|
61
|
+
report, keep moving. Mid-execution questions are reserved for discoveries
|
|
62
|
+
that invalidate the plan.
|
|
63
|
+
|
|
64
|
+
## 4. Adversarial review — bounded on purpose
|
|
65
|
+
|
|
66
|
+
Dispatch the review to a **fresh** pass or sub-agent; the coder cannot review
|
|
67
|
+
its own work in-context. Structure it:
|
|
68
|
+
|
|
69
|
+
- **Input:** the full diff, the task statement, and the design report if one exists.
|
|
70
|
+
- **Charge:** *find reasons this change is wrong* — correctness, missed edge
|
|
71
|
+
cases, untested behaviour, inconsistency with surrounding code, docs drift,
|
|
72
|
+
security or data-loss risk.
|
|
73
|
+
- **Output:** a findings list, each with a severity, the evidence (`file:line`),
|
|
74
|
+
and a concrete fix.
|
|
75
|
+
|
|
76
|
+
**The severity gate — this is the fleet-specific part.** The old rule was
|
|
77
|
+
"fix ALL findings including lows, then re-review". That is a loop generator by
|
|
78
|
+
construction: an adversarial reviewer always surfaces lows (that is its job),
|
|
79
|
+
fixing lows produces a new diff, and a new diff earned another re-review. It
|
|
80
|
+
produced PRs going four rounds where the last round's only finding was an
|
|
81
|
+
inaccurate doc comment. So:
|
|
82
|
+
|
|
83
|
+
- **Blockers and mediums block the merge.** Fix them.
|
|
84
|
+
- **Lows do NOT block.** Fix a low inline only if it's a genuine one-liner;
|
|
85
|
+
otherwise **file a follow-up issue and merge**. Filing is mandatory — an
|
|
86
|
+
unfiled low is a dropped bug, and there is no human team to catch it later.
|
|
87
|
+
- **A re-review is earned only by a behavioural fix.** A docs-, comment-,
|
|
88
|
+
log-, or test-only fix commit does not earn another pass.
|
|
89
|
+
- **Two rounds is the cap.** Prefix every commit answering a review round
|
|
90
|
+
`review-fix:` — `scripts/check-review-rounds.mjs` counts them and the
|
|
91
|
+
`review-rounds` check fails past two, unless a `review-cap-override` label
|
|
92
|
+
is on the PR.
|
|
93
|
+
|
|
94
|
+
If a finding is genuinely invalid, rebut it with evidence in writing; silence
|
|
95
|
+
is not a rebuttal.
|
|
96
|
+
|
|
97
|
+
When you do re-review, the verdict states per original finding: the finding
|
|
98
|
+
ID, what changed (`file:line` of the fix), whether it fully resolves the
|
|
99
|
+
finding (`RESOLVED` / `PARTIAL` / `REBUTTED` with evidence), and whether the
|
|
100
|
+
fix introduced anything new. A bare "fixed" is not a verdict.
|
|
101
|
+
|
|
102
|
+
## 5. Non-obvious pipeline rules
|
|
103
|
+
|
|
104
|
+
- **CI is the full-suite authority.** Local scoped tests are a fast filter,
|
|
105
|
+
never the merge evidence. Never claim done off a local run alone.
|
|
106
|
+
- **A test that wouldn't fail on the bug it guards is not a test.** Assert the
|
|
107
|
+
observable outcome, not that the code path executed.
|
|
108
|
+
- **Prefer a deterministic mechanism over prompt discipline.** If a check, a
|
|
109
|
+
hook, a schema, or a lint gate can enforce the guarantee, write that instead
|
|
110
|
+
of a convention — conventions demonstrably fail here.
|
|
111
|
+
- **Never go dark, and never foreground-watch over 30 seconds.** Builds, CI
|
|
112
|
+
waits, and deploys run in the background with a notification. Batch related
|
|
113
|
+
findings into one substantive update rather than five fragments. Cap
|
|
114
|
+
parallel sub-agents at 15.
|
|
@@ -12,7 +12,9 @@ Cut a release of `switchroom/switchroom` and get it live on the fleet. This is a
|
|
|
12
12
|
|
|
13
13
|
- A release = a `vX.Y.Z` **git tag** on `main` (the merge commit of the CHANGELOG PR).
|
|
14
14
|
- The tag is the version source of truth (`scripts/build.mjs:resolveVersion()`). `package.json` `version` is a **stale placeholder by design** — never bump it in a commit (the `//version` comment + #2733 discipline). The uncommitted pack-time bump happens in CI now, not by hand.
|
|
15
|
-
- Cutting the tag fires
|
|
15
|
+
- Cutting the tag fires **two** workflows: `docker-images` (builds + pushes the 6 ghcr images) and `release` (the **orchestrator** — builds the four static binaries, attaches them, waits for `docker-images`, then calls `npm-publish`, then takes the GitHub Release out of draft).
|
|
16
|
+
- **`npm-publish.yml` no longer has a tag trigger (#3654).** It is reachable only via `workflow_call` from `release.yml` and via `workflow_dispatch`. npm is the one leg that cannot be undone, so it runs last and only once everything else is green. It also re-proves both preconditions from inside its own run, so a hand dispatch cannot bypass the ordering either.
|
|
17
|
+
- **The GitHub Release is created as a DRAFT and stays one until every leg is green.** `install.sh` resolves the version to install from `/releases/latest`, and that endpoint excludes drafts — so a half-finished release is invisible to `curl | sh` users and the previous complete release keeps serving them. This is not theoretical: v0.19.19 shipped published-with-zero-assets and broke the installer on every platform.
|
|
16
18
|
|
|
17
19
|
## Before you start — pre-flight (verify, don't assume)
|
|
18
20
|
|
|
@@ -28,30 +30,97 @@ Cut a release of `switchroom/switchroom` and get it live on the fleet. This is a
|
|
|
28
30
|
- The release commit touches **CHANGELOG.md only**. Do NOT bump `package.json` (placeholder discipline).
|
|
29
31
|
- Branch protection blocks direct push to `main`, so: create a `release/vX.Y.Z` branch, push it, open a `chore: release vX.Y.Z` PR (base `main`), arm auto-merge (squash, delete-branch) on green CI.
|
|
30
32
|
|
|
31
|
-
## Step 2 —
|
|
33
|
+
## Step 2 — Create the DRAFT release on a PINNED SHA, then push the tag
|
|
32
34
|
|
|
33
|
-
Once the changelog PR is merged
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Once the changelog PR is merged. **Order matters, and so does the pin.**
|
|
36
|
+
|
|
37
|
+
### 2a — Resolve and PIN the commit
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git fetch origin
|
|
41
|
+
SHA="$(git rev-parse origin/main)"
|
|
42
|
+
git --no-pager log -1 --oneline "$SHA" # show the operator exactly what is being released
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Never pass `--target main`.** `main` is resolved server-side at the moment the API call lands, and agents merge PRs in parallel here — a PR that merged in the seconds between your pre-flight check and your `gh release create` would be silently swallowed into the release. Resolve `$SHA` once, confirm it is the commit you inspected in pre-flight, and use that literal SHA everywhere below.
|
|
46
|
+
|
|
47
|
+
### 2b — Create the release as a DRAFT
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
gh release create "vX.Y.Z" -R switchroom/switchroom \
|
|
51
|
+
--draft \
|
|
52
|
+
--target "$SHA" \
|
|
53
|
+
--title 'vX.Y.Z — <summary>' \
|
|
54
|
+
--notes-file <notes-file>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **`--draft` is mandatory.** A published release with no assets immediately becomes `/releases/latest` and 404s every `curl | sh` install for the entire ~25-minute build window. `release.yml` will forcibly re-draft an incomplete published release within about a minute, but do not rely on the safety net — it exists for the case where this step was done wrong.
|
|
58
|
+
- Creating the release object *before* the tag is what removes the race: `release.yml`'s `guard` job needs it to exist within ~150s of the tag push.
|
|
37
59
|
- **Notes extraction gotcha (historical):** the naive `awk '/^## vX/,/^## v/' CHANGELOG` range collapses to a single line. Use a start-flag awk: `awk 'f{print} /^## vX\.Y\.Z/{print; f=1} f && /^## v/ && !/^## vX\.Y\.Z/{exit}'` — or extract the section to a temp file by line range.
|
|
38
|
-
- **`gh release create` has been silently dropped in past runs.**
|
|
60
|
+
- **`gh release create` has been silently dropped in past runs.** Verify it exists and is a draft:
|
|
61
|
+
```bash
|
|
62
|
+
gh release view vX.Y.Z -R switchroom/switchroom --json tagName,isDraft
|
|
63
|
+
```
|
|
64
|
+
`gh` resolves drafts by tag name (it falls back to scanning the release list). Note the raw REST `GET /releases/tags/{tag}` does **not** — it 404s on a draft. That difference is why the workflow scripts use the list endpoint; don't "fix" them to use the by-tag endpoint.
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
### 2c — Push the tag at that same pinned SHA
|
|
41
67
|
|
|
42
|
-
|
|
68
|
+
GitHub does not create the tag ref for a *draft* release (it creates it on publish), so the tag push below is what actually starts the pipeline. Confirm that before pushing, because if the ref already existed at `$SHA` the push would be a silent no-op and **no workflow would fire**:
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
```bash
|
|
71
|
+
git ls-remote --tags origin "refs/tags/vX.Y.Z" # expect NO output
|
|
72
|
+
git push origin "$SHA:refs/tags/vX.Y.Z"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Pushing the SHA-to-ref form rather than `git tag && git push --tags` guarantees the tag lands on the commit you pinned in 2a, not on whatever your local `main` happens to be. (If the ref *does* already exist at a different commit, the push is rejected — that is the safe direction. Do not force it; work out why first.)
|
|
76
|
+
|
|
77
|
+
Then confirm both workflows actually started before you walk away:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
gh run list -R switchroom/switchroom --branch vX.Y.Z --limit 5
|
|
81
|
+
```
|
|
48
82
|
|
|
49
|
-
|
|
83
|
+
You should see a `docker-images` run and a `release` run. If you see neither, the tag ref already existed and nothing fired.
|
|
84
|
+
|
|
85
|
+
## Step 3 — Wait for the pipeline (two workflows, one of them orchestrated)
|
|
86
|
+
|
|
87
|
+
The tag push triggers `docker-images` and `release`. `release` internally waits for `docker-images`, then publishes to npm, then un-drafts the GitHub Release. **Do not proceed to rollout until both are green AND verified.**
|
|
88
|
+
|
|
89
|
+
### Gate A — docker images (`docker-images.yml`)
|
|
50
90
|
- `gh run list --workflow=docker-images.yml --limit 1` — wait for `completed` / `success`.
|
|
51
|
-
- Verify all 6 images are published
|
|
52
|
-
|
|
91
|
+
- Verify all 6 images are published. The repository name is `switchroom-<name>`, **not** `<name>` — `.github/workflows/docker-images.yml` builds `${REGISTRY}/${IMAGE_NAMESPACE}/switchroom-${{ matrix.image.name }}`, so `ghcr.io/switchroom/agent` does not exist and returns `manifest unknown`. Check each of:
|
|
92
|
+
```bash
|
|
93
|
+
for n in agent auth-broker kernel broker web hostd; do
|
|
94
|
+
docker manifest inspect "ghcr.io/switchroom/switchroom-$n:vX.Y.Z" >/dev/null \
|
|
95
|
+
&& echo "OK switchroom-$n" || echo "MISS switchroom-$n"
|
|
96
|
+
done
|
|
97
|
+
```
|
|
98
|
+
Each must resolve.
|
|
99
|
+
- If any image is missing: do NOT roll — the rollout canary version-assert fails on an unpublished tag. Wait + re-check. `release` will block on this by itself, so a red image build means npm never publishes and the release never leaves draft. That is the design.
|
|
100
|
+
|
|
101
|
+
### Gate B — the release pipeline (`release.yml`)
|
|
102
|
+
- `gh run list --workflow=release.yml --limit 1` — wait for `completed` / `success`. Expect ~25-30 minutes: four native build legs plus the wait on `docker-images`.
|
|
103
|
+
- Its jobs, in order: `guard` (release exists + held out of `latest`) → `build` ×4 → `bundle` → `publish` (attach) → `images-gate` (wait on docker-images) → `npm` → `finalize` (un-draft). A red job anywhere leaves the release a **draft** and npm **unpublished** — which is the correct, recoverable state.
|
|
104
|
+
- Verify the release page actually has assets **and is no longer a draft**:
|
|
105
|
+
```bash
|
|
106
|
+
gh release view vX.Y.Z -R switchroom/switchroom --json isDraft,assets \
|
|
107
|
+
--jq '{isDraft, assets: [.assets[].name]}'
|
|
108
|
+
```
|
|
109
|
+
`isDraft` must be `false`, and `assets` must list all four binaries (`switchroom-{linux,macos}-{amd64,arm64}`) **and** `switchroom-checksums.txt`. This is the gate that did not exist through v0.19.19 — every release up to then shipped **zero** assets and the advertised `curl | sh` installer (`install.sh`) was dead on every platform (#3633).
|
|
110
|
+
- Confirm the installer's own resolution path agrees:
|
|
111
|
+
```bash
|
|
112
|
+
gh api repos/switchroom/switchroom/releases/latest --jq '{tag_name, assets: [.assets[].name]}'
|
|
113
|
+
```
|
|
114
|
+
This is literally what `install.sh` calls. If it still reports the previous version, `finalize` did not run.
|
|
115
|
+
|
|
116
|
+
### Gate C — npm publish
|
|
117
|
+
- npm is published by the `npm` job **inside** the `release` run, not by a separate workflow run. `gh run list --workflow=npm-publish.yml` will show nothing new for a normal release — that is expected, not a failure.
|
|
118
|
+
- Verify the publish is live: `npm view switchroom version` must return `X.Y.Z` (not the old version). Retry a few times — npm registry propagation can lag a few seconds.
|
|
119
|
+
|
|
120
|
+
**Only when Gates A, B AND C are green + verified** do you proceed.
|
|
53
121
|
|
|
54
|
-
|
|
122
|
+
### Rehearsing a workflow change without releasing anything
|
|
123
|
+
`gh workflow run release.yml --ref <branch>` — `dry_run` defaults to `true`, so it builds, checksums and verifies the full bundle and attaches it as a workflow artifact, touching no GitHub Release, no npm, and no image tags. This is the only supported way to prove a change to the release pipeline before a real tag.
|
|
55
124
|
|
|
56
125
|
## Step 4 — Fleet rollout (operator-gated, canary-first)
|
|
57
126
|
|
|
@@ -63,14 +132,28 @@ The tag push triggers `docker-images` AND `npm-publish` in parallel. **Do not pr
|
|
|
63
132
|
|
|
64
133
|
- **Never bump `package.json` `version` in a commit.** It's a stale placeholder; the tag is the source of truth and `npm-publish.yml` does the uncommitted pack-time bump.
|
|
65
134
|
- **Never run `npm publish` by hand from the agent container.** You can't reach the operator's npm auth, and the workflow is the reliable path. If the workflow is broken, fix the workflow — don't side-step it.
|
|
66
|
-
- **Never
|
|
135
|
+
- **Never create the GitHub Release without `--draft`,** and **never `gh release create --target main`.** Pin the SHA (step 2a).
|
|
136
|
+
- **Never take the release out of draft by hand** while the pipeline is still running. `finalize` is the only thing that should publish it; un-drafting early puts an incomplete release on `/releases/latest` and breaks every installer.
|
|
137
|
+
- **Never roll the fleet before Gates A, B AND C are green + verified.** A release that's on the fleet but not on npm is the exact regression this skill exists to prevent.
|
|
67
138
|
- **Never push directly to `main`.** The CHANGELOG PR goes through auto-merge on green.
|
|
68
139
|
- **Never force-push `main` or bypass hooks (`--no-verify`).**
|
|
69
140
|
|
|
70
141
|
## If something goes wrong
|
|
71
142
|
|
|
72
|
-
|
|
73
|
-
|
|
143
|
+
**The single recovery command for almost everything is a re-dispatch of the orchestrator:**
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
gh workflow run release.yml -R switchroom/switchroom --ref vX.Y.Z -f dry_run=false
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
It re-runs every leg — including `finalize`, which is what actually takes the release out of draft. Re-running one leg on its own generally leaves the release stuck as a draft.
|
|
150
|
+
|
|
151
|
+
**But a re-dispatch cannot fix a defect that lives in the tag's own tree.** `workflow_dispatch` executes the workflow YAML **at the dispatched ref**, so `--ref vX.Y.Z` re-runs the same broken file and fails identically, every time. This is not theoretical — it is how v0.19.20 died (#3691). Before reaching for the re-dispatch, root-cause the failure in the workflow source **at that tag** (`git show vX.Y.Z:.github/workflows/release.yml`) and compare it to `main`. If the bug is in the tag's tree, the only path is a fix merged to `main` plus a **fresh tag**; abandon the old tag as a permanent draft (do not delete it, do not hand-publish it) and burn a patch version. Re-dispatch is for *transient* failures — a flaky runner, an npm 5xx, a `docker-images` run that has since gone green.
|
|
152
|
+
|
|
153
|
+
- **`release` failed at `guard` ("no GitHub Release exists"):** step 2b was skipped or the tag name is misspelled. Create the draft release, then re-dispatch as above.
|
|
154
|
+
- **`release` failed at `images-gate`:** `docker-images` was not green for this tag+commit. Fix it, `gh workflow run docker-images.yml --ref vX.Y.Z`, wait for green, then re-dispatch `release.yml`. Nothing was published to npm and the release is still a draft — nothing to undo.
|
|
155
|
+
- **`release` failed at `npm` (e.g. a transient npm 5xx):** re-dispatch `release.yml` as above. `npm-publish` treats "already published" as success, so a re-run is safe and idempotent. Dispatching `npm-publish.yml --ref vX.Y.Z` directly also works and its own gates still apply, but it will NOT un-draft the release, so you would then have to re-dispatch `release.yml` anyway.
|
|
156
|
+
- **Everything is green but the release is still a draft:** `finalize` did not run. Check `gh run view <run-id>` for a skipped job, then re-dispatch. Do not hand-publish — `finalize` re-verifies the asset set immediately before flipping the flag.
|
|
74
157
|
- **Rollout started before publish verified (the old bug):** abort the rollout, publish, then re-roll. Do not let a half-published release sit on the fleet.
|
|
75
158
|
|
|
76
159
|
## Operator one-time setup (tell them once, not every release)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# bun test configuration for runs whose CWD is telegram-plugin/ — CI's
|
|
2
|
+
# `bun-test-run` job (scripts/bun-test-ci.sh) and `bun run --cwd telegram-plugin`.
|
|
3
|
+
#
|
|
4
|
+
# bun reads the bunfig.toml in its CWD only, so this file exists purely to load
|
|
5
|
+
# the same state-dir hermeticity preload as the repo-root bunfig.toml. Keep the
|
|
6
|
+
# two in sync; `npm run lint:agent-state-dir-hermeticity` fails if either stops
|
|
7
|
+
# loading the guard. Rationale for the guard itself lives in
|
|
8
|
+
# tests/vitest-setup/agent-state-dir-guard.mjs.
|
|
9
|
+
[test]
|
|
10
|
+
preload = ["../tests/vitest-setup/agent-state-dir-guard.mjs"]
|
|
@@ -38,6 +38,69 @@ import { normalizeDashes } from './text-voice-scrub.js'
|
|
|
38
38
|
*/
|
|
39
39
|
export { escapeMarkdown, codeSpanSafe }
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Separator appended to every card line that is followed by a hard break when
|
|
43
|
+
* `stackCardLines` runs in `collapseSafe` mode (#3666).
|
|
44
|
+
*
|
|
45
|
+
* It is a NO-BREAK SPACE (U+00A0), chosen deliberately:
|
|
46
|
+
*
|
|
47
|
+
* - It is a real character in the message text, so it survives Telegram's
|
|
48
|
+
* pinned-bar collapse (which drops the `\n` and substitutes nothing) and
|
|
49
|
+
* keeps the two lines' glyphs apart in the one-line preview.
|
|
50
|
+
* - It is NOT ASCII whitespace, so neither this module's own
|
|
51
|
+
* trailing-whitespace strip nor the GFM parser's hard-break handling
|
|
52
|
+
* (which consumes only the ASCII spaces immediately before the newline)
|
|
53
|
+
* can eat it.
|
|
54
|
+
* - It is invisible where the card is normally read — the chat feed — since
|
|
55
|
+
* it lands at end-of-line. That is the whole reason a separator with
|
|
56
|
+
* visible ink (`' ·'`, `' —'`) was rejected: it would put dangling
|
|
57
|
+
* punctuation on every line of every pinned card to fix a defect that only
|
|
58
|
+
* manifests on the pin bar.
|
|
59
|
+
*
|
|
60
|
+
* If a real-pin eyeball shows a single space is too weak a break, this constant
|
|
61
|
+
* is the one place to strengthen it (e.g. `' ·'`) — the seam is deliberate.
|
|
62
|
+
*/
|
|
63
|
+
export const COLLAPSE_SAFE_SEPARATOR = '\u00A0'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalise a line's tail before `stackCardLines` re-terminates it.
|
|
67
|
+
*
|
|
68
|
+
* Two strips, both there for the SAME reason: this function must be idempotent,
|
|
69
|
+
* so re-stacking an already-stacked body can never accumulate terminator
|
|
70
|
+
* characters.
|
|
71
|
+
*
|
|
72
|
+
* - trailing ASCII whitespace, so exactly one ` \n` hard break is emitted;
|
|
73
|
+
* - in `collapseSafe` mode, any `COLLAPSE_SAFE_SEPARATOR` run the line
|
|
74
|
+
* already carries (interleaved with ASCII whitespace, since a caller may
|
|
75
|
+
* have split the body on `\n` rather than on ` \n`), so exactly one
|
|
76
|
+
* separator is emitted.
|
|
77
|
+
*
|
|
78
|
+
* The separator strip is written against the CONSTANT, not against a hardcoded
|
|
79
|
+
* U+00A0, so it stays correct if the separator is ever strengthened to visible
|
|
80
|
+
* ink. That one-constant seam is what COLLAPSE_SAFE_SEPARATOR's own doc comment
|
|
81
|
+
* promises, and it would be a lie if this strip did not follow it.
|
|
82
|
+
*/
|
|
83
|
+
function normalizeLineTail(line: string, collapseSafe: boolean): string {
|
|
84
|
+
let out = line.replace(/[ \t\r]+$/, '')
|
|
85
|
+
// The length guard is not defensive noise: `''.endsWith('')` is true and
|
|
86
|
+
// `''.slice(0, -0)` is `''`, so an empty separator would spin this loop
|
|
87
|
+
// forever — a hang in the gateway's render path. The doc comment above the
|
|
88
|
+
// constant invites future edits to it, so make that edit unable to hang.
|
|
89
|
+
if (!collapseSafe || COLLAPSE_SAFE_SEPARATOR.length === 0) return out
|
|
90
|
+
while (out.endsWith(COLLAPSE_SAFE_SEPARATOR)) {
|
|
91
|
+
out = out.slice(0, -COLLAPSE_SAFE_SEPARATOR.length).replace(/[ \t\r]+$/, '')
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Options for `stackCardLines`. */
|
|
97
|
+
export interface StackCardLinesOpts {
|
|
98
|
+
/** Append `COLLAPSE_SAFE_SEPARATOR` to each hard-broken line so the stack
|
|
99
|
+
* stays readable on a surface that collapses it to one line (#3666).
|
|
100
|
+
* Opt-in: only pinned cards pay the (tiny) extra character per line. */
|
|
101
|
+
collapseSafe?: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
41
104
|
/**
|
|
42
105
|
* Join a card's pre-rendered, single-line entries so they STACK in the
|
|
43
106
|
* Bot API 10.1 rich-message renderer (#2669) — the same visual result the
|
|
@@ -63,8 +126,19 @@ export { escapeMarkdown, codeSpanSafe }
|
|
|
63
126
|
* This is the card-surface analogue of the reply path's
|
|
64
127
|
* `normalizeParagraphBreaks`: it guarantees a card authored as stacked
|
|
65
128
|
* bullet/step lines renders identically to a normal reply.
|
|
129
|
+
*
|
|
130
|
+
* `opts.collapseSafe` (#3666) additionally makes the stack survive being
|
|
131
|
+
* rendered on a surface that COLLAPSES the message to one line — Telegram's
|
|
132
|
+
* pinned-message bar drops the newlines and substitutes nothing, mashing the
|
|
133
|
+
* last glyph of each line into the first glyph of the next
|
|
134
|
+
* (`… · opus 5✓ Reading gateway.ts→ Running search …`). Opt-in, because it is
|
|
135
|
+
* only correct for cards that are actually pinned; see COLLAPSE_SAFE_SEPARATOR.
|
|
66
136
|
*/
|
|
67
|
-
export function stackCardLines(
|
|
137
|
+
export function stackCardLines(
|
|
138
|
+
lines: string[],
|
|
139
|
+
opts?: StackCardLinesOpts,
|
|
140
|
+
): string {
|
|
141
|
+
const collapseSafe = opts?.collapseSafe === true
|
|
68
142
|
const pieces: string[] = []
|
|
69
143
|
for (let i = 0; i < lines.length; i++) {
|
|
70
144
|
const line = lines[i]
|
|
@@ -75,12 +149,27 @@ export function stackCardLines(lines: string[]): string {
|
|
|
75
149
|
// A blank current or next line is a genuine `\n\n` paragraph gap — leave
|
|
76
150
|
// the separator a plain newline so the blank entry reconstructs the gap.
|
|
77
151
|
if (cur === '' || next === '') {
|
|
152
|
+
// The gap still needs a collapse separator when THIS line has content:
|
|
153
|
+
// a collapsing surface drops the `\n\n` with no substitute exactly as it
|
|
154
|
+
// drops a ` \n`, and the blank entry is the empty string, so it
|
|
155
|
+
// contributes NO character of its own to hold the two sides apart. Only
|
|
156
|
+
// when `cur` is blank is the separator skipped — appending it to a blank
|
|
157
|
+
// entry would turn the gap into a visible U+00A0 paragraph in the FEED.
|
|
158
|
+
if (collapseSafe && cur !== '') {
|
|
159
|
+
pieces[pieces.length - 1] = normalizeLineTail(line, true) + COLLAPSE_SAFE_SEPARATOR
|
|
160
|
+
}
|
|
78
161
|
pieces.push('\n')
|
|
79
162
|
continue
|
|
80
163
|
}
|
|
81
164
|
// Strip any trailing whitespace the line already carried so we emit
|
|
82
|
-
// exactly one ` \n` hard break (never accumulate spaces on a re-run)
|
|
83
|
-
|
|
165
|
+
// exactly one ` \n` hard break (never accumulate spaces on a re-run) and,
|
|
166
|
+
// under collapseSafe, exactly one separator — see normalizeLineTail.
|
|
167
|
+
// The collapse separator is appended AFTER the strip (it is not ASCII
|
|
168
|
+
// whitespace, so it survives both this strip and the markdown parser's
|
|
169
|
+
// own trailing-whitespace handling) and BEFORE the two hard-break spaces,
|
|
170
|
+
// which stay immediately adjacent to the `\n` so the break still parses.
|
|
171
|
+
pieces[pieces.length - 1] =
|
|
172
|
+
normalizeLineTail(line, collapseSafe) + (collapseSafe ? COLLAPSE_SAFE_SEPARATOR : '')
|
|
84
173
|
pieces.push(' \n')
|
|
85
174
|
}
|
|
86
175
|
return pieces.join('')
|