Conversation
…ts box (#2640) Two defects, one report. **The layout jumped.** `Portal.vue` toggled `<main>` between `flex-1` and `sm:flex-[2_1_0%]` on `voiceCall.active` and swapped `PortalRail` for `PortalVoiceCanvas` in the same frame. Nothing transitioned, so both columns landed at their new shares in one paint, and End call jumped back. `flex-grow` is a `<number>` and therefore animatable, so the share now transitions — 300 ms ease-out on `<main>`, and the canvas column ramps its own grow 0 → 3 over the same curve. The ramp is expressed as Vue enter/leave classes rather than a class toggle because a newly inserted element has no value to transition FROM. Opacity rides the same transition so the canvas's content is not re-wrapping in view while the column is still moving. `motion-reduce:transition-none` on every transitioning element. The shares stay shares: reverting to `w-[40%]` / `w-[60%]` would animate just as well and re-open #2581, where those summed to 100% + an 18rem sidebar and the shell clipped the canvas column off the right edge. Two consequences, both deliberate: * The `v-if` / `v-else-if` chain is gone — a `<Transition>` wrapper breaks the adjacency a chain needs. The exclusivity it guaranteed by construction is now a named computed both arms read, so they cannot drift into both claiming the column. * The rail waits for the canvas to finish leaving. Vue keeps a leaving element in the DOM for its transition; without the gate the rail would mount at full fixed width beside a canvas that is still shrinking — three columns in a row sized for two, `<main>` squeezed by flex for 300 ms, a worse jump than the one being fixed. **The orb rendered squashed.** `VoiceOverlay.vue::resizeCanvas` sized the bitmap ONCE, from the `watch(canvasEl)` that fires on mount — no ResizeObserver, no window listener, no per-frame check — while the canvas is `absolute inset-0 w-full h-full`. Every later width change left CSS stretching a stale bitmap into an ellipse, and the overlay mounts in the same tick the call re-lays out the columns, so the single measurement could capture the pre-call width on its own. It now observes both: a ResizeObserver for the box moving under a stable window (the column swap, a rail drag), and a window `resize` for a devicePixelRatio change, which resizes no box and so fires no observer. The bitmap is sized at `css × dpr` (capped at 2) and the render loop draws in CSS pixels via `ctx.setTransform`, so the 45px core and the particles' fixed radii keep meaning what they meant. Resizing re-scales and never re-seeds — the particle field is seeded once and lives in a fixed space around (0,0), so the orb does not restart when the column moves. A zero-sized box is ignored rather than throwing the last good size away, and a same-size measurement does not touch the bitmap, because assigning to `canvas.width` clears the canvas. Tests: `portalVoiceLayoutMotion.spec.js` (17) — the resize contract EXECUTED against a stub canvas whose box changes (DPR scaling, the cap, a missing DPR, the zero-box and same-size guards), mutation-checked by pinning the bitmap to its first measurement, which fails exactly the "FOLLOWS a changed box" case; plus the transition classes, the reduced-motion fallback counted over every transitioning element, and the leave gate. Two existing guards in `portalVoiceMode.spec.js` / `portalRail.spec.js` were rewritten to read the shared condition instead of the retired chain — the property they protect is exclusivity, not which construct expresses it. Frontend suite: 113 files, 2532 tests, all passing; both ratchets green; vite build clean. Related to #2640 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
…ent#535) `run_task` from a Workspace voice call went to the agent container's task endpoint: a stateless run with a 30s timeout, no thread, no memory of the conversation the person was in. In the issue's words, that is "what makes voice just chat today". A Workspace call now runs the turn through `portal_chat` — the SAME pipeline a typed message takes — into the thread the call is bound to, on the thread's own `cached_claude_session_id`. The agent has its skills, its files, its memory and its mid-work state, and the answer lands in that chat as a turn (and on the canvas if it drew). A call with no thread (VoIP, the legacy Agent Detail session) keeps the container path, because there is nothing to run it in. The split is `_is_workspace_bound`, which requires BOTH `portal_session_id` and `client_email`: a thread with no email cannot be attributed, an email with no thread has nowhere to land, and either alone would silently fall back to the container. **The spoken budget is not a cancellation.** Past `_SPOKEN_BUDGET_SECONDS` (20s) the model is told the work is still running and keeps the floor, while the turn CONTINUES and its reply lands in the chat. The old 30s `wait_for` cancelled it — throwing away work already done and paid for. The detached turn is strongly referenced so it cannot be collected mid-flight (the #1083 footgun), and it deliberately outlives the call: a turn the person asked for is worth landing whether or not they are still on the line. `_on_tool_result` fires when it lands, so a badge clears on the real event rather than on a timer. **The manifest is locked.** `services/voice_tools.py` owns the policy: resolved once at session start, `_build_live_config` builds the config FROM it, and the dispatcher refuses any name outside it before reading an argument. A per-agent declaration may only NARROW — `template.yaml` is agent-writable, so a declaration that could ADD would let an agent grant itself a capability by editing itself. Fleet tools are absent by construction: `PLATFORM_VOICE_TOOLS` is the only door a name enters through. Two defects found while building it, both fixed here: * the manifest was read as `session.tool_manifest or default`, so an agent declaring `voice.tools: []` — the strongest narrowing — fell through to the FULL platform set. The field is tri-state now: `None` is "never resolved" (→ the safe default), `frozenset()` is a decision. * `_execute_and_respond` read a nameless tool call as `run_task` (`getattr(fc, 'name', 'run_task')`), sending the model's arguments to the agent under a name nobody chose. It is refused now, and the refusal ANSWERS the call — a model that never receives a response for a call it made stops speaking. Scoped out, as decisions rather than omissions: * **the template-declared narrowing is mechanism-only.** `resolve_manifest` takes and honours a declaration, and `create_session` threads it — but nothing reads one yet, because `/api/template/info` does not expose a `voice` block. Adding it means an agent-server field plus a base-image rebuild, and a reader against a field no deployed agent returns would be a feature that reads as working and does nothing. * **the orb badge is backend-only.** The session counts in-flight turns and the landing fires `_on_tool_result`; rendering it is frontend work this PR does not do. `_execute_tool` keeps its original `(agent_name, …)` contract — the routing moved to the dispatcher, which already holds the session — so the existing container-path tests still exercise the real thing. Two panel tests move to `workspace_mode=True`: the canvas tools only exist in a workspace session, which is the combination the model could ever produce. 23 new tests, mutation-checked (a cancelling budget, a widening union, and a dropped refusal each turn the suite red). 1001 passed across the voice, canvas, portal and VoIP families. Related to Abilityai/trinity-enterprise#535 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rseded planning docs The migration's state lived across eleven planning/testing documents plus the tracker, and they had drifted apart. Five of them were finished work that was never retired: they were last touched on 2026-07-08, the day the Phase 0-3 PR merged, and their conclusions had already been folded into TARGET_ARCHITECTURE.md. Leaving them in the tree is not free. EFFECT_IDEMPOTENCY_DIRECTION_B_RETRY_CONTEXT.md is a *proposal* whose content was adopted into the spec; it frames the fail-closed execution_id policy as an open question, while the spec states it as settled. A reader who finds the proposal first concludes the policy is undecided and re-opens a closed decision. That happened, and it cost a full review cycle. Deleted (git retains them; nothing in tests, CI or scripts referenced any of them): PULL_PILOT_946_SOAK.md #946 closed; the decision record was never filled in - it is a blank scaffold EFFECT_IDEMPOTENCY_1084_REVIEW_NOTE.md #1084 closed; reviews Direction A, retired EFFECT_IDEMPOTENCY_DIRECTION_B_RETRY_CONTEXT adopted into TARGET_ARCHITECTURE.md (#1404) ORCHESTRATION_BUG_META_ANALYSIS_2026-06.md its own §5 is titled "applied 2026-06-06"; the spec header records the three changes TRANSACTIONAL_EXECUTIONS_2026-06.md June planning artifact, nothing points at it Archiving rather than deleting was considered and rejected: an archived file is still in the working tree, still grepped, still loaded into an agent's context, and still read by someone who does not scroll up to the "superseded" header - which is the exact failure mode above. Git history is the durable copy. PULL_MIGRATION_STATUS.md is rewritten as the entry point and verified against dev and the tracker. It was materially wrong: Phase 4 read "not started" (it is built and in review), #1401/#1402 read "not yet built" (closed in July), and it named a branch that no longer exists. It now also carries two things that were recorded nowhere - the trigger reach table (six of nine on dev, nine with Phase 4), and the behaviour the loop and fan-out rewrites change on installs with an EMPTY pilot allowlist, which is not flag-gated and is operator-visible. Corrected across the surviving docs: - The Phase-5 ">=2-week zero-orphan soak" bar was cited to #856 in four places. #856 sets no such bar - it is a closed spike on multi-agent fleet stability and does not mention duration, orphans or pull. The figure originates in #429's gating condition for #306 (the event bus, closed 2026-04-21) and was carried over. #1766's own criterion asks for "several days". Flagged for deliberate re-derivation rather than silently re-fixed. - T6.3 now states that the fail-closed policy is DECIDED in the spec and that #2392 is the build, not the decision. - M5 records the first end-to-end observation of lease expiry, re-delivery and poison-park outside the 2026-07-08 synthetic pilot (2026-09-09, local PG, forced fault): re-delivery preserved the execution id and recovered the work on a different worker, and killing on every claim walked the counter to the cap and parked it with a high-priority operator alert. Every production window so far has reported redelivery_count=0, which until now was indistinguishable from "untested". The five surviving reference docs get a banner pointing at the status file. Net: eleven documents to six, one entry point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Tm7UEkd4G9KQZD5oeLRSa
… step (#2640) Two review findings. 1. **Reduced motion was not instant.** Tailwind's `transition-none` emits only `transition-property: none` — the `duration-300` beside it still applies, so `transitionDuration` stays `.3s`. That is the exact property Vue's `getTransitionInfo` reads to size the fallback timer it resolves `@after-leave` on, so under `prefers-reduced-motion` nothing animated and every leave was still gated for 300ms: the canvas vanished, the right column sat empty, then the rail popped in. `motion-reduce:duration-0` on every transitioning element drives that timeout to 0. Verified against Tailwind's own output — the variant emits `transition-duration: 0s` inside the media query and after `duration-300`, so it wins. The comment on `voiceCanvasLeaving` claimed "`after-leave` fires immediately and this is never observably true", which was false as written; it now says what makes it true. Same correction in the feature flow. 2. **The rail column still steps** — accepted here, tracked at #2676. Its `<aside>` carries no width transition and it is a `shrink-0` flex sibling of `<main>`, so on call end it mounts at full width in one frame: 48px collapsed, 384px open, or the dragged `--ws-rail`, which on a wide rail is a bigger step than the 211px snap this PR removes. The honest fix is an explicitly animatable width for that column — CSS cannot transition to `auto` — and that width is owned by ent#492, not by the voice-call code. Doing it from here means either a wrapper element in the row or holding the rail mounted through a call, both of which want a browser to verify rather than the node-env source scan this suite is limited to. Recorded as a Known Limitation in the feature flow, with a test that fails if the limitation is deleted from the record or if the rail starts animating. Tests: the class-counting case now requires BOTH reduced-motion classes and asserts the ordering relation, since asserting the class string rather than the behaviour is exactly why (1) shipped green. 113 files / 2534 tests green. Fixes #2640 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSjVEjay9ztC1oDXXkh9uN
…on the session (ent#535) **The critical.** The Redis session blob omitted `tool_manifest`, so the cross-worker rebuild silently unlocked it: `get_session` passed nothing, `_session_manifest` read `None` as "never resolved", and the reconstruction handed the model the FULL platform default — in the `LiveConnectConfig` and in the dispatcher, with no log line, because from that worker's view nothing had ever been narrowed. Production runs `--workers 2` and the WebSocket routinely lands on a worker other than the one `/voice/start` ran on, so this is the normal path, not an edge case. The block comment directly above that dict says "EVERY field a reconstructed session decides on must be here"; this PR added a decision field and did not. It is stored as `sorted(...)`/`null` — `json.dumps` cannot serialize a set, so writing the frozenset raw would raise inside the try and lose the whole blob — and read back by `_manifest_from_meta`, which maps three inputs onto two answers: absent or `null` → unresolved (the pre-ent#535 default, and the mid-deploy case); a list, INCLUDING `[]` → a decision; anything else → unresolved rather than a crash in the audio loop. Collapsing `null` and `[]` would reintroduce the exact inversion this PR fixed. `test_create_session_writes_redis` asserted three named keys and never round-trip completeness, which is why this shipped green. It is now a SUBSET relation over the session's own dataclass fields, with the deliberately-unpersisted ones listed by reason, so a field added tomorrow fails the guard instead of shipping unpersisted. **Also folded in, all from the review:** * `_portal_turn` had 15 unreachable lines below its `return`, copy-pasted from `_execute_tool` and referencing names not in that scope. Removed, with an AST guard so nothing lands after that return again. * The empty-prompt guard is back on the chat path. `portal_chat` calls `_persist_user_turn` unconditionally, so a blank `run_task` durably wrote an empty user row into the person's Workspace thread and dispatched a real, cost-tracked execution; `required=["prompt"]` makes that unlikely, not impossible. Stripped rather than falsy, and worded identically on both paths. * `include_owned=True` is no longer a constant at the turn site. It travels on the session as `is_platform` (default False), written by `start_workspace_voice` — the function that refuses a non-platform caller, so the gate that authorizes the wider roster read is the one that records it. Sound today because that function is the only writer of `portal_session_id` + `client_email`; a future path setting both would otherwise widen `agent_on_roster` with no change at that line (Invariant #8). `canvas_audience` already travels for exactly this reason. * `docs/memory/requirements/runtimes.md` §29.7 (VOICE-007) rewritten — it still claimed a 30s timeout and `_execute_and_respond()` → `POST /chat`, both false for the Workspace path since this PR. AC 7 said it had been; only the feature-flow had. Removing the persisted field reds three of the new tests; verified by deleting it and re-running. 848 passed across every voice / portal test in the suite. Related to Abilityai/trinity-enterprise#535 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSjVEjay9ztC1oDXXkh9uN
…log copy (#2281) DigitalOcean granted Vendor Portal access and named `digitalocean/marketplace-partners` as the process of record. Auditing the bundle against that repo and against `droplet-1-clicks@master` found four gaps. Three are fixed here; the fourth needs a measurement. **Security updates were never installed.** Their `99-img-check.sh` scores a pending security update as a hard `[FAIL]`, not a warning, and any FAIL exits non-zero — which fails the build at the last provisioner, furthest from the cause. Nothing in the bundle ran an upgrade, so every green build so far was luck about what the base image carried that day, or about whether Ubuntu's own boot-time timers happened to land first. Neither is ours to own. Adds the `full-upgrade` from their reference `marketplace-image.json`, before the first install so the packages we add are patched too. **`X-DO-MARKETPLACE` was missing** from the Caddyfile first boot writes. Rule 10 of their build standard specifies it on the reverse-proxy block and their own catalog apps ship it. It breaks nothing at runtime, which is why it survived review — it is visible only to a Marketplace reviewer. **There was no `listing.md`.** Rule 11 asks for catalog copy in-repo so the page and the image are versioned together. Ours lived nowhere. It is also where the >=8 GB sizing guidance and the "DigitalOcean does not build or support Trinity" line required by the vendor terms actually get written down. **Submission is now automatable.** `manifest` + `shell-local` post-processors read the snapshot id from the build's own record and PATCH it to the vendor API with the real payload (`imageId` required, plus `reasonForUpdate`, `osVersion`, `softwareIncluded[]`). A `post-processors` CHAIN rather than sibling blocks, because shell-local reads what manifest writes and siblings run in parallel. `mp-submit.sh` is a no-op unless `TRINITY_DO_APP_ID` is set, so a plain `packer build` still just builds, and it names the 400 that means "a previous submission is still in review" — which otherwise reads as a bad token. The runbook gains a **submission gate**: nothing goes to DigitalOcean until another team member has QA'd a droplet from that snapshot. A green `img_check` says the image is acceptable, not that Trinity works on it. **Still open: `build_size`.** It is `s-2vcpu-4gb` (80 GB), and the comment above it claimed the opposite of the value. The build droplet's CPU and RAM never reach the snapshot — Trinity does not run during the build — but the disk size does, and DigitalOcean does not allow shrinking it. So 80 GB excludes every plan with a smaller boot disk, and on the v5 line disk is decoupled from memory: `s5-8vcpu-16gb-30gb` is 16 GiB of RAM on a 30 GiB disk, exactly the plan a Trinity operator should pick, and this snapshot cannot deploy to it. The comment now states the real criterion — the smallest boot disk the baked images fit on — and the value waits on measuring them. Guards are mutation-checked: reverting any one fix turns its test red. The no-op default is executed rather than pattern-matched, per #2522. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sj48trT3XmyaSus4GUtXB
Previewing a text file containing one unbreakable token — a long path, a URL, a base64 blob, a run of `===` — grew the whole preview pane far past the visible area. The panel root is `overflow-hidden` and nothing in the chain scrolls horizontally, so the overflow was unreachable: every line was cut off at the right edge with no scrollbar to drag. Because the `<pre>` is `whitespace-pre-wrap`, ordinary prose then wrapped at the blown-out width too, so the whole preview was clipped, not just the token. Two classes, each fixing a different half: - `FilesPanel.vue` — `min-w-0` on the preview column. As a flex item at the default `min-width: auto` it could not shrink below the min-content width of its contents, which the token defined. - `FilePreview.vue` — `break-words` on the `<pre>`. Wrap rather than horizontal scroll, deliberately: it matches the in-repo precedent `PortalFilePreview.vue`, it is what the design-system contract asks for (bounded viewport, unbounded data), and a scrollbar on a `<pre>` that can be 80 KB tall sits below the fold and is effectively unreachable. Measured in Chromium at a 1280px viewport on a file holding one 320-char unbreakable token, reading scrollWidth/clientWidth: neither root 3048/1280 pane 2768 pre 2720 sidebar 280px break-words only root 3048/1280 pane 2768 pre 2720 sidebar 280px min-w-0 only root 1280/1280 pane 960 pre 2720/912 both root 1280/1280 pane 960 pre 912/912 sidebar 320px `break-words` alone changes nothing — with `min-width: auto` the pane still sizes to min-content, which a break-word rule does not reduce. `min-w-0` alone stops the pane growing but leaves the token overflowing into the `<pre>`'s own `overflow-auto`, i.e. the below-the-fold scrollbar. The sidebar column shrinking 320px → 280px in the broken states is the layout-stability half of the report. Edit mode (the `<textarea>` branch) never blew out — a textarea's width is not content-derived and Chrome's UA sheet already gives it `overflow-wrap: break-word` — and is measured here to confirm it. vitest runs `environment: 'node'` with no mount harness, and even a DOM harness computes no layout, so the browser measurement cannot live in CI. `tests/unit/filesPanelPreviewClip.spec.js` is the source-structure guard for it (the `agentDetailDeepLink.spec.js` shape): it pins both classes, pins that the root still clips — the premise that makes `min-w-0` necessary — and pins the `PortalFilePreview.vue` precedent so the two surfaces cannot silently disagree about wrap-vs-scroll. Each assertion was proven load-bearing by reverting its class and watching it fail. Related to #2666 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RSjVEjay9ztC1oDXXkh9uN
Measured the snapshot the build actually produces:
Name Min Disk Size Size
trinity-v0-9-5-rc2-20260903 80 9.01 GiB
The content is 9 GiB. The 80 GB floor came entirely from the build droplet's
own disk, and DigitalOcean does not let a snapshot deploy to anything smaller —
"you must select a disk size equal to or larger than the Droplet used to create
the snapshot." So every customer was being pushed onto an 80 GB boot disk, and
paying for it, to hold 9 GiB. On the v5 plans, where the boot disk is sized
independently of RAM, that is the difference between choosing a disk and being
told one.
s-1vcpu-2gb (50 GB) rather than DigitalOcean's recommended $6 s-1vcpu-1gb
(25 GB): the 9.01 GiB is their COMPRESSED stored size, while Docker's overlay2
tree on the build droplet is uncompressed — actual build-time use is nearer
18-20 GB before Ubuntu and the apt caches. Too tight against 25 GB to commit
without a build proving it, and a build droplet that exhausts its disk fails
only after pulling every image. 50 GB is also what three of DigitalOcean's own
catalog apps build on (openclaw, jellyfin, craftcms).
25 GB is probably reachable and would match their guidance exactly; one
experimental build settles it, and it is not worth blocking the listing on.
Marked with a ponytail: note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011sj48trT3XmyaSus4GUtXB
…asure it (#2281) A droplet booted from the 2026-09-10 snapshot reports: /dev/vda1 77G 8.8G 68G 12% / 8.8 GB, with everything installed and Trinity running. The comment justifying s-1vcpu-2gb assumed Docker's uncompressed overlay2 tree would reach 18-20 GB and called 25 GB too tight. That was a guess dressed as a reason, and it was wrong by more than a factor of two. Value unchanged — 50 GB still works and is what three of DigitalOcean's own catalog apps build on — but the stated reason is now the measurement rather than the guess, and the ponytail note names what is actually left: 25 GB is settled on disk, and the only open question is whether 1 GB of RAM survives `apt full-upgrade` and `docker pull`. Their own exa-24-04 builds on that size. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sj48trT3XmyaSus4GUtXB
…olliding (#2281) Two builds of v0.9.5-rc2, identical except for build_size, settle what was guesswork: s-1vcpu-2gb (50 GB) 14m38s img_check 8/0 snapshot min disk 50 s-1vcpu-1gb (25 GB) 11m53s img_check 8/0 snapshot min disk 25 Both open questions about the $6 droplet are now measured. Disk: a droplet from the snapshot reports 8.8 GB used with everything installed and Trinity running, against 25 GB available. RAM: a full build on 1 GB completed with no OOM and no disk exhaustion, through `apt full-upgrade` and the agent base image pull, the two places 1 GB would have bitten. It is also faster than the size it replaces, and DigitalOcean's own exa-24-04 builds here. That is AC 2 of #2281 met literally rather than in substance, and it takes the floor a customer must deploy onto from 80 GB down to 25 GB. DigitalOcean does not allow a snapshot to deploy to a smaller disk than it was built on, so this value is the listing's real hardware constraint: on the v5 line, where boot disk is sized independently of RAM, it decides whether an operator picks their disk or is handed one. Also: `snapshot_name` gains minute resolution. Those two builds produced two snapshots named `trinity-v0-9-5-rc2-20260910`, distinguishable only by ID and min disk size, while the Vendor Portal's "select a system image" step picks by what it shows you. Submitting the wrong image costs a review cycle and fails silently, since both are valid Trinity snapshots. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sj48trT3XmyaSus4GUtXB
The PR deletes docs/planning/PULL_PILOT_946_SOAK.md and already removed this same bullet from MESSAGE_ENVELOPE_SCHEMA.md; the copy in agent-to-agent-collaboration.md's "Reference docs" list was missed because the PR's link verification scoped to docs/planning/, not docs/memory/. Verified: all five files this PR deletes now have zero live (non-archive) references. merge-train: mechanical, per the merge-train note on the PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CswDvc38w7EheWhbbUD4So
…ers (#2281) Found by booting a droplet from the 25 GB snapshot and looking at the Console: it renders `trinity-25gb-verify login:` and nothing else. `passwd -S root` reports `L` — locked. DigitalOcean's create page requires either a password or an SSH key, and the choice decides which route to the MOTD exists: password auth root has a password, Droplet -> Console works, no SSH needed SSH key auth root is LOCKED, the Console cannot accept a login at all; the customer must ssh root@<ip> `listing.md` and this README both described the Console as *the* way to get the admin password, unconditionally. For every customer who picks key auth — which DigitalOcean's own create page nudges toward — that instruction leads to a prompt they cannot satisfy, for the one credential without which the listing is unusable. Not fixable by setting a root password: `img_check.sh` scores "User root has no password set" as a PASS condition, so an image that sets one fails review. Both routes reach the MOTD, so documenting both is the fix. The MOTD itself is correct and was never the problem — it renders the password, the URL, the HTTPS status and the support line as designed. Also adds the `cat /etc/trinity/admin-credentials` fallback for re-reading it later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011sj48trT3XmyaSus4GUtXB
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration surface for #2657, #2681, #2680, #2656, #2647.
This PR is never merged. It exists only to run the full suite over the five members together, because every member is tested against
devand never against its siblings. Members merge individually once this is green; the branch is then deleted.No closing keywords here on purpose — the members carry those, and the train must promote and close nothing.
Batch validated 2026-09-10. Ejected this round: #2532 (pre-created RUNNING rows bulk-FAILed by the #106 sweep, reproduced), #2619 (search-to-one-match makes the match unreachable). Held: #2675 (ours; two exits still wedge the 24h claim).