@caddis/cli 0.4.13 → 0.4.15

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 (28) hide show
  1. package/bundles/antigravity-plugin/caddis_statusline.py +30 -3
  2. package/bundles/antigravity-plugin/plugin.json +1 -1
  3. package/bundles/antigravity-plugin/repo_freshness.py +146 -0
  4. package/bundles/antigravity-plugin/scripts/caddis-run.ps1 +11 -0
  5. package/bundles/antigravity-plugin/scripts/caddis-run.sh +9 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_gate.py +24 -3
  7. package/bundles/antigravity-plugin/scripts/caddis_governor.py +542 -0
  8. package/bundles/antigravity-plugin/scripts/caddis_host_guard.py +689 -0
  9. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +1632 -0
  10. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +30 -3
  11. package/bundles/antigravity-plugin/scripts/install-host-guard.ps1 +393 -0
  12. package/bundles/antigravity-plugin/skills/_registry.md +2 -0
  13. package/bundles/antigravity-plugin/skills/confluence-hla/SKILL.md +375 -0
  14. package/bundles/antigravity-plugin/skills/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
  15. package/bundles/antigravity-plugin/skills/confluence-hla/scripts/discover_db_lineage.py +970 -0
  16. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +30 -4
  17. package/bundles/antigravity-plugin/skills/implement/SKILL.md +48 -1
  18. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +155 -0
  19. package/bundles/antigravity-plugin/warm_start_agy.py +38 -4
  20. package/bundles/codex/.codex/skills/docs/confluence-hla/SKILL.md +375 -0
  21. package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
  22. package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/discover_db_lineage.py +970 -0
  23. package/bundles/manifest.json +2 -2
  24. package/dist/claude-deepseek.js +50 -10
  25. package/dist/claude-glm.js +50 -10
  26. package/dist/claude-oss.js +50 -10
  27. package/dist/cli.js +173 -63
  28. package/package.json +1 -1
@@ -126,6 +126,24 @@ A tier can be served by an OSS provider instead of Anthropic (cheat sheet:
126
126
  `claude-glm -p "/caddis:implement <plan-path> — Phase N only"` (from the repo root, on the
127
127
  feature branch). The local-coder gate below exists exactly so these phases survive a weaker
128
128
  implementer — a phase may only take this lane if it passes that gate.
129
+ - **codex-sol / codex-astra / agy / glm (via caddis_lanes)** — select one of these five task types
130
+ and its default chain:
131
+ - `backend`: `glm`, `codex-sol`, `claude`
132
+ - `hard`: `claude`, `codex-astra`
133
+ - `ui`: `claude`, `codex-astra`
134
+ - `tests`: `codex-sol`, `glm`, `claude`
135
+ - `docs`: `agy`, `glm`, `claude`
136
+ Use `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_lanes.py" run --plan <plan-path> --phase N
137
+ --type <type>` as the launch command. The chains are defaults, not limits on Claude: while Claude
138
+ has budget, give any phase the `claude` lane. Override chains and model pins per project in
139
+ `.caddis/config.toml` `[lanes.chains]` and `[lanes.models]`.
140
+
141
+ **Evidence standard — never plan a load test on a shared host.** Prove timing robustness with
142
+ event-driven fakes, ordering assertions and an A/B check that fails against the racy version. A phase
143
+ must never ask for CPU burners, stress tests, load tests or "every core". `caddis_lanes.py` refuses
144
+ this wording. The host guard kills runaway agent processes. On 2026-09-13, 32 all-core busy loops
145
+ froze CI on the dev box.
146
+
129
147
  - **review lane (every phase that touches code)** — cross-vendor review by a vendor that did NOT
130
148
  write the code: `/caddis:cross-review --range main..HEAD`
131
149
  (Claude or GLM implemented) or `--provider glm` / a Claude `code-reviewer` pass (DeepSeek-adjacent
@@ -219,6 +237,7 @@ Creating Model: <model-id>
219
237
  **Milestone:** M1 *(omit on plans without milestones)*
220
238
  **Model:** <tier> (`/model <alias>`) — <one-line rationale tied to this phase's difficulty>
221
239
  **Lane:** claude — this session | glm-headless — `claude-glm -p "/caddis:implement <this plan's path> — Phase 1 only"`
240
+ **Type:** backend | hard | ui | tests | docs *(required when the Lane runs through caddis_lanes.py)*
222
241
  **Session:** continue | **fresh** (start a new session for this phase — note why: lane change / heavy context)
223
242
  **Risk:** high — token-verification seam; a wrong answer here is a silent auth bypass *(omit this line when normal)*
224
243
  **Goal:** <one sentence>
@@ -273,6 +292,11 @@ yourself (relay + Tracker) before stopping — do not hand back "run `/caddis:ha
273
292
  - **Ship gate:** NEVER push or merge to main from a phase — phases only commit to the feature branch.
274
293
  When all phases are ✅: `/caddis:ship-pr` (stops at green CI), then STOP and wait for the human's
275
294
  explicit go before `/caddis:ship-merge`.
295
+ - **Finish:** after the last phase, run `caddis_lanes.py finish --plan <plan>`; the plan is not done
296
+ until it prints `LEFTOVERS: 0` (merged lane worktrees and branches removed, unmerged or dirty ones
297
+ reported, feature branch pushed; merging to main stays with `/caddis:ship-merge`).
298
+ - **No load wording** — no phase asks for a CPU burner, stress test, load test or "every core"; prove
299
+ timing by design.
276
300
 
277
301
  ## Affected files
278
302
  | File | Action |
@@ -303,16 +327,18 @@ done:
303
327
  - **No abbreviation** — never "etc.", "similar to Phase 1", "and so on". Write every item in full.
304
328
  - **Model tier named** — every phase names a tier (cheap/mid/frontier/ultra) + a one-line rationale; no
305
329
  phase silently defaults to the most expensive model. Default mid; frontier/ultra only where justified.
306
- - **Lane + reviewer named** — every phase names its execution lane (`claude` or `glm-headless`, with
307
- the literal launch command for glm phases) and a cross-review provider that is a different vendor
308
- than the implementer. A glm-headless phase MUST pass every bullet of this gate — that lane has no
309
- slack for reasoning out gaps.
330
+ - **Lane + reviewer named** — every phase names its execution lane (`claude`, `glm-headless`, or a
331
+ `caddis_lanes.py` lane with its `**Type:**`, with the literal launch command for every non-claude
332
+ phase) and a cross-review provider that is a different vendor than the implementer. A glm-headless
333
+ phase MUST pass every bullet of this gate — that lane has no slack for reasoning out gaps.
310
334
  - **Launch command is spawnable** — every `glm-headless` phase's command carries **`-p`** (or
311
335
  `--print`) and ends with **`— Phase N only`**. Both are load-bearing, not style: the launchers set
312
336
  `CADDIS_HEADLESS` *only* on `-p`, and that variable is the guard that stops a spawned session
313
337
  spawning another one forever; the `Phase N only` suffix is what stops the child running to the end
314
338
  of the plan. The command is executed verbatim by a model with no slack to notice a missing flag,
315
339
  and `/caddis:implement` now refuses a command without `-p` rather than running it.
340
+ - **caddis_lanes command carries --phase** — a `caddis_lanes.py` launch command must name `--phase N`
341
+ and `--type`; lane-check refuses one without `--phase`.
316
342
  - **Gate-detectable** — if this phase's instruction were WRONG, its exit gate would go red. A phase
317
343
  whose gates pass either way (copy, nav entries, badges, ordering, wording, config defaults) must
318
344
  not take a lane that cannot escalate, no matter how completely it is specified. Every other bullet
@@ -101,6 +101,51 @@ the bug the lane routing exists to fix.
101
101
  lane the plan planned. A deviation then shows up as a diff between the phase block and the tracker
102
102
  row, visible to any later reader without an Advisory Hub having to catch it live.
103
103
 
104
+ **Multi-lane phases — `caddis_lanes.py`.** Use this procedure when the phase launch command is
105
+ `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_lanes.py" run …`.
106
+
107
+ 1. In an interactive session, run the phase's literal command. The script creates a temporary
108
+ worktree. It runs the lane or its fallback chain inside a resource-capped job. It re-runs the
109
+ gates with the repo interpreter. It runs the DeepSeek review. It then prints a `NEXT:` line.
110
+ 2. Obey `NEXT:`:
111
+ - `review` → read the packet JSON and inspect `git -C <worktree> diff`. Then decide ACCEPT or REDO.
112
+ - `redo` → run `abandon`. Then run `run` with `--attempt 2 --findings <file with your findings>`.
113
+ - `takeover` → run `abandon`. Then implement the phase yourself on the `claude` lane.
114
+ - `claude-implement` → write the code in the printed worktree. Then run `check`.
115
+ - `stop` → report the result and halt. `stop (host guard killed the lane…)` means
116
+ `FAILED-RESOURCE`. The lane saturated the host. Never re-run it with more load. Fix the phase
117
+ so it proves timing by design.
118
+ 3. **ACCEPT** means run `caddis_lanes.py accept --plan <plan> --phase N --attempt K --message
119
+ "<the phase's commit message>"`. It runs only on the programme branch, never on `main`. It
120
+ refuses a `BLOCKING` review. If you checked the finding and it is false, add
121
+ `--override-review "<why the finding is false>"`. The script records that reason in the packet.
122
+ 4. Allow at most **2 attempts** per phase on a non-Claude lane. An out-of-budget switch is not an
123
+ attempt. The owner can request TAKEOVER at any time.
124
+ 5. A UI phase needs a `ui-design-reviewer` screenshot review before ACCEPT. This rule applies
125
+ whichever lane wrote the phase.
126
+ 6. A phase that Claude wrote needs a vendor review that is not MISSING. If `review.status` is
127
+ `MISSING`, run `codex exec -m gpt-5.6-sol -s read-only -C <worktree> --skip-git-repo-check -`.
128
+ Give the diff to the command on stdin.
129
+ 6b. **Orchestrator artefacts live under `.caddis/orchestrator/`.** Store packets and locks in
130
+ `lane-runs/`. Store evidence notes as markdown directly in `.caddis/orchestrator/`. Follow its
131
+ `AGENTS.md`. Never write orchestration notes to `.caddis/kb/`.
132
+ 7. **Never write a load or stress test into a lane prompt or findings file.** The script refuses
133
+ this wording without `--allow-load`. Only the owner gives that flag.
134
+ 8. Record these Tracker values:
135
+ - `Model` = the packet's `model`.
136
+ - `Lane` = the lanes tried, joined with ` -> ` and followed by the reason. For example,
137
+ `glm -> codex-sol (glm out of budget)`.
138
+ - `Cross-review` = `CLEAN`, `BLOCKING` or `missing`.
139
+ - `Notes` = `attempt K, ACCEPT|TAKEOVER|FAILED-RESOURCE`.
140
+ 9. The PR body lists every phase accepted with `review.status == MISSING`. It also lists every phase
141
+ accepted while Codex astra stood in for Claude.
142
+ 9b. **Finish.** After the last phase, run `caddis_lanes.py finish --plan <plan>`. It removes merged
143
+ lane worktrees and branches. It reports unmerged or dirty ones. It pushes the feature branch.
144
+ The plan is not done until it prints `LEFTOVERS: 0`. Then run `/caddis:ship-pr`. Merging and
145
+ post-merge cleanup stay with `/caddis:ship-merge` after the owner's go.
146
+ 10. **When Claude is out of budget**, the owner opens `caddis-run codex -m gpt-6-astra` and asks it
147
+ to follow this same subsection.
148
+
104
149
  **Advisory-Hub mode (conditional — OFF by default).** Check whether a companion file
105
150
  `<plan-dir>/<plan-stem>-advisory-context.md` exists (e.g. plan `.caddis/plans/foo.md` → look for
106
151
  `.caddis/plans/foo-advisory-context.md`). **If it does not exist — the normal case, including every
@@ -187,7 +232,9 @@ report — do not redo completed work.
187
232
  first, every phase, not just the one you resumed at. Step 1's check only ever saw the *resume*
188
233
  phase: on a plan whose phase 1 is `claude` and phase 2 is `glm-headless`, it finds no mismatch and
189
234
  this loop then carries you straight through phase 2 — so the lane silently executes nothing for
190
- every phase after the first, which is the original bug wearing a different hat.
235
+ every phase after the first, which is the original bug wearing a different hat. For a
236
+ `caddis_lanes.py` launch command, follow "Multi-lane phases" above instead of spawning the command
237
+ blind.
191
238
  - **RED** — write the failing test(s) the phase names (`<test file>::<case>`). Run them; confirm they
192
239
  fail for the right reason (the missing behavior, not an import error). If a phase genuinely has no
193
240
  testable surface, say so in the review file and implement the minimal change directly.
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: pr-merge
3
+ description: End-to-end pull request and merge lane — push feature branch, open or update PR, monitor CI to green, merge to default branch, and clean up the feature branch.
4
+ ---
5
+
6
+ # /pr-merge — the unified reviewed lane: branch → PR → green → merged → cleaned up
7
+
8
+ End-to-end delivery of feature-branch work through the reviewed lane in one shot: run preflight checks,
9
+ push safely, create or update the Pull Request, monitor CI checks to green, merge into the default
10
+ branch, validate the deployment, and clean up the feature branch.
11
+
12
+ For a hotfix straight to the default branch with no PR ceremony, use `/ship` (the express lane) instead.
13
+ To open a PR and stop at green for human review without merging, use `/ship-pr`.
14
+ To merge an already-opened PR, use `/ship-merge`.
15
+
16
+ Optional argument: **$ARGUMENTS** (PR title override; default = derived from the branch's
17
+ conventional-commit summary). An optional strategy override (`squash` | `merge` | `rebase`)
18
+ can be provided.
19
+
20
+ ## Step 0 — Detect the lane (before anything else)
21
+
22
+ - **Gitea lane** — `.gitea/workflows/` exists. PR creation, CI monitoring, and merge via the Gitea API
23
+ (token loading and job-polling procedure: the `deploy-local` skill).
24
+ - **GitHub lane** — `.github/workflows/` exists (and no `.gitea/`). PR creation, checks monitoring,
25
+ and merge via the `gh` CLI (`gh pr create`, `gh pr checks --watch`, `gh pr merge` — see the `gh-cli` skill).
26
+ - **Local-only lane** — neither exists, or there is no push remote. There is no forge and no PR concept:
27
+ **refuse early**, tell the user to use `/ship`, and stop. Do not fabricate a PR flow.
28
+
29
+ Read `AGENTS.md` + the detected workflow file(s) first for the repo's real gate names, default
30
+ branch, and merge conventions — never assume `main`, never assume job names.
31
+
32
+ ## Step 1 — Refuse on the default branch
33
+
34
+ A PR is *branch → default*. If HEAD **is** the default branch, stop and point at `/ship` — there is
35
+ nothing to open a PR from. (Offer to create a feature branch from the current work if the changes
36
+ are uncommitted.)
37
+
38
+ ## Step 2 — Preflight (local gates mirroring CI)
39
+
40
+ - Backend: `ruff check .` clean (or repository linter).
41
+ - Frontend: `npm run typecheck && npm run build` if `frontend/` exists.
42
+ - Fix failures first. **Never open a PR over red preflight** — a PR that arrives red wastes CI resources
43
+ and reviewer time.
44
+
45
+ ## Step 3 — Scope-guard pre-check (before the PR exists)
46
+
47
+ - If the repo has a **named scope-guard workflow** (e.g. a Gitea `pr-scope-guard` that blocks PRs
48
+ mixing infra `.gitea/`/`.github/` changes with product `src/`/`frontend/`/`tests/` changes): read
49
+ the workflow's own rule and check the branch diff against exactly that rule **before** creating
50
+ the PR. On a violation, **refuse with the specific offending files** — do not let the guard fail
51
+ the PR after the fact.
52
+ - Without a named guard, apply the generic infra-vs-product mixed-diff heuristic as a **warning
53
+ only** — surface it and proceed (repos without a guard allow mixed PRs by construction).
54
+
55
+ ## Step 3b — Cross-review trigger (before anything is pushed)
56
+
57
+ <!-- shared:cross-review-trigger — keep byte-identical across commands; a test pins it -->
58
+ **Run the trigger, do not eyeball the diff:**
59
+
60
+ ```bash
61
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-trigger --range <base>...HEAD
62
+ ```
63
+
64
+ Exit **0** — say nothing, carry on. Exit **2** — it prints which rule fired and on which files.
65
+ Run `/caddis:cross-review`, show the findings, and let the user decide. It **never blocks**: this
66
+ picks when to ask for a second opinion, it is not a verdict, and a review gate that blocks a ship
67
+ gets switched off inside a week.
68
+
69
+ It fires on SQL and repositories, `services/`, caches and refresh jobs, auth and RBAC, or a diff
70
+ over 400 changed lines. No judgement in any of those — deliberately, because judgement is exactly
71
+ what failed.
72
+ <!-- /shared:cross-review-trigger -->
73
+
74
+ ## Step 4 — Rebase-safe currency
75
+
76
+ - If the branch is **behind** the default branch, offer to rebase onto it (stale branches are how
77
+ merges rot). Only when behind — never rebase gratuitously.
78
+ - If the branch was already pushed, a rebase rewrites history. **Snapshot first, then push safely:**
79
+ ```
80
+ git branch backup/<branch>-preship # recovery ref BEFORE any rewrite
81
+ git rebase <default-branch>
82
+ git push --force-with-lease origin <branch> # never bare --force
83
+ ```
84
+
85
+ ## Step 5 — Push + create or update the PR
86
+
87
+ - Push the branch to remote (plain push if no rewrite happened).
88
+ - Create the PR: title from the branch's conventional-commit summary (or `$ARGUMENTS`); body = the
89
+ commit list + a "generated by /pr-merge" note.
90
+ - **Idempotent:** if a PR for this branch **already exists**, reuse it, update its title/body, and
91
+ report it — never error, never open a duplicate.
92
+
93
+ ## Step 6 — Monitor PR CI checks
94
+
95
+ Watch the PR's checks job-by-job until completion:
96
+ - Gitea lane: poll the run for the PR's head SHA via the API (`deploy-local` skill procedure).
97
+ - GitHub lane: `gh pr checks <pr> --watch` / `gh run watch` (`gh-cli` skill).
98
+ - If any check fails (red check): report the failure, classify the cause, and **stop before merging**.
99
+ Do not merge a failing PR. Apply the minimum source fix and re-push.
100
+
101
+ ## Step 7 — Merge to default branch
102
+
103
+ Once all required checks are **green**, there are no merge conflicts, and review requirements are satisfied:
104
+ - Use the repo's configured **merge strategy** — read it from repo settings (Gitea repo settings API /
105
+ `gh repo view --json squashMergeAllowed,mergeCommitAllowed,rebaseMergeAllowed`) and `AGENTS.md`;
106
+ an explicit argument wins. Options: **squash**, **merge-commit**, rebase-merge. **Don't assume** —
107
+ if detection is inconclusive, use merge-commit (never silently squash someone's history without evidence).
108
+ - Execute merge:
109
+ - GitHub: `gh pr merge <pr> --<strategy>` (e.g. `gh pr merge <pr> --squash`).
110
+ - Gitea: call the Gitea PR merge API with the chosen strategy.
111
+ - Record the merge SHA.
112
+
113
+ ## Step 8 — Monitor deployment & validate (if applicable)
114
+
115
+ If the default branch triggers an automated deployment pipeline:
116
+ - Gitea: `lint_and_test` → `deploy_prod` → `release_metadata` (`deploy-local` skill).
117
+ - GitHub: `gh run watch <run-id> --exit-status` on the default branch. If CI-only, note that no deploy occurs.
118
+ - Validate health and deployed SHA if a live service is deployed. If deploy validation fails, report
119
+ immediately and **skip cleanup** (Step 9).
120
+
121
+ ## Step 9 — Close feature-branch lifecycle (Clean up)
122
+
123
+ **Only after** the merge is complete and deployment (if any) is validated green:
124
+ - Switch to the default branch: `git switch <default-branch>`.
125
+ - Fast-forward to the merge commit: `git pull --ff-only origin <default-branch>`.
126
+ - Delete the **local** feature branch: `git branch -d <branch>` (use `-D` only if squashed after verifying tree contents match).
127
+ - Delete the **remote** feature branch: `git push origin --delete <branch>` (if not deleted by forge).
128
+ - Drop the temporary `backup/<branch>-preship` ref if one was created in Step 4.
129
+
130
+ If deploy validation failed: **skip cleanup** entirely and report why — never delete a branch whose
131
+ work may need re-work.
132
+
133
+ ## Step 10 — Report
134
+
135
+ ```
136
+ PR: #<n> <title> — merged (<strategy>) as <merge-sha>
137
+ Checks: <check ✓ per check>
138
+ Deploy: <job ✓ / ✗ per job | n/a>
139
+ Prod SHA: matches merge ✓ | n/a
140
+ Cleanup: remote branch deleted / local branch deleted / backup ref dropped | SKIPPED (<why>)
141
+ ```
142
+
143
+ ## Rules
144
+
145
+ - Never merge a PR whose required checks are red or absent.
146
+ - Never delete a feature branch before its merge is confirmed and any deploy validated.
147
+ - Never `git add -A` without reviewing `git status` first (stage named files only).
148
+ - Never force-push without a `backup/*-preship` recovery ref; force pushes are `--force-with-lease` only.
149
+ - Never edit a workflow file (`.gitea/workflows/`, `.github/workflows/`) to make a gate pass — fix the source.
150
+ - Local-only lane: refuse + point at `/ship`. On the default branch: stop + point at `/ship`.
151
+
152
+ ## Skill reference
153
+
154
+ Gitea API monitoring, deploy tracking, failure triage: the `deploy-local` skill (ships in optional `caddis-extras`).
155
+ GitHub `gh pr create`, `gh pr checks`, `gh pr merge`: the `gh-cli` skill.
@@ -50,6 +50,29 @@ RELAY_FRAME_FOOTER = "=== end session-context ==="
50
50
 
51
51
  _TRUTHY = {"1", "true", "yes", "on"}
52
52
 
53
+ # The freshness report (fetch, then say how far this checkout is from the remote default
54
+ # branch) is the ONE piece here that is imported rather than duplicated: the agy bundle ships
55
+ # hooks/repo_freshness.py beside this file, listed in runtime-targets.json. In the source repo
56
+ # it sits one directory over, so both layouts resolve. Fail open — an older bundle without it
57
+ # simply reports no freshness.
58
+ try:
59
+ _HOOKS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "hooks")
60
+ if os.path.isdir(_HOOKS_DIR) and _HOOKS_DIR not in sys.path:
61
+ sys.path.append(_HOOKS_DIR)
62
+ import repo_freshness # noqa: E402
63
+ except Exception:
64
+ repo_freshness = None # type: ignore[assignment]
65
+
66
+
67
+ def freshness_lines(root: str) -> list[str]:
68
+ """Where `root` stands against the remote default branch; [] on any problem."""
69
+ if repo_freshness is None:
70
+ return []
71
+ try:
72
+ return repo_freshness.report(root)
73
+ except Exception:
74
+ return []
75
+
53
76
 
54
77
  def is_headless() -> bool:
55
78
  """True when no human is watching — a run given its task explicitly needs no resume pointer."""
@@ -268,11 +291,22 @@ def main() -> None:
268
291
  if conversation:
269
292
  _mark_injected(root, conversation)
270
293
 
271
- if not blocks:
294
+ # agy has no SessionStart event, so this first invocation is also where the freshness
295
+ # report belongs. It is emitted even when there is no relay to inject, and it stays
296
+ # silent in a repo that is clean, level and on the default branch.
297
+ fresh: list[str] = []
298
+ for root in roots:
299
+ fresh.extend(freshness_lines(root))
300
+
301
+ if not blocks and not fresh:
272
302
  return
273
- message = (RELAY_FRAME_HEADER + "\n" + "\n\n".join(blocks)
274
- + "\n\n" + RELAY_FRAME_FOOTER)
275
- payload = json.dumps({"injectSteps": [{"ephemeralMessage": message}]})
303
+ parts = []
304
+ if blocks:
305
+ parts.append(RELAY_FRAME_HEADER + "\n" + "\n\n".join(blocks)
306
+ + "\n\n" + RELAY_FRAME_FOOTER)
307
+ if fresh:
308
+ parts.append("\n".join(fresh))
309
+ payload = json.dumps({"injectSteps": [{"ephemeralMessage": "\n\n".join(parts)}]})
276
310
  sys.stdout.write(payload)
277
311
  except Exception:
278
312
  return # fail open: no injection is always better than a broken invocation