Skip to content

v5.32.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 00:44
· 626 commits to main since this release
6078dda

Added

  • The operator is told in the chat when the Discord helper has lost its connection to Discord, and when it is back (#2272, last of five parts). After five minutes with no connection one notice says since when, and whether the helper is still trying or Discord has refused it; when the connection returns, one more says how long it was gone and to send again anything sent in that time. One outage is announced once, across a helper restart or a server restart. A helper that has stopped reporting raises nothing: the bridge panel and tc bridge status show that.
  • The operator bridge can carry a notice that the Discord helper has lost its connection, and one that it is back (#2272, fourth of five parts). gateway-down and gateway-recovered are admitted as typed server notifications; a refused connection is said to be refused, with the reason. Times are the host's local time with its zone. Nothing raises either notice yet. Schema v63: the upgrade rebuilds one table, bridge_outbound, and carries every row over.
  • The operator bridge panel says whether the Discord helper is connected (#2272, third of five parts). A new Discord connection line under Operator bridge (Discord) in global settings says what tc bridge status says: connected, NOT CONNECTED, REFUSED by Discord (with the close code; the helper will not retry until it is put right and restarted), not reported, or never reported, which is what it reads until the helper is restarted after this update. It is read when the panel opens and on Refresh; it does not update by itself.
  • TangleClaw can say whether the Discord helper is connected (#2272, second of five parts). The helper reports its Gateway connection on every pass and tc bridge status prints it: connected, NOT CONNECTED (with Discord's close code when the connection was refused), NOT REPORTED after three minutes of silence, or never reported since this server started. GET /api/bridge/operator/status and GET /api/bridge/master/status carry it as discordGateway. Nothing is stored: it reads never reported after a server restart until the helper's next pass. The helper must be restarted after the update (launchctl kickstart -k gui/$(id -u)/com.tangleclaw.bridge-helper). A helper set to ask less often than every three minutes reads as not reported between passes.
  • The operator is told about a silent lane nobody else could be told about (#2262). When a lane's expired workload report draws no answer and it has no coordinator who can be told (it names none, names itself, names a project this install does not have or one with no live session, or the message could not be sent), the operator bridge now raises one lane-silent notification for it, which the chat helper posts. The message names the lane's project, says how many minutes it had reported nothing, and why nobody else was told; it carries nothing a session wrote. It is raised once per expired report, only while the bridge is enabled, and not for anything recorded before the bridge was last enabled. tc sessions and GET /api/tc/sessions say what became of it (operatorNotice on the lane's nudge block): the line reads recorded for the operator 4m ago, no notice raised, notice queued, not yet delivered, or notified once the helper reports it posted. Needs workloadNudge on for the lane's project, which is off by default.
  • The operator bridge can carry a fourth kind of server notification, lane-silent, and nothing sends one yet (#2262). Schema v62 rebuilds the bridge's outbound queue table so that it accepts a notification of type lane-silent: a lane whose expired workload report drew no answer and that has no coordinator to tell. Every queued and delivered item is carried over unchanged, with its leases and posted parts. An uncollected lane-silent notification is let go after 7 days. ADR 0023 Decision 9 is amended to name the type.
  • A workload nudge that goes unanswered is escalated once, to the coordinator the project names (#2262). When a session nudged for an expired workload report has still not reported after workloadNudge.escalateAfterMinutes (10 by default, 2 to 120), the server sends one switchboard message to the live session of the project named in that lane's own workloadNudge.coordinatorProject. The message names the lane, how long it has been silent, whether and when it was nudged, and what its engine was last observed doing; it carries nothing from a report's summary. A lane that could not be nudged (a web UI session, an engine with no wake profile, a startup dialog, an engine not observed at rest) is escalated with the reason once the same time has passed since its report expired; a lane that is held, stopped or running a wrap is left alone. A lane with nobody to tell (no coordinator named, the lane naming itself, an unknown project, a coordinator with no live session, or one the Hub refused five times) is recorded on the operator route, and the operator bridge raises a lane-silent notification from that record while it is enabled. tc sessions and GET /api/tc/sessions show it on the lane (; nudged 14m ago, escalated to TC-Lane-INT 4m ago; escalatedAt, escalatedAgeSeconds, escalationRoute, escalatedTo and notNudgedReason in the nudge block). Nothing is restarted or cleared. Off by default, with the nudge.
  • A session whose workload report expired is nudged once, for projects that turn it on (#2262). With workloadNudge.enabled set to true in a project's config, a monitor in the server types one line into the pane of a live session of that project whose newest workload receipt expired by age, asking it to report with tc workload set. It does so once per expired receipt, recorded in workload_nudge_facts so a server restart does not repeat it, and only into a pane observed at rest: never while the engine is busy, at a prompt or unobserved, while a wrap runs, into a held lane or onto a startup dialog. workloadNudge.text replaces the line. tc sessions and GET /api/tc/sessions show the nudge on the lane (; nudged 14m ago, not yet answered; a nudge block). Off by default. The WORKLOAD_NUDGE_NOT_ACTED_ON warning on a save is gone.
  • The record and the setting a workload nudge will use, with nothing acting on them yet (#2262). Schema v61 adds workload_nudge_facts, an append-only table with one row per nudge or escalation of a lane whose workload report expired, unique per receipt, kind and route. A project's project.json can now carry workloadNudge (enabled, text, coordinatorProject, escalateAfterMinutes), validated by PATCH /api/projects/:name and off by default.
  • The Launch readiness panel shows a project's recovery mode and lets the operator choose it (#1937). Settings → Project Rules → Launch readiness now opens with the project's recovery mode as it stands: operator-cleared or advisory, who clears a recovery under it, and why the project is in that mode (the operator pinned it, the operator chose advisory, nobody has chosen and the install's default applies, or the project's file holds a value TangleClaw does not recognise). It shows the decision on record with when it was made, and any disagreement between that decision and the project's file.
    • Below it, two options and a Save recovery mode button set the mode. A saved choice applies from the project's next launch. Launches already started keep the mode they froze, and a held launch's row now says which mode that was and, when the project's mode has since changed, that the next launch will use the new one.
    • What a save was told stays beside the readout until Settings is closed, and each failure says what it left behind: not signed in or refused (nothing changed), the decision could not be saved (nothing changed), or the decision was saved and the rest of the update did not finish.
    • The control is not offered to a page the server would refuse, which is a page that is not signed in while the login is being enforced; the panel says why instead.
    • On an install whose login is not in force the control still works, because the server takes the choice from the dashboard there. The advisory option says that nobody can read a session's reconciliation back on such an install, and a decision on record is described as a request with the dashboard's shape and not as a person's.
    • A launch held in operator-cleared recovery is now also told that the same panel sets the mode for the project's future launches. It is told this only where it is sent to the panel to be cleared (a login in force, or no login at all).
  • A live public roadmap is now linked from the repository README and ROADMAP.md. The generated view groups work by named release and workstream, shows issue order, dependencies, and live status, and keeps public work without a designed version in an Unsorted issues bucket. Security and internal items remain filtered.
  • Session rules can be retired, and an active rule's edits now go through approval as a replacement proposal (#1696, #1709, #1971). A governing project rule can now be taken out of force deliberately — Retire on the Project Rules modal, or PUT /api/session-rules/:id/status {status:'retired'} — rather than only ever disabled or deleted. A retired rule moves to a closed-by-default Rules Graveyard disclosure, its text frozen until it is Restored (always comes back switched off, so it governs again only once turned on). Retirement is password-gated like approval, and the password is checked before the rule is even looked up, so a wrong or missing password answers identically whether the target rule exists or not. Creating a replacement directly (POST /api/session-rules with replacesRuleId) retires its target the same instant it lands active, so the operator must supply the same password there too, before the target is even looked up.
    • Behavior change: PUT /api/session-rules/:id and POST /api/session-rules/:id/restore on an active, non-master rule no longer rewrite its content in place. They now file the change as a replacement proposal and answer 202 with replacementProposed, leaving the rule itself governing its approved text until the proposal is approved — per the #1053 ruling the issue body cites (approval names the exact text it approves), extended from proposals to every governing-rule edit. This is a compatibility change, not a breaking one: the route still exists, the request still succeeds, and a caller that only checked for 200/201 on success should also accept 202. A still-proposed rule (never yet approved) is unaffected — its content still changes in place, 200.
    • A new rule may name the active rule it replaces (replacesRuleId); approving it atomically retires the rule it names. The Project Rules modal shows what approving a pending replacement will do to the rule it names, including when that target has since been retired by someone else or no longer exists.
    • Schema v56 adds status:'retired', replaces_rule_id, superseded_by, retired_at and replacement_origin to session_rules. Existing rows are unaffected; the new columns are NULL until a rule is explicitly retired or filed as a replacement.
    • tc rules marks a retired rule and names what replaced it where known, and says how to propose an amendment to an active one.
    • See docs/session-rules-self-improvement.md's "Rule lifecycle" section for the full transition table and the replacement/supersession rules.
  • Roadmap train cards can show a car as in review or dropped, and name the lane that owns a train (#2165). A car in a tc-train block may now carry "state": "in-review", drawn purple, for an issue whose pull request is open for review, or "state": "dropped", drawn grey and struck through, for an issue closed as not planned. A dropped car is no longer green and no longer counted as done: it is left out of both sides of the train's count and reported separately in the expanded card, so a train with five cars, two closed and one dropped reads (2/4). dropped needs "closed": true and in-review needs "closed": false; a closed car with no state is still drawn as closed. A train may also carry an optional owner of up to 60 characters, shown as lane: name at the end of its name line.
  • Release panels on served plan pages (#2165). A plan can include a fenced tc-release block holding one planned release: {"version": "5.32.0", "status": "planned", "trains": [...]}. TangleClaw draws it as one panel with a header (the version, a status badge and the count) and one train card per workstream, in the order written. Every row shows without a click, and each card still opens to its issue table.
    • A workstream is written like a tc-train block and checked by the same rules. Inside a release its kind must be written out as train or bucket, it must have no version of its own, no train identity may repeat, and no two buckets may share a title.
    • The header's count is worked out from the cars of every workstream, Topic Buckets included, and cannot be supplied by the block. Dropped cars are left out of both figures and reported beside them, for example 2/4 cars · 2 workstreams · 1 dropped.
    • version must be three numbers such as 5.32.0. A release holds up to 50 workstreams and 1,000 cars.
    • A mistake anywhere refuses the whole block, shown as code with a reason that names the workstream. tc-queue blocks are unchanged, and every tc-train block that was valid stays valid.
  • A legend of car states on served plan pages (#2165). A plan page that draws at least one train card or release panel now shows a key to the car colours, once, directly above the first of them: a sample car for each state with its meaning in words. Nothing is added to the plan itself. Green is described as issue closed — not proof the code is written, ready or shipped, and red as needs attention, since a card can only vouch for the state of the issue. A page with no train card or release panel, or whose blocks were all refused, has no legend.
  • Every roadmap car says its state in words (#2165). A car on a train card or release panel now shows its state beside its number, for example #2104 blocked or #411 open, so you never have to tell cars apart by colour. A long train takes more lines than it did as a row of bare numbers.
  • Press a roadmap car to see what it is (#2165). Each car on a train card or release panel now opens a detail when you click it, tap it, or press Enter or Space on it (every car is a Tab stop), and closes it when pressed again. Moving the pointer over a car does nothing. The detail shows the issue number and title, the car's state with the same meaning the legend gives, and the owning lane when the train names one. A car may carry a new optional reason (up to 300 characters) saying why it needs attention, shown as Reason: …. The detail opens in the row, directly under its car's line: the car you pressed stays put, the cars after it move down, and nothing is covered or cut off. Several can be open at once. It replaces the browser tooltip a car had before, which did not appear for keyboard or touch users. For screen readers each car is the browser's ordinary disclosure control, with nothing added to it; this has not been checked with a screen reader (#2207).
  • A train card's cars now sit on their own row, above its name (#2165). The cars are no longer part of the row you press to open the card, so pressing a car never opens or closes it. The name, count and badges are on the line below and still open the card. Existing blocks stay valid and need no change.
  • Progress cards on served plan pages (#1949). A plan can include a fenced tc-progress block naming one of two cards. Project Health shows the open backlog, how many of those issues have gone untouched for 90+ days, and the backlog's net change. It then compares the current and baseline windows, with Delivery (issues closed, PRs merged, cars and trains completed) shown separately from Discovery / Intake (issues opened). Recent Progress is one line for today (Pacific calendar day) plus the current window, and opens to a day-by-day table. If the newest day in the scorecard isn't today, the line says Latest day instead of Today. The figures are read from this host's scorecard file (scorecard/v1.json under the TangleClaw directory) each time the page is viewed, so plans are never rewritten to stay current. Every figure and trend is shown as the scorecard states it, and times are in Pacific time with PDT/PST, never UTC. Both windows are labelled with their dates. A missing or malformed scorecard shows a card that says so, with no numbers. An overdue one shows its numbers under a Stale warning. Nothing publishes the scorecard yet; that comes in a later change.
  • The operator's choice of a project's launch recovery mode is now recorded by the server (#1937). Until now the mode lived only in the project's .tangleclaw/project.json, which the project's own session can edit, and nothing recorded who had chosen it. An operator PATCH /api/projects/:name naming launchSequence.recoveryMode now records the decision in the server store as well.
    • Choosing operator pins the project. Choosing advisory lifts the pin, and is recorded as a decision too.
    • GET /api/launch-sequences says why the project is in its current mode (projectRecoverySource), reports the operator's last decision (projectRecoveryDecision), and says when the project's file disagrees with that decision (projectRecoveryDiscrepancy). GET /api/tc/start/status carries the same fields. The printed tc start status adds a line only when there is such a disagreement, and the same disagreement is logged when the project launches.
    • Every decision is written to the server log and to the activity log as a project.recovery-mode-decided event, with the mode and who decided. The stored record holds only the latest decision; the events are what show that a pin was set before it was lifted.
    • The PATCH answer carries recoveryMode, with fileWritten: false and a RECOVERY_FILE_NOT_WRITTEN warning when the decision was saved and project.json could not be written. The decision still applies in that case. If the decision itself cannot be saved the answer is 500 RECOVERY_DECISION_NOT_SAVED and nothing in the request is applied. If the decision is saved and something else in the same request then fails, the answer is an error with code RECOVERY_DECISION_SAVED_UPDATE_FAILED, and it says the mode was saved and carries it as recoveryMode.
    • A project with no decision on record takes the default, which is described under Changed below.
    • A pin belongs to this install and does not travel with a clone of the project.
    • Downgrade: an older TangleClaw reads only project.json. The PATCH writes the chosen value there too, so a downgraded server sees the operator's last choice wherever that file write succeeded.
    • Upgrade: the store moves to schema v55, which adds one table and changes no existing data.
    • If you activated the operator bridge on v5.31.0: the procedure that puts back the build from before the bridge accepts a store only up to schema 54, and that does not change. Once this release opens your store, that procedure refuses it and changes nothing. This is deliberate: its snapshot predates v5.31.0, and restoring it would also drop everything written since. If you might still need that rollback, do it before updating. The activation runbook now tells an installer to stop when the newest release is later than v5.31.0.
  • GET /api/launch-sequences reports the project's current recovery mode (#1937). Each launch in the list carries the mode it froze when it started. The response now also has projectRecoveryMode, the project's setting as it stands, so a reader can tell a launch that started in operator mode from a project that is in operator mode now. A change to the setting applies from the next launch. It is null for a project this install does not have.
  • The operator can read the reconciliation a session wrote when it attested (#1937). When a launch needs one, the session writes a reconciliation into its READY attestation, and in advisory recovery mode that text is what clears the launch's recovery. It was stored and nothing showed it to anyone. Settings → Project Rules → Launch readiness now has a Read the session's reconciliation button on each attested launch.
    • The answer shows the text with the launch it belongs to: the sequence and the revision it was accepted against, when it was accepted, the digest of the accepted attestation, the preflight verdict, and how the recovery was cleared. An attestation that carried no reconciliation says so.
    • The text is the session's own account. TangleClaw checks only that it is long enough, and the panel says so above it. It is shown escaped, and long text opens from a preview.
    • It is served to a signed-in operator only. The route is POST /api/sessions/:project/launch/reconciliation with {sessionId, sequenceId}. A local process, the session that wrote the text and every other session are refused.
    • It needs a login. On an install with no login the read is refused for everyone, the dashboard included (403 LOGIN_GATE_REQUIRED), and the panel shows a line saying so in place of the button. Nothing on such an install identifies a person, and a session on the same machine can send a request that looks like the dashboard's, so no request there can establish that the reader is the operator. Turn the login on to read reconciliations.
    • The text is not in GET /api/launch-sequences, tc start status, tc start review or the activity log, for any caller. Reading it changes nothing: the stored attestation, its digest and the recovery state are as they were.
  • A signed-in operator can list every launch waiting on their clear (#2049). GET /api/launch/recovery-held answers with each launch whose recovery is required in operator mode, for a session whose stored status is active, across all projects that are not archived. The dashboard's Recovery panel, described below, is the page that shows it.
    • Each launch carries the exact ids and recovery revision a clear needs, the preflight record exactly as the launch stored it (including whether it required recovery, and whether the worktree was dirty, clean or never measured), and how the prior session ended, as the stored status (killed, crashed, wrapped). The status is what the session row says. It is not a statement about why the session ended.
    • Each launch also carries what TangleClaw durably recorded about work that may have been queued or in flight, as five separately sourced parts: stranded wraps, handoffs the prior session staged and never finished, a startup prompt dispatch still in flight, reminders sent to the pane, and pane input. A part says recorded, none-recorded or unavailable. An empty stranded-wrap read is marked incomplete-history, because that record is pruned and an empty list does not show that no wrap was stranded. Pane input is always unavailable: nothing records what was typed or queued in a pane. A source that cannot be read is reported as unavailable with a reason code, and the error stays in the server log.
    • It needs a login. It is served only while TangleClaw's login is in force and the reader is signed in. On an install with no login it answers 403 LOGIN_GATE_REQUIRED, as the reconciliation read does: nothing there could show that the reader of a fleet-wide list is the operator. Each project's Launch readiness panel still shows its own launches on such an install.
    • The read changes nothing and says nothing about whether a session is ready or has resumed work. The session status it selects on is the stored one, not a check that the pane is alive.
  • An operator's recovery clearance is now kept as a permanent record (#2049). Each time an operator clears a launch's required recovery, TangleClaw writes one row to a new launch_recovery_clearances table: the project, session and launch, the recovery revision that was cleared, the operator's name, how they were proven (operator-verified, or open-install-unverified on an install with no login), the time, and the launch's preflight record exactly as it was stored. Until now the only durable trace was on the launch row itself, plus an activity-log entry that is pruned.
    • The clear and its record are written together. If the record cannot be written, the launch stays required and POST /api/sessions/:project/launch/recovery-clear answers 500. A launch is never shown as cleared with no record of who cleared it.
    • A row cannot be changed or deleted, and nothing prunes the table. A launch has at most one row. A refused clear writes none, and neither does a session clearing its own recovery in advisory mode, which is not an operator's clearance.
    • Each row has room for a batch id, empty for a clear made on its own. The batch clear below sets it.
    • No page lists the record of a clear made on its own. A clear made in a batch can be read back by the batch read below, and the Recovery panel shows that read after each batch it sends; a clear made from a project's Launch readiness panel is in the database only.
    • Schema v59. Clearances made before the upgrade get no row: nothing recorded them at the time, and none is invented. Downgrade: an older TangleClaw ignores the table, and clears it makes while downgraded are not recorded there.
  • A signed-in operator can clear several launches in one request (#2049). POST /api/launch/recovery-clear-batch takes up to 100 launches, each named by the exact project, session, launch and recovery revision the operator read, and answers with one outcome per launch. The dashboard's Recovery panel, described below, sends it.
    • Only a proven operator may send one. It needs the login in force, an operator's session and its CSRF token. An install with no login is refused with 403 LOGIN_GATE_REQUIRED, even with the dashboard's page token: the single clear still works there and records itself as unverified, but a batch is a larger grant and is never taken from a sender nobody proved.
    • A launch is cleared only if the request named it. There is no "all" and no wildcard. A request with a malformed item, an unknown field, the same launch twice or more than 100 launches is refused whole with 400, and nothing in it is applied.
    • Each launch gets its own outcome, and one launch that cannot be cleared does not stop the others: cleared, already-clear (that launch, at that revision, was already cleared), stale (the launch has moved on from what the request describes), advisory, session-ended, not-found, archived or failed. The answer is 200 even when nothing was cleared. Sending the same batch twice clears nothing the second time.
    • Each outcome says whether the launch is still blocked, from a read made after it was decided. That is the launch's recovery at that moment and nothing more. It says nothing about whether the session is ready, has acknowledged its task or has resumed work.
    • The batch and every outcome in it are kept as a permanent record, refusals included, in two new tables that cannot be updated or deleted. Each clearance made in a batch carries the batch's id and the operator's name, and a launch's cleared outcome is written in the same transaction as its clear.
    • A project that is archived is not cleared in a batch. Its launch can still be cleared on its own from the project's Launch readiness panel.
    • Internal change: store.launchSequences.clearRecovery now refuses the two operator clearance words. An operator's clear can only be written by clearRecoveryAsOperator, which records it.
    • Schema v60. Downgrade: an older TangleClaw ignores both tables and has no batch route.
  • A signed-in operator can read a batch clear back, with what each launch says now (#2049). GET /api/launch/recovery-clear-batch/:batchId answers with the batch as recorded (who sent it, when, and each item's outcome and clearance) and, beside each item, an observation of its launch read at that moment: its stored recovery, whether it is still blocked, its step cursor, whether a READY attestation is recorded and its session's stored status. The dashboard's Recovery panel, described below, shows it after each batch.
    • An observation is a stored fact, not a claim about work. It does not say a session acknowledged its task, is at a prompt or has resumed, and nothing looks at a pane. A launch that cannot be found reads none-recorded and one that cannot be read reads unavailable, so neither is ever shown as "not blocked".
    • Only a signed-in operator may read one, on the same terms as the list of held launches: the login in force and an operator's session, with no CSRF token. An install with no login answers 403 LOGIN_GATE_REQUIRED.
  • A Recovery panel on the dashboard lists every launch waiting on an operator and clears the ones you choose (#2049). The header has a new Recovery button. It opens a panel that lists each launch held in operator-cleared recovery across all your projects, with the evidence to decide on, and clears a chosen set in one request.
    • What each launch shows: its project, session, launch and recovery revision; its stored preflight record, whole; how the prior session ended, as the session row says; and what was recorded about work that may have been queued or in flight, part by part. A part that holds nothing, a part whose record is pruned and a part nothing records are worded apart. A value never measured reads not recorded, never as false.
    • Choose, review, clear. Tick the launches to clear, or Select all, then press Review. The review names exactly the launches that will be sent. Nothing is sent until you press Clear there. One batch clears at most 100 launches, and the panel says so before the review when more are selected. An evidence list you have opened stays open while you tick, and a button that cannot be pressed is dimmed.
    • Each launch gets its own result: cleared, already clear, changed since you read it, not an operator's to clear, session ended, not found, project archived or failed. Only a clear is marked as one. A launch that changed after the list was read is left alone and reported.
    • After a clear, each launch is read back and shown as an observation with the time it was read: its stored recovery, the launch's step cursor, whether a READY attestation is recorded and the session's stored status. Read the launches again repeats the read. If the read straight after a clear fails, the result says so and shows what the clear itself re-read of each launch: its stored recovery and whether it is still held. If a later read fails, the last observation stays, with its time. An observation says what the stored record said at that moment and nothing about what a session is doing.
    • It needs a login and a signed-in operator. On an install with no login the panel shows the server's refusal and no control, and there is no fleet clear. Each project's Launch readiness panel still clears its own launch there.
    • Clearing types nothing into any session's pane and does not wake a session. A separate wake action is #2250, and the recorded reason a session was stopped is #2249.
    • If the answer to a clear is lost, sending it again is safe: a launch that was cleared answers already clear, under a new batch id.
  • tc message owed lists what a session still owes on the switchboard (#1976).
    • What it shows: replies owed first, each with its age and the exact tc message send --in-reply-to … command, then messages not yet handled. tc message read points to it.
    • What it leaves out: a send still in flight (no Hub id yet) is not listed. Neither is an exchange this host cannot supervise (untracked), which is counted aloud instead. When the route returns its full 200-row page, the output says older exchanges may be missing rather than claiming nothing is owed.

Changed

  • A Codex startup fire blocked for folder trust now says what was actually read (#2186). In a folder with no trust entry in Codex's config (a new project, a renamed directory), the automatic launch fire and "Fire startup prompt" were blocked with a message saying the pane was showing a folder-trust dialog. Nothing had looked at the pane, and Codex 0.156.1 was measured to show no such dialog on this launch path, so there was nothing to answer and the session got no first turn.

    • The message now says that Codex's config has no trust entry for the folder, that TangleClaw did not look for a dialog and does not claim one, and how to grant trust: add [projects."<folder>"] with trust_level = "trusted" to Codex's config.toml, or run codex once in the folder outside TangleClaw and accept its prompt. Then Fire again.
    • Built, not yet active: the operator's decision is that such a fire may go without a trust entry when the session's thread is idle and the pane positively shows an empty composer. The mechanism ships in this release switched off. It applies only to Codex versions listed in the profile's startupControl.remoteTrustPrompt.absentOn, and that list is empty until one signed-in fire has been probed. Until then every such fire still blocks. #2186 stays open.
    • When it is switched on: the pane is read twice, a second apart, and the fire goes only if both reads show an empty composer under the cursor, no trust or update prompt, no header still reading model: loading and no running turn, and the thread is still idle immediately before the send. A trust dialog that is actually on screen still refuses the fire. TangleClaw never writes a trust entry and never types an answer.
    • A fire sent that way carries a note on its row in the Launch readiness panel saying trust was not granted. The note is kept whatever the fire becomes afterwards.
    • Upgrade: the store moves to a new schema version, which adds one nullable column (dispatch_note) to startup_prompt_fires and changes no existing data.
    • A folder whose entry in Codex's config says it is not trusted is reported as exactly that, and is never sent into.
    • Where the folder simply has no entry, other blockers are now reported before the trust one: a fire with no account, no usage left or no idle thread names that first.
    • A config/read answer that reports projects: null (a Codex home that has never trusted a folder) is now read as "no trust entry" and gets the same message; before, it was reported as readiness_unknown.
  • A Codex launch no longer presses Enter into whatever Codex opens on (#2177). Outside the native startup channel, TangleClaw used to send two Enter keys three seconds after starting Codex, meant for its folder-trust prompt. Codex's update prompt highlights "Update now", so on a host with Codex's default update checks that key upgraded the global Codex install and ended the session. Reproduced in an isolated sandbox on codex-cli 0.156.1.

    • Behavior change: TangleClaw now answers neither prompt. A Codex session started in a folder Codex does not trust stops at "Trust this folder?" until you answer it in the pane; before, the blind Enter usually accepted trust for you. TangleClaw does not accept folder trust on your behalf.
    • The update prompt is likewise left for you (Escape skips it). To stop Codex offering it, set check_for_update_on_startup = false in ~/.codex/config.toml.
    • While either prompt is on screen the launch context is not pasted. The project's rule-delivery record shows the paste as skipped, names the prompt and says what to do, and the server log warns when the prompt is first seen. Nothing pastes it later: once you have answered, start the session as you would any unprimed one.
    • The same check refuses a blind paste into a Codex screen TangleClaw does not recognise, or a pane it cannot read. A profile whose guardedDialogs entry cannot be read gets nothing typed at all.
    • Who this affects: Codex sessions that do not use the native startup channel, for example any installed Codex other than 0.156.1, or a launch whose channel could not start. A native launch already typed nothing.
    • Engine profiles gain launch.guardedDialogs; see docs/engine-guide.md. Codex's launch.preKeys are removed. preKeys remains for operator-written profiles and is never sent into a declared prompt.
  • A launch recovery cannot be cleared once its session has ended (#2049). POST /api/sessions/:project/launch/recovery-clear used to clear the launch of a session that had already been killed, had crashed or had wrapped, and recorded a clearance for a session that no longer existed. It now answers 409 SESSION_ENDED and changes nothing. The Launch readiness panel shows the refusal and re-reads the project's launches. Two answers are unchanged: a launch that is already cleared, and a request naming a recovery revision the launch has left, still answer 409 STALE_RECOVERY whether or not the session has ended.

  • Advisory is now the default launch recovery mode where TangleClaw's login is in force (#1937, ADR 0017 R3). When a launch's handoff state needs recovering, operator mode withholds the task step until a person clears it, and advisory mode serves the task step behind a warning and lets the session clear its own recovery by attesting with a written reconciliation, which a signed-in operator can read back from the Launch readiness panel. Until now operator was the default everywhere.

    • On an install with a working login, a project whose operator has made no decision now launches in advisory. The value in a project's project.json does not count as a decision: every save has written "recoveryMode": "operator" there, so it is read as the old default and not as a choice. This takes effect at each project's next launch. A launch already running keeps the mode it started with.
    • Everywhere else the default stays operator: an install with no login, one whose login is stood down behind Caddy's, one with no account yet, and one whose login is locked or cannot be read. Nothing there can say who the operator is, and the reconciliation readback is refused, so a session is not left to clear its own recovery unseen.
    • A project file that says advisory no longer chooses it on those installs. With no operator decision on record, such a project now launches in operator and logs a warning. The file sits in the project's own checkout, where its session can write it. If you set advisory by editing the file on an install with no login, choose it again with PATCH /api/projects/:name ({"launchSequence": {"recoveryMode": "advisory"}}), which records it as your decision.
    • To keep a project in operator mode on an install with a login, pin it: PATCH /api/projects/:name with {"launchSequence": {"recoveryMode": "operator"}}. A pin holds whatever the file says.
    • A one-time notice. A project's first launch under the advisory default prints a notice above its task step, and projectRecoveryInheritedNotice on GET /api/launch-sequences records which launch carried it. The notice belongs to that launch: if it ends before its task step is read, no later launch repeats it. A hand-edited project.json whose launchSequence block has no recoveryMode key takes the default with no notice.
    • A launch held in operator mode is told why, and what can actually be done. The withheld task step, the READY refusal, the unready nudge and tc start status now carry one sentence: the project is pinned, or the launch started in operator mode before the default changed, or the login is not in force, or the setting is unrecognised. What follows depends on the login as it stands. With a login, a signed-in operator clears it from the Launch readiness panel. On an install with no login the clear from the panel is recorded as unverified, because nothing there shows who or what made it. Where the login is stood down or otherwise unavailable the launch cannot be cleared until that is resolved, and the sentence says so. Before this, all four told the session to ask for a clear from the panel, including where that clear is refused.
    • GET /api/launch-sequences gains projectRecoveryGateState, the login gate state the project's mode was resolved against, and projectRecoverySource gains inherited and not-armed (a project with no decision on record while the login is not in force). GET /api/tc/start/status gains status.recoveryHint, the sentence above, present while the launch is held.
    • Launch pages are cut slightly shorter, to leave room for the one-time notice. This applies to launches started after the upgrade.
    • Downgrade: an older TangleClaw reads only project.json, so every project whose file says operator returns to operator mode.
  • New installs default the projects directory to ~/Projects, not ~/Documents/Projects (#880). The setup wizard warns about ~/Documents, ~/Desktop and ~/Downloads, which macOS protects, and the old default sat under ~/Documents, so every fresh Mac opened the wizard with a caution about its own pre-filled value. The new default draws no caution, and the wizard's Create-it offer makes it. Typing a protected folder still draws the caution. Existing installs keep the directory they chose. That includes an install whose config.json never recorded a projectsDir: it stays on ~/Documents/Projects, where it has been all along, and the installer's protected-folder note reads it the same way. The protected-folder list and its wording now come from one place (lib/tcc-folders.js) for every server-side message. The installer and the browser keep their own copies, and a test holds them to the shipped default.

Security

  • tc whoami now says whether a session's launch identity is one TangleClaw honours, and tc refuses to speak for a pane that recorded a different one (#2233). tc whoami used to name the project a session claimed without checking the launch id beside the claim, so a session with a stale or foreign identity was told it was that project, and found out only when a write was refused.
    • tc whoami and GET /api/tc/whoami now carry a binding verdict: verified (the launch is its project's current, active session, or the live Project Master's), stale (a launch id was sent and is not honoured, with the reason), or unbound (none was sent). A verdict that is not verified is the first line of tc whoami, with the cause and what to do, and the answer says "You claim project X. TangleClaw has not verified that." in place of "You are a session of project X". The answer still arrives when the binding is stale, because that is when it is needed. A check that fails reads unknown, never verified.
    • One wording. The refusal of a write (403 LAUNCH_BINDING_REQUIRED and LAUNCH_BINDING_INVALID), tc start next and tc start ready (409 SEQUENCE_SESSION_MISMATCH and SESSION_ENDED), a refused Medusa send or read, and tc whoami now give the same sentence for the same cause and the same instruction. The codes and statuses are unchanged; the error text of those refusals changed.
    • Behavior change: before any request, tc compares the launch id, project id, role and switchboard workspace id in its own environment with the ones tmux recorded for the pane it runs in. If they differ it sends nothing, prints both values for each one that differs, and exits 2 with PANE_IDENTITY_MISMATCH, for every verb. It does not rewrite either identity, and there is no switch that skips the check. Where the comparison cannot be made (the shell is not in a tmux pane, tmux does not answer within a second, or the pane's session recorded no TangleClaw identity) the verb runs, and tc whoami prints "Pane check: not made" with the reason.
    • What the pane check does not catch, and this is the case #2233 was reported for: a tool shell that inherited its whole environment from another pane's process inherits that pane's id too, reads that other pane, and finds agreement. That is what happens on an engine process shared between sessions. Keeping a launch off such a process is the launch check below, and refusing its writes is the check on writes below; the pane check is a third, narrower one. On Codex's native startup path tools do not run in the pane at all, so there the check reads "not made".
    • A Codex session on the native startup channel is no longer handed the tmux pane of the TangleClaw server itself. The per-launch Codex server used to inherit TMUX and TMUX_PANE from TangleClaw, which has none under launchd. On an install where TangleClaw was started by hand from a TangleClaw-launched pane, every tc verb in such a session would have been refused with PANE_IDENTITY_MISMATCH, and ending and relaunching the session would not have helped.
    • A Project Master launch id that tmux could not be asked about reads LAUNCH BINDING NOT CHECKED in tc whoami (binding.state is unknown), not STALE: nothing was found wrong with it. A write with that binding is still refused. The server log warns when tc whoami finds a binding stale.
    • Sessions are now told to act on it. The guide TangleClaw generates into each project's engine config (the Medusa switchboard section) adds one instruction: a tc whoami that does not read Launch binding: verified means stop and report. A session picks that up at its next launch.
    • A request that reaches a write is now logged with who was admitted: via (operator, master, project, service token, or the listed exception), and for a project its id and session. No log line was added; the fields are on the existing request line. Reads log as before.
    • A request to the port registry, shared documents or a group's sync now gets an answer when the service-token setting cannot be read. Before, that read sat outside the route's error handling, so the request was never answered and the caller waited on an open connection.
      Reported-by: GURULifeline
  • A request that changes anything must now say who is asking (#2233). Until now each route decided for itself whether to check its caller, and more than half of the routes that change state did not. A local process with no TangleClaw identity, a session whose launch had ended, or a session that a newer launch of the same project had replaced could still lease ports, send Medusa messages, type into a pane, change configuration and more. Every such request now passes one check before its route runs, and is refused unless the caller is the operator, the live Project Master, or a project session whose launch is that project's current, active session.
    • Behavior change for agents and scripts: a POST, PUT, PATCH or DELETE to /api/ with no launch headers is refused with 403 LAUNCH_BINDING_REQUIRED; with launch headers that do not verify, 403 LAUNCH_BINDING_INVALID and a reason. tc sends the headers already. A raw curl has to send x-tangleclaw-launch-id: $TANGLECLAW_LAUNCH_ID and x-tangleclaw-project-id: $TANGLECLAW_PROJECT_ID. This includes every port registry write (/api/ports/lease, owner-kind, sync, release, heartbeat) and the Medusa routes (/medusa/send, /medusa/read), whose guides showed those calls without headers; the guides are corrected. Reads are unchanged.
    • A session started before this change keeps its old instructions until it is relaunched. Its project guide still shows the calls without headers, so a curl it copies from there is refused. Relaunch the session to give it the corrected guide.
    • The refusal code changes for a caller that cannot be identified. Routes that used to answer such a caller themselves (PROJECT_BINDING_REQUIRED, SHARED_DOCS_BINDING_REQUIRED, ROTATION_BINDING_REQUIRED, WORKLOAD_BINDING_REQUIRED, OPERATOR_ONLY, OPERATOR_REQUIRED, and 423 CONTROL_CALLER_UNATTRIBUTABLE on restart and update) now answer 403 LAUNCH_BINDING_REQUIRED or LAUNCH_BINDING_INVALID on a write, because the caller never reaches the route. So do routes that checked for a login themselves and answered 401: a request that is not a browser and carries no launch headers now gets the 403. A caller that is identified still gets the route's own answer.
    • Setup and the Caddy credential. While an install was waiting for its first account, a bare request from the same machine could still reach the setup and Caddy-credential writes. It is now refused. The supported path is unchanged: create the account, sign in, then change the credential.
    • A service that is not a TangleClaw session can still lease and release ports with the service token, but only while the operator has that gate turned on. With the gate off there is no credential to check, so such a service is refused.
    • The audit heartbeat (POST /api/audit/heartbeat) carries no credential and is now refused for a remote engine. Audit ingest is unaffected: it proves its connection's audit secret, which is now compared in constant time. A connected OpenClaw instance will show as silent in audit telemetry until the heartbeat is given a credential (#2239).
    • The soak harness (scripts/soak.js) does not yet identify itself on writes, so a soak run against an install with this change is refused on its launches, kills and Medusa sends. Its repair is tracked in #2240.
    • A session that finalized itself can still repeat that one request after its session has ended, so an interrupted teardown can be finished. It reaches no other route and no other project.
    • How strong this is depends on whether a login is set. With a TangleClaw login in force, the operator is a signed-in session and the check is backed by a credential. On an install with no login, or while the login is stood down by the fallback marker, the dashboard is recognised by the shape of its request, as it always has been there, and a local process that sends the dashboard's header is taken for it. There the check stops a session that is honest about being a session from acting under the wrong identity; it does not stop a process that claims to be the dashboard. Set a login to close that.
    • Not covered: a conversation that presents another live session's valid launch headers is admitted as that session. Keeping a conversation off a shared engine process is the launch check above.
      Reported-by: GURULifeline
  • TangleClaw no longer starts Codex when it cannot show the launch is kept off Codex's shared background process (#2233). A Codex started with a bare command can be served by a background process that another launch started. Tools run there carry that other launch's TangleClaw project and launch identity, so the session can act as a different project. Until now, a Codex version TangleClaw had not verified, a version probe that failed, and a fallback that could not build an isolated command all launched the bare command anyway. Now the command is checked as it is about to run, for project sessions, relaunches and the Project Master, and a launch that is not positively isolated does not start.
    • A launch is allowed in two cases only: the pane attaches with --remote to the per-launch server TangleClaw started for it, on a server version verified for that (0.156.1), or it runs with --no-daemon on a Codex version verified to accept that flag (0.156.1 and 0.157.1). Launch arguments that repeat or contradict those flags, or that are not plain words a shell passes through unchanged, are refused.
    • Behavior change: Codex 0.154.0 and every version not on that list, 0.161.0 included, no longer launch. The launch answers 409 LAUNCH_ISOLATION_UNVERIFIED with the version found, a reasonCode and what to do about that reason. There is no override.
    • The install command TangleClaw offers for Codex now installs a version that launches. The first-run wizard's command was npm install -g @openai/codex, which installs the newest Codex, and the newest Codex is refused. It is now npm install -g @openai/codex@0.156.1, and a version refusal gives the same command.
    • The pane now runs the exact Codex executable whose version was checked, by its full path. Before, the pane ran the name codex, which its own shell could resolve to a different install.
    • A launch that an engine profile identifies as Codex but that would run a wrapper not named codex is refused: a wrapper's version output does not show that it passes the isolation flag on.
    • Other engines launch as before. A launch that names Codex is also refused when TangleClaw's own Codex check is missing or cannot run, so a broken install cannot fall back to an unchecked launch.
    • After upgrading, end and relaunch every Codex session, and the Project Master if it runs Codex. The check runs when a session is launched; a session that was already running is not examined or ended. docs/engine-guide.md ("Codex compatibility after this check") lists the verified versions for each launch path, what is refused and what the check cannot see.
    • Not covered: sessions that were already running before this change (end and relaunch them), a wrapper or alias that nothing identifies as Codex, a different program that is itself named codex, Codex reached over SSH, and an executable replaced between the check and the launch.
      Reported-by: GURULifeline
  • A project's session can no longer loosen an operator's recovery-mode pin by editing its own project file (#1937, ADR 0017 R3a). Once the operator has pinned a project to operator recovery, deleting the setting from .tangleclaw/project.json, writing advisory or writing an unrecognised value all leave its launches in operator mode. A launch that cannot read the operator's decision also takes operator mode. This covers projects the operator has pinned through the API since this change. A project with no decision on record is still decided by its file, which its session can write (#1982).
  • Only the operator can choose a project's launch recovery mode through the API (#1937, ADR 0017 R3a). PATCH /api/projects/:name used to let a project's own session set launchSequence.recoveryMode, the setting that decides whether that session may clear its own recovery gate. A request from anyone but the operator that names the key, even at its current value, is now refused 403 OPERATOR_ONLY, and nothing else in that request is applied. The project's other settings are unaffected. This guards the API; the entry above covers the copy of the value in the project's own .tangleclaw/project.json.
  • Changing a project's engine no longer carries its launch mode onto the new engine (#2189). Three engines name a mode bypassPermissions, and each means something different by it: on Codex it removes the sandbox as well as the approvals. A project that had Bypass as its default kept it across an engine change whenever the new engine defined the same key, so a Codex project on Bypass switched to Antigravity launched agy --dangerously-skip-permissions without anyone choosing that. With the launch-mode picker hidden, no warning appeared at the switch or at launch.
    • Behavior change: an engine change now resets the default launch mode to Interactive unless the same save names a mode for the new engine. A named mode is checked against the new engine's modes, and Bypass behind a hidden picker has to be confirmed again for the new engine.
    • This reverses what #731 recorded, which kept a confirmed Bypass across a switch to any engine with the same key. A launch mode is now treated as a choice about one engine.
    • The save's answer says when a mode was reset, naming both engines, whether the request left the mode out or (as the dashboard does) sent Interactive by name. A mode other than Interactive chosen for the new engine is not described as a reset. The dashboard's settings modal shows Interactive when its engine dropdown moves to another engine, with a line saying why, and always sends the mode with an engine change, so one chosen for the new engine is kept even when its key matches the old one.
    • A save that names the engine the project already has is not an engine change and keeps the stored mode.
    • A launch that overrides the engine for one session (engineOverride on the launch API) no longer picks up the project's stored default mode either; name a mode with that launch if you want one.
    • If you rely on Bypass or another non-default mode after switching engines, choose it again for the new engine.
  • Changing the Project Master's engine no longer carries its launch mode onto the new engine: the server's half (#2197). The Master's launch mode was honored on whatever engine the Master ended up on, as long as that engine defined a mode under the same key. Bypass skips approvals on Claude Code; on Codex it also removes the sandbox. A Master set to Bypass for one and then moved to the other got the second meaning without anyone choosing it or being told.
    • Behavior change: a save that changes the engine set for the Master resets its launch mode to Interactive, unless the same save names a mode, which is then a choice for the new engine and is refused if that engine does not define it. This applies to master.engine and, for a Master that follows the default engine, to defaultEngine.
    • This reverses #756, which kept the Master's mode across an engine switch so that switching back restored it. A launch mode is now a choice about one engine, as #2189 made it for projects.
    • The save says when it reset the mode: PATCH /api/config answers with a warnings list naming both engines and both modes. warnings is present on every successful answer and empty when there is nothing to say.
    • The mode is stored with the engine it was chosen for (master.launchModeEngine, written by TangleClaw, refused from a caller, shown by GET /api/master/status). At launch it runs only on that engine: when the configured engine is not installed and another starts in its place, the Master starts in Interactive and the stored choice is left alone.
    • A mode stored before this change has no engine recorded. It is read as chosen for the engine configured now, so nothing changes at upgrade, and it stops carrying at the next engine change.
    • A mode sent on its own must be one the engine set for the Master defines. Sending the stored mode back unchanged is never judged.
    • If you rely on Bypass or another non-default mode for the Master after changing its engine, choose it again for the new engine.
  • Changing the Project Master's engine no longer carries its launch mode onto the new engine: the settings modal's half (#2197). The Master settings modal sent the launch mode on every save, read from a list drawn for the engine stored before the engine dropdown was moved. So an engine change made in the modal told the server the old mode had been chosen for the new engine, or was refused where the new engine does not define it.
    • Behavior change: moving the Engine dropdown in Master settings now lists the new engine's launch modes and selects Interactive. Moving it back shows what is stored. A mode reaches another engine only if you pick it there; left alone, the server resets it to Interactive and says so.
    • This reverses #756 in the modal as PR #2277 did on the server: the sentence "Your choice is kept and applies again if you switch back" is gone, because the choice is no longer kept across an engine change.
    • The modal says when a save reset the mode: the server's notice is shown under Launch mode and stays there. The status line, which hides after three seconds, only says a notice is there.
    • A save that leaves both the engine and the mode alone no longer sends the mode at all.
    • While the engine set for the Master is not installed and another stands in, the list is that of the engine that is set, and the modal names the stand-in and the mode it will start in.
    • A stored mode that belongs to another engine than the one now set is shown as that, and saving the form replaces it with the mode selected.
  • Changing the Project Master's engine no longer carries its launch mode onto the new engine: the setup route, and which engine is set (#2197). POST /api/setup/complete set the default engine without the rule the settings save applies, so a Master that follows the default engine kept a launch mode chosen for the previous one.
    • Behavior change: a default engine set through POST /api/setup/complete now resets the launch mode of a Master that follows it to Interactive, when the mode was chosen for another engine. The response's warnings say so, naming both engines and both modes, and the server logs it. A pinned Master, an unchanged default engine, and a mode already recorded for the new default engine are left alone. This reverses #756 on this route as PR #2277 did on the settings save.
    • GET /api/master/status now states which engine is set for the Master, as settings.configuredEngine: the pin, else the default engine, null when neither is set. resolvedEngine is still the engine that will start, which is another one when the engine set is not installed.
    • The Master settings modal reads that field. On a Master with no pin it no longer takes an installed stand-in, or a stale copy of the default engine, for the engine that is set: a mode chosen for the default engine is shown as selected, and a mode that belongs to another engine is replaced by saving the form, as it already was with a pin.
  • You can now see which running sessions were started off their engine's shared background process, and check that a relaunch took (#2233). TangleClaw checks a Codex launch for isolation as it starts, which says nothing about a session that was already running when that check arrived. tc sessions isolation (and GET /api/launch/isolation) lists every live session and the Project Master with one of four verdicts and the reason: ISOLATED, NOT ISOLATED, NOT SHOWN ISOLATED or not applicable. Reported by GURULifeline.
    • It reads the command tmux recorded when each pane was started, so it needs no record of its own and works for sessions started before the upgrade. A Codex session started by the bare name codex reads NOT ISOLATED: that is how Codex was started before the launch check existed.
    • NOT SHOWN ISOLATED means the command could not be read or judged (tmux did not answer or failed, the pane records no start command, or the command could not be read back exactly). Treat it as not isolated. It is never counted as a pass.
    • To act on it: end and relaunch each session it marks, then run tc sessions isolation --replaced <session ids>. For each id it says whether that session has ended and whether its project's current session reads isolated.
    • Read-only: it ends and restarts nothing. It answers the operator, the Project Master and a session with a verified launch binding; anyone else is refused 403.
    • What it cannot tell you. It judges the command a pane was started with, not the process running in it now. For a --no-daemon session the Codex version was checked at launch and is not checked again. A session whose pane tmux no longer has reads not applicable: nothing is running in a pane for it. That is read only from a tmux that listed its panes or said it has no server; any other tmux failure reads NOT SHOWN ISOLATED for every session.
    • Behavior change: tc sessions followed by a word it does not know now exits 1 with usage. It used to ignore the word and print the roster.

Fixed

  • The landing page shows a banner when tmux's global environment holds a launch identity variable, or when that could not be checked (#2268). It names each variable found, the tmux set-environment -g -u <NAME> that removes it and the time of the read, and never shows a value. It is hidden only when the server says none was found: a missing report is shown as "could not check", with the instruction to restart the server. No dismiss button; it clears at the next server restart. Known: the banner has not been seen in a browser; it is checked after the deploy only if the host shows one.
  • tmux's global environment is checked for launch identity variables at server start (#2268). If it holds TANGLECLAW_LAUNCH_ID, TANGLECLAW_PROJECT_ID, TANGLECLAW_ROLE or TANGLECLAW_WORKSPACE_ID, the server logs one warning, and GET /api/server-info now always carries launchIdentityNotice, whose state is none, found or could-not-check. Names only, never a value. Nothing is removed. tc's PANE_IDENTITY_MISMATCH refusal now names this cause, with the command, when it applies.
  • A launched pane no longer inherits another launch's identity from tmux (#2268). A launch that did not set one of the four identity variables got a leftover value from tmux's global environment, and every tc verb there was refused with PANE_IDENTITY_MISMATCH. Each launch now decides all four, for its pane and for the per-launch Codex app-server. Takes effect for sessions launched after the upgrade; nothing is removed from tmux's global environment.
  • The Discord helper no longer loses its Gateway connection for good (#2272, first of five parts). Twice on 2026-10-10 the helper stopped receiving the operator's replies for hours while it kept posting notices, until it was restarted by hand. A failed connection is now retried at once, an attempt not ready in 30 seconds is given up and retried, and a once-a-minute watchdog starts one if the helper is ever left with none. The helper must be restarted after the update (launchctl kickstart -k gui/$(id -u)/com.tangleclaw.bridge-helper). Not proved against Discord itself, which cannot be made to stall on demand.
  • The setup wizard no longer says a notice was "skipped" when it was not (#2284). The confirm step headed every server notice "N item(s) were skipped". The heading now reads "Setup finished. One thing to know:" (or "N things to know:") and each notice says what happened in its own words.
  • A rule changed while a session runs is now in what that session is re-delivered after a /clear (#2273). A rule approved mid-session was missing from the block a cleared session was shown as binding, and a retired, disabled or deleted one was still in it. Each rule change now rewrites the delivered files; when they could not be rewritten, the response carries deliveryWarning and the server log says why. One case is stated, not fixed: a project launched with no rule in force is not re-delivered the first rule approved for it mid-session after a clear until its next launch; tc rules shows the rule meanwhile. The warning reaches the API response and the server log only: no screen shows it yet. Delivering the rule, and showing the warning on a screen, are tracked as #2293.
  • A recovery-mode decision made where nobody was identified is no longer recorded as made by "operator" (#2244). Choosing a project's launch recovery mode recorded who decided as the caller kind, and on an install whose login is not in force that kind is granted to any request with the dashboard's shape. The decision now records the proof it was taken on, in the words a recovery clear uses: operator-verified for a signed-in session with its CSRF token while the login gate is armed, and open-install-unverified for every other request the route takes, fallback included. decidedBy carries that word in the PATCH /api/projects/:name response, in projectRecoveryDecision on GET /api/launch-sequences, in the project.recovery-mode-decided activity event and in the server log. The Launch readiness panel says which it was, whatever the login's state is now. Who may make the decision is unchanged. A decision recorded before this release still reads operator and is not rewritten: the record cannot say which it was, and the panel keeps saying so where the login is not in force. A reader that compared decidedBy to "operator" must compare to the two proof words.
  • The Launch readiness panel no longer offers Clear recovery for a launch whose session has ended (#2178). A launch held for an operator's clear kept its Clear recovery button after its session was killed, crashed or wrapped. The server refuses that clear (SESSION_ENDED), so the click showed a refusal and the same button came back. The row now keeps the reason the launch was held and says the session has ended and a new session has a launch of its own, with no button. GET /api/launch-sequences carries sessionActive on each launch, decided by the same check the clear route refuses on.
  • node scripts/apply-update.js --help applied the update instead of showing usage (#2115). The script read one flag and ignored every other argument, so asking it for help ran the real update. --help and -h now print usage and exit 0. Any other argument the script does not recognize, a mistyped --discard-tc-files included, is refused with exit 2: the reason is on stderr, stdout is empty, and the updater is never called. Running the script with no arguments, or with --discard-tc-files, is unchanged.
  • A Claude Code launch into a folder it has never trusted no longer dies with an unexplained crashed (#2128). The first time Claude Code opens a repository it asks whether to trust the folder, and the option under its cursor is "No, exit". TangleClaw pasted its prime into that screen about two seconds after launch without looking at it, the paste's Enter confirmed the default, the pane ended within a second, and the session read crashed with no cause anywhere. The dialog does not time out by itself; it was TangleClaw's keystroke that ended the session.
    • A launch now reads the pane before it types. While the dialog is up it sends nothing: no pre-key, no prime, no kickoff. The same check runs before every later text send TangleClaw makes into the pane, in the function those sends go through (the command bar, a peer's wake, a coordinator's command, a wrap's content prompt, the Critic action); each is refused with trust_required and the reason. Once the blocker is recorded, sends stay refused until TangleClaw sees the engine's prompt: a pane it could not read, or a dialog caught half-drawn, is not taken as answered.
    • The session records a named blocker, trust_required. It is in session status as launchBlocker while the session is live and as lastSession.launchBlocker after it ends. A live session waiting at the dialog shows a "needs you" badge on its project card and a banner on its session page; one that ended there says so on the card's last-session badge and row. It is also in the activity log (session.launch_blocked, and cause on session.crashed). The record survives the pane, so a session that dies at the dialog still says why. It is checked against the pane whenever the session's status is read, so it goes when you answer the dialog and a later, unrelated crash is not blamed on it.
    • You answer the dialog. Open the session and choose in the pane. TangleClaw does not accept trust for you: accepting lets the folder's own hooks, MCP servers and permission rules run. Once you answer, the launch carries on and sends its first turn, for up to 15 minutes after the dialog appeared. After that the session still works, and you start it by typing.
    • A launch with no dialog behaves as before, except that Claude Code's prime is now pasted when its prompt appears instead of a fixed two seconds after launch. A screen TangleClaw does not recognise does not block a launch, with one exception described below (a menu with an option selected, or text already typed at the prompt): after 45 seconds it proceeds as it used to, and still takes one last look before each send. If Claude Code rewords the dialog so that neither option text matches, TangleClaw does not see it; the declaration records the version it was measured on (2.1.283).
    • Which screens count is declared per engine (capabilities.startupDialogs in the engine profile, see docs/engine-guide.md). Only Claude Code declares one. Other engines are not watched and behave as before.
    • Your own engine profiles are covered. A profile you made for the same program (a second Claude Code profile pinned to another model, for instance) takes Claude Code's declaration through the command it runs, with no edit. To opt a profile out, give it "startupDialogs": []. Such a profile also takes Claude Code's whole measured prompt signature (the bare-composer pattern, the glyph, its pad and the suggestion styling) if it has no wake block of its own, so it is watched through its boot like the bundled one. Where no measured prompt signature exists for a profile or its program, a launch types nothing and says what to add, and a recorded blocker's refusal says to relaunch after approving the dialog or add a wake declaration.
    • What TangleClaw takes as "the engine's prompt" is its empty input line, not any line that starts with the prompt mark. A menu draws its selected option with the same ❯, so ❯ 2. Yes, I accept is not a prompt: it does not let a send through, does not resume a launch waiting at a dialog, and does not clear a recorded blocker. The input line counts when it is bare, or when it shows Claude Code's own faint suggestion with the cursor shown and resting at its start between the input box's two border lines. A line holding text you have typed is never taken as "ready".
    • A launch types nothing into a menu it does not recognise. If the last line starting with the prompt mark is a selected option, or text already typed there, the launch's pre-key, prime and kickoff are withheld as pane_not_at_prompt, which names no dialog. Start the session by typing in its pane. A screen with no such line at all is still typed into after the 45 seconds, as before.
    • Not changed: a command or nudge sent to a session later still keeps and clears a draft at the prompt, as it always has, including when the session's recent output quotes the trust dialog's wording above that draft. That also means a later send is still typed into a menu no engine profile declares, if one is up mid-session and no blocker is recorded (#2219).
    • The Bypass Permissions confirmation is refused, but not by name. It is not declared as a dialog, because it could not be captured (#2205). As that screen was reported, it shows the "No, exit" text the trust dialog declares, and on that wording both of its options are refused, for a launch and for every later send; the refusal therefore says trust_required. This rests on the reported wording, not on a capture: if the real screen words that option differently, a launch still types nothing into it, but a later send is not refused (#2205 is where it gets confirmed).
    • If TangleClaw cannot read the terminal cursor at the moment a launch's first turn is due and Claude Code is showing its suggestion, that turn is withheld as pane_not_at_prompt and is not retried. Type in the pane to start the session.
    • A mistake in your own profile's startupDialogs no longer switches the protection off. If an entry cannot be read (a missing markers list, a code that is not lower_snake_case), or the value is not a list, a launch on that profile types nothing: no pre-key, no prime, no kickoff. The ledger and the log say startup_dialogs_unreadable and name the profile and the entry. Before, the bad entry was dropped and the list read as shorter, or as empty, which is the opt-out. Only "startupDialogs": [] opts a profile out. A profile with no list of its own inherits this from the profile that declares for the same program (#2224). The same refusal applies to such a profile while TangleClaw cannot read its installed engine profiles at all (a store fault): what its program declares cannot be found out, so nothing is typed, and it clears by itself once the profiles read again. Until then a legacy launch on any profile that has no startupDialogs of its own, Codex's included, has its first turn withheld, and every other send TangleClaw would type into a session on such a profile is refused too (the command bar, wake nudges, coordinator commands, wrap prompts, the Critic action), because nothing can show the pane is safe to type into. The refusal says the cause is TangleClaw's own profile store, not an error in the profile; sends go through again as soon as the profiles read, and if it keeps happening a profile file is unreadable and needs repair (the server log names the error).
    • The same refusal covers two more cases where nothing can be checked. A profile whose own startupDialogs has no readable entry at all (every entry mistyped) now has every send refused, not only its launch: correct the entry or set []. And if the engine profile of a session cannot be read at all, or is missing, sends that name that engine are refused until it can be. An OpenClaw session's openclaw:<connection> engine reads the base openclaw profile, so a healthy one is unaffected. A profile with at least one readable entry keeps working as before: later sends still look at the pane. If a blocker is already recorded for the session, the refusal says so, as recorded and not cleared. The same goes for a profile file that parses to something that is not a profile (a number, a string, a list). The refusal says the profile may be missing, unreadable or malformed, and to install or restore a valid one or repair the file; it does not claim which.
    • Compatibility: an engine profile file must carry its own id. When TangleClaw is about to type into a session, the profile it reads for that session's engine must carry an "id" equal to the profile's own id (its file name without .json). Profiles saved through TangleClaw, and all bundled ones, already do. A profile file you placed in the engines directory by hand with no id, or with a different one, used to work. It now cannot be launched (ENGINE_PROFILE_INVALID, HTTP 409 from the launch route, with nothing started: no engine, no pane, no pre-key), and a session already running on it has every send refused (startup_dialogs_unreadable, "does not carry an id") until you add it. The same applies if a project names its engine in a different case from the profile's own id on a case-insensitive disk (Claude for claude.json). The Project Master follows the same rule: it will not start on such a profile, and the Master settings refuse to select one (a Master already running is not touched). This is what stops an emptied or overwritten file ({}) from reading as a profile that declares no dialogs. It checks identity only: a file with the right id and broken fields is still taken as the profile.
    • The Master's other settings no longer depend on its engine's profile. Changing the Master's access level, auto-start or scope was refused whenever the engine already stored for it had a profile that was missing. Now the engine's profile is looked at only when a settings change names the engine, so those settings save whatever state the stored engine's profile is in: missing, unreadable, or without its id. Choosing an engine whose profile is missing, unreadable or unidentified is still refused (400; the message no longer quotes the JSON parser), and a Master whose stored engine is in that state is refused when it is next started.
    • Lowering the Master's access level no longer depends on working out which engine it runs. Applying a changed level first resolved the Master's engine, and that fails while any installed engine profile file cannot be parsed. A change from write to read-only was then saved in the settings and not applied: the Master kept write access, and the request answered 500 saying so. Now a change that lowers access (or restates the current level) is applied without that answer: the level file is written and the Claude Code write guard is put back in the Master's home, including when the Master had deleted or edited it. The request still answers 500 (MASTER_LEVEL_NOT_APPLIED), because the change is partial, and says which part holds: a Master running Claude Code is held to the new level from its next tool call; the Master's instructions and memory files are not rewritten and still describe the previous level; and a Master on another engine, which has no write guard, is bound only by those instructions until the profiles are repaired and it is restarted. If the guard cannot be confirmed in place the message says not to rely on the change. The same applies when no engine can be resolved for the Master at all (a configured engine id that is not in TangleClaw's engine selection list is passed through as itself; otherwise, when no engine in that list is available, there is no answer. Being in the selection list is not the same as having a profile file: a profile hidden from selection is not in it): before, that was read as a Master with no write guard, so the level was written, a deleted guard was left deleted, and the request reported success. Every change that lowers access now writes the Claude Code write guard and reads it back, whichever engine TangleClaw resolves for the Master. When the engine set for the Master is in the selection list but not detected, and another one is, TangleClaw resolves the other one; a Master still running Claude Code was then treated as having no guard, and lowering its access left a deleted guard deleted behind a success. A Master that really runs another engine gains two unused files in its home (.claude/settings.json and the guard script) the first time its access is lowered. Raising access is refused (500 MASTER_LEVEL_NOT_APPLIED, previous level kept) while the engine resolved for the Master is not the one configured for it: the change goes through once the configured engine is detected again. Setting master.engine to the engine TangleClaw resolved also clears the refusal; do that only if it is the engine the running Master actually uses. TangleClaw does not record which engine a running Master was started on, so this compares against the configured engine only, and does not apply when no engine is configured at all. If the level file and the write guard do not both read back after an otherwise complete change, the 500 now says so outright, and says to repair the Master's home and restart it, not to change the access level again. A change that raises access is never applied this way: it is refused as before and the previous level stays in force. Not covered: a Master started, or TangleClaw restarted, while a profile is unreadable does not reconcile a level file that disagrees with the saved setting (change the level and change it back, or repair the profiles); and when a level is applied normally and a later step of that refresh fails, a write guard the Master had edited may not have been restored, which the error message says.
    • A command into the Project Master's pane is refused when its engine cannot be determined (MASTER_ENGINE_UNRESOLVED). The Master has no session record, so without an engine there was nothing to check its pane against, and the command was typed regardless. In practice this was a narrow case: while an installed profile file could not be read the Master was already left out of the switchboard's nudges, so it could happen only in the moment between that check and the delivery, or when no engine could be resolved for the Master at all. It is now refused with the reason, and goes through again once the engine can be determined.
    • If the check itself fails, nothing is typed. Before, an error inside TangleClaw's look at the pane was caught and the text was sent anyway when no blocker was recorded. For a send that names its engine it is now refused (startup_dialogs_unreadable, "the check ... could not be completed"), and the next send tries the check again. A profile file that holds something other than a profile (the word null, for instance) is reported as unreadable profiles and no longer breaks the check. A profile that takes its dialogs from another profile whose every entry is unreadable needs TangleClaw restarted after that profile is corrected.
    • Upgrade: the store moves to schema v58, which adds one column to sessions and changes no existing data. Downgrade: an older TangleClaw ignores the column.
  • Text sent to a session can no longer be swapped with another send's on the same tmux server (#2173). TangleClaw delivers prompts, switchboard nudges, command-bar input and wrap content by loading the text into a tmux buffer and pasting it, as two separate commands. Neither command named a buffer, so both acted on whichever buffer was newest on the tmux server, and that server is shared by every session, test run and operator on the host. A load by another process between the two could put its text into this session, and leave this session's text for whoever pasted next. Each send now loads and pastes a buffer with its own random name. This was found by reading the code and reproduced only by forcing the order on a private tmux server. It has not been seen to happen on an install.
    • An empty send is now refused. tmux loads an empty file without error and creates no buffer, so a send of empty text pasted, and deleted, whatever buffer was newest: the operator's last copied selection, for one. It is now an error, raised before the prompt line is cleared, and no Enter is sent. Text that is only whitespace is still delivered as given.
    • A send that fails after loading its buffer deletes that one buffer and reports the original failure. It never pastes a different buffer in its place.
    • The Copy button is unaffected: a named buffer is never what it reads, even between a send's load and its paste.
    • The temporary file that carries the text to tmux is named per send, created exclusively and readable only by the user TangleClaw runs as. It was named after the process id.
    • Known limit: a TangleClaw process killed between the load and the paste leaves one buffer named tc-send-… on the tmux server until that server exits. It is not pasted anywhere and the Copy button does not read it. tmux delete-buffer -b <name> removes one by hand.
  • Governed commits, pushes and wraps work again on a direct-mode HTTPS install (#1947). The control hooks called https://localhost with a plain fetch, which does not trust the local mkcert root, so every governed commit, push and wrap was refused. When TangleClaw can prove its certificate was issued by the mkcert root, the marker now records that root, and the hook trusts it only for a literal loopback origin (localhost, 127.0.0.1, [::1]), connecting to the loopback address with full verification. A non-loopback origin is refused before connecting, TLS is never relaxed, and every failure still refuses. An untrusted certificate is now reported as such, not as "unreachable". Relaunch a governed session to rewrite its marker.
  • A session held by the operator-mode recovery gate is no longer told to run commands that refuse it (#1937). After a crash, an operator-mode launch withholds its task step and refuses READY until the operator clears recovery. A session parked there was still nudged to attest with tc start ready, and tc start status still ended with "Run tc start next to continue", which only answered "withheld" again. The nudge now says the launch is waiting on the operator and that nothing the session can run opens it, and tc start status drops the pointer while the step is withheld. Both read the gate's own answer (taskWithheld in the status block) instead of working it out again. Before the task step, the nudge is unchanged: those steps are still served.
  • A wrap no longer commits a file you answered Keep local for (#2154). Each wrap step works out again which uncommitted files this session changed, and an Include / Leave answer was applied only to a file a step counted as not the session's. A file answered Leave could therefore still be committed when a later step read it as the session's own, for example after another session regenerated it while the wrap was running, in a worktree the session had moved into. A Leave now keeps the file out of the commit whatever a later step concludes, and the wrap lists it under Keep local. That also holds for a file a wrap step rewrites afterwards: the rewrite stays uncommitted with it. Leave still discards nothing.
    • This changes one rule from #1406: a Leave sent for a file the wrap counts as this session's own used to be ignored and the file committed. It is now honored. The drawer offers the choice only for files that need a decision; options.pathDecisions on the wrap API accepts it for any path.
    • Files TangleClaw commits as its own maintenance are not affected by a Leave, as before. Answers are per wrap: a new wrap starts with none.
  • Claude's native cross-session messaging no longer depends on a shared /tmp (#1904). Claude Code puts its messaging socket under /tmp unless told otherwise. It rightly refuses a directory another local user could tamper with, so a host whose /tmp had lost its sticky bit (macOS /private/tmp at 0777) started every TangleClaw-launched Claude session with "Cross-session messaging is off".
    • The fix: TangleClaw now gives each Claude pane, and the Project Master's, a private root, ~/.tangleclaw/run/claude-tmp (mode 0700), through CLAUDE_CODE_TMPDIR. It checks the root the way Claude will: no symlinks in TangleClaw's own directories, no ancestor that is group- or world-writable without the sticky bit or owned by another user, and a socket path within the 103-byte Unix limit.
    • Fails closed: a root that fails the check is never handed to Claude. The launch goes ahead. The server log, and a new privateTempRoot field on the launch response (POST /api/sessions/:project), name the failing path and a command to run by hand, and say whether Claude's own default location will work.
    • Never changes system directories: TangleClaw never changes /tmp, /private/tmp or any directory it does not own.
    • Operator settings win: a CLAUDE_CODE_TMPDIR you already set, in the engine profile's launch.env or TangleClaw's own environment, is left alone.
    • Declared in the profile: the behaviour comes from a new capabilities.privateTempRoot field on the Claude profile, so other engines are untouched.
    • Claude's other temp files move too: CLAUDE_CODE_TMPDIR relocates Claude's other temporary files (scratchpads included) into the same root. The OS does not clear it the way it clears /tmp, so it grows until you clear it. docs/engine-guide.md gives the manual cleanup, which must only be run with no Claude session running.
    • Medusa is unaffected: it is a separate transport. docs/engine-guide.md now explains how the two differ.
  • Sessions are told how to reply to a Medusa message in a way that works, and can see what they still owe (#1976). The switchboard nudge said to mark a message handled with a raw POST …/read and then reply with a raw POST …/send. Neither mentioned inReplyTo or the launch headers. A raw reply carrying inReplyTo without those headers is refused (EXCHANGE_BINDING_REQUIRED). A reply sent without inReplyTo records no reply. Either way, the sender stayed blocked.
    • The nudge, the prime's Medusa section and the engine config block now say: reply with tc message send --in-reply-to <message-id> <workspace-id> "<reply>" (or a raw send with the launch headers), before marking the message handled.
    • The nudge names the API base once, so it fits the composer. Claude Code shows a paste of up to 800 characters as typed and a longer one as [Pasted text #N]. A nudge whose Enter is lost is recognised by reading the composer back, so a collapsed one is never recognised and that session's wakes stop until the exchange escalates. The project nudge now reads tc message read, tc message send --in-reply-to …, tc message ack …, then names the API base once as the fallback, with its routes relative to it: GET /messages, POST /send (inReplyTo + launch headers), POST /read. The maximum measured, for names that pass validateName under the origin and unread-count bounds tested, is 746 characters, and a test holds those cases at 750 or less. Two operator-only routes register a name without validateName (#2180); a name longer than the rule allows can still exceed the limit. Intentional test-contract change: four assertions that pinned the raw GET …/messages, POST …/read and POST …/send routes in the project nudge now assert the tc commands and the single API base with its relative routes instead; three full raw routes cannot fit beside a long project name.
    • The Project Master's nudge names only its raw routes (POST …/send, then POST …/read), in the same reply-first order. tc message needs a project identity, which the Master does not have, so its nudge names no tc message command.
    • Never use /clear as an acknowledgement. The nudge and every engine config form now say so. The prime does not: its Medusa line is at the prime's length budget, and a longer line was measured to drop the ecosystem primer.
    • tc message owed is new; see ### Added.
    • No protocol, schema, escalation or UI change. A normal message still needs no reply by default. Nudging recipients about overdue replies, and a turn-end check, are later chunks of #1976. The dashboard inbox panel acknowledging messages on display (#1987) and the Project Master being unable to reply with inReplyTo (#1988) are tracked separately.

Internal

  • The Codex startup-control adapter's tests no longer order their fake app-server by wall-clock timers, so a busy host can no longer fail them. Test-only; no behaviour change (#2057).

  • The soak-guest test no longer fails on a busy host (#2210). The test allows a probe 60 seconds (6 where it must hang); those few cases still depend on the host at very high load. Test-only.

  • The setup-scan own-install test no longer walks the folder the checkout sits in, so its result does not depend on how many projects the host holds or how loaded it is (#1999).

  • The README opens with the problem, the product, and what it is not (#2288). The opening is text the Operator approved; "SDLC Departments & Agent Roles" moves below "Core Features" and says which roles exist today. Known: "Core Features" still says "no build step" under "Zero dependencies".

  • CONTRIBUTING.md says who can open a pull request (#2282). Collaborators only; anyone else describes the fix in an issue, and a report leading to a fix is still credited by name. Outside code is never merged, fetched or run here.

  • This repository's tracked CLAUDE.md block is held to what TangleClaw generates (#1796). A test now fails CI when the generator's output moves and the tracked block does not (node scripts/sync-claude-md-block.js --write updates it). The block is rendered for one named profile, so a checkout launched with other settings still shows a difference.

  • A project's delivered rules files can be regenerated from the rules in force (#2273, first of two). New lib/session-rules-refresh.js; nothing calls it until the second part.

  • A direct test holds every answer of the operator-proof helper (#2286). Removing any of the three conditions of server.js#_operatorProofOf now fails a test. Test-only: the helper's behaviour is unchanged.

  • CLAUDE.md names the lane test bed's changelog exception (#2271). "This Repo's Exceptions To The Global Rules Below" now says that in a lane test bed project a Builder keeps CHANGELOG.md and FEATURES.md off its branch (the Integrator lands a batch's entries in one records PR) and does add its entry to .prawduct/change-log.md, and that the project's own rules, read with tc rules, are the authority. Documentation only; every other checkout is unaffected.

  • The launch-isolation inventory (#2233) is now also tested against a real tmux server read from outside a pane, the way the TangleClaw server reads it. Its first version separated a session's name from its command with a tab, which tmux prints as _ when it is run with no pane and no UTF-8 locale, as under launchd, so on a running install it found no session and reported nothing to relaunch. It was caught on the first live read after deploy and never shipped in a release. The separator is now a colon, the listing is asked for in UTF-8 so a non-ASCII path is read as written, a listing that cannot be split counts as a failed read, and a start command is read in both forms tmux quotes a variable in (tmux 3.4 differs from 3.3a and 3.6a).

  • The test that a roadmap car's state word is never hidden now checks the rules that can hide it (#2165). It looked only at style rules written for the word itself, and there are none, so it passed without checking anything. It now checks every rule on the boxes the word sits in (the release panel, the card, the row of cars, the car's disclosure, the pill and each of its six states), including rules inside a block for narrow screens or one colour scheme, for anything that would hide, clip, shrink or blank it. Companion tests show the one pattern it uses catching each of those, and a rule inside such a block being seen. Two statements about the fallback layout for browsers without ::details-content now say it is reasoned from the CSS and has not been seen in such a browser. No behaviour changes.

  • The fleet recovery panel script no longer publishes its route table (#2049). public/fleet-recovery-panel.js set tcFleetRecoveryRoutes on the page and nothing read it. The script now publishes only tcCreateFleetRecoveryPanel and tcMountFleetRecovery, and a test holds it to those two names.

  • The launch recovery clear's checks moved into one function (#2049). lib/launch-recovery-clear.js#clearOneLaunch now decides whether a named launch may be cleared, performs the compare-and-set and writes the launch.recovery-cleared activity row. POST /api/sessions/:project/launch/recovery-clear calls it and keeps its own operator proof, status codes, messages and log lines, so nothing a caller sees has changed: the route's existing tests pass unedited. This is the first step toward clearing several launches in one operator action, which must not grow a second copy of these checks. New test/launch-recovery-clear-one.test.js holds the function's contract, including that a launch cannot be cleared through a project it does not belong to.

  • The committed CLAUDE.md carries the switchboard reply line the server now generates (#1991). #1991 added a line to the generated Operational Guide telling a session to reply with tc message send --in-reply-to before marking a message handled. The tracked CLAUDE.md was not regenerated with it, so a server on that code rewrote the file in its own checkout and left the working tree modified. The committed file now matches what the server writes.

  • The Codex wake fixtures now cover the versions an installer is likely to be running, and pin what the gate refuses on them (#2059). test/fixtures/codex-panes/ holds whole panes captured from codex-cli 0.156.1, 0.159.0 and 0.161.0 on a private tmux server: the screen Codex opens with, the folder-trust prompt, the update prompt (0.156.1 and 0.159.0; 0.161.0 was the newest release and showed none), at rest with and without the status line's run-state item, a typed draft and a turn in flight. test/medusa-wake-codex-fixtures.test.js runs each through the real gate. It pins that the opening screen, which draws an empty composer before any prompt appears, is not at rest under the shipped profile and would read idle if the at-rest marker were dropped; that a turn, a draft and both prompts are refused either way; and that a version the profile has not verified gets no channel and so is never woken. One cell is recorded as known unsafe, not as a passing check: with a fresh idle from the engine's channel the pane gate does not refuse the opening screen and a nudge is typed, on the verified 0.156.1 as well. Whether the channel can answer idle while that screen is up is unproven. Test-only; nothing in lib/ changed.

  • Two tmux tests no longer share a temp file between concurrent runs (#2173). The launch-environment tests in test/tmux.test.js named their dump file from the clock alone, so two runs reaching that line in the same millisecond wrote and deleted the same path. Each now gets a directory of its own from mkdtemp.

  • Two test runs on one machine no longer break each other's tmux sessions (#1983). The tests that start a real tmux session named it with a fixed string, and tmux session names are shared by everything on the host. Two suite runs at once therefore failed each other with duplicate session and can't find session, which said nothing about the code under test. Each session name now comes from test/_tmux-session-names.js#uniqueSessionName, which combines the process id, a random value drawn once per process and a per-process counter. A test still tears down exactly the name it created, and nothing cleans up by prefix or pattern. This covers test/tmux.test.js, test/tmux-draft-capture.test.js and test/activity-observer.test.js; no product code changed. test/tmux-session-names.test.js traces every name those three files hand to tmux.createSession or tmux new-session back to the factory, and fails when a name is a literal, is built from the pid, or cannot be traced. It also fails when another test file starts a real session without being listed there. It reads source, so a createSession pulled out of the module by destructuring is outside what it sees.

  • The control-state rollback guide says what happens to control state (#1867). A rollback deletes no assignments, holds, events or receipts, and an older server leaves them untouched. Re-upgrading makes them authoritative again, so a lane held or stopped before the rollback is refused again. docs/control-state.md now says so, and says to resolve that state through the control workflows rather than by editing the database.

  • The Feature Index lists two panel tests it had missed (#1906, #1768). FEATURES.md now has entries for test/panel-toggle-rows.test.js and test/port-owner-kind-panel.test.js, both of which have been in the suite since those issues shipped. The entries were written in a session's working copy and never committed; they are carried over here, with the first brought up to date for the focus test added since (#1946).

  • The Feature Index gains seven more entries it was missing. FEATURES.md now lists ADR 0019 (generated-file provenance) and ADR 0020 (session workload receipts, replacing a TBD stub), and five tests that were in the suite without an entry: medusa-wake-stranded-nudge (#1621), prime-hook-output (#1888), inline-handler-args (#1384, #1902), project-tags-shape (#1375) and session-header-cleanup (#1472, #1474-#1476). Each entry was checked against the file as it is now; the one for inline-handler-args was rewritten, since the test has covered every page script since #1902.

  • The wizard-scan routing test no longer races the 5-second scan deadline (#1993). uses a scanner the background poll cannot kill underneath it in test/projects.test.js stubbed only the background dirScanner.request, then ran a real scan through the scanner child under PROJECT_SCAN_TIMEOUT_MS. Under full-suite parallelism and fleet load, spawning the child and walking the directory could overrun that deadline, so the test failed for a reason unrelated to what it guards. It now stubs dirScanner.interactiveRequest too, so it answers at once. The test asserts the scan reached the interactive entry point exactly once and the background one never, and that the result is the interactive answer. It spawns no child and walks no directory. Production code and the timeout are unchanged. Pointing scanDirectoryForProjects at dirScanner.request turns it red; it passed 20 of 20 runs at a load average near 29.

  • The advisory READY path is pinned by tests before advisory becomes the default (#1937). New test/advisory-ready-audit.test.js covers the five properties the Architect named for the audit. A reconciliation must be a string of real length after trimming, and cannot stand in for reading the task step or for the verdict. The preflight record, the files behind it and earlier sessions' statuses are unchanged by the clear. The clear is recorded as agent-reconciled with no operator, whatever the artifact claims. A refused attestation, or a clear that cannot be written, records nothing. A launch keeps the mode it froze when the project's mode changes under it. Test-only: no behaviour changes, and the default is still operator.

  • ADR 0024: Release Versions group one or more Trains, superseding one-train-one-release where they conflict (#2157, #2158). Records the Architect's ruling (A84) on an Operator-directed roadmap restructuring: a new nullable target_release field on each Train (Train id stays immutable per #1942), one train maps to zero or one release, one release to one or many trains, and target_release is forecast only, never proof of shipped contents. Implementation (schema/generator changes, the Train/issue sweep) is tracked separately in #2157 and has not started.

  • Fleet-concurrency guardrails added to the stand-up runbook (#2159, docs/runbooks/stand-up-a-new-agent-fleet.md). A new "running the fleet concurrently" section records six evidenced guardrails (one writer per workspace, one chunk per session, merge authority kept outside the Builder, review results bound to an exact head, caller/auth-surface changes routed through the Architect before code, verifying a peer's claim before acting on it) from a 2026-09-16 architectural assessment plus one overnight shift that exercised them in practice.

  • The Feature Index gains two entries it was missing: ADR 0024 (release versions group Trains) and test/operator-bridge-runbooks.test.js. A session wrap's index step found both undescribed and stubbed them as TBD on a local branch; they are written out here in full, so no placeholder reaches main.

  • ADR 0024 gains Amendment 1: a release holds its whole workload (#2157, #2165). It amends Decision 4 and parts of Decisions 2 and 5. Decision 4 excluded bucket entries from the release view. The roadmap now plans work by release, and most release-scoped workstreams have no permanent Train id yet, so that rule would have left most of a release outside it. The amendment admits release-scoped buckets, says a bucket is not a Train and gets no Train number until a recorded identity decision, and keeps dependencies structured and visible. A Train spanning releases is reviewed, not split automatically, and placing an issue in a release settles its forecast but not its Train. Release targets remain forecasts.

  • Groundwork for choosing an engine's model per project (#2188). lib/engine-models.js decides which models an engine may be launched with and whether one can be selected now: a model must be on the engine profile's allowlist and in the roster the installed CLI reports, and anything that cannot be confirmed is refused with a code and a reason. The Codex profile declares gpt-5.6-sol and gpt-6-luna. Nothing reads this at save or launch yet, so no setting, API field or launch command has changed. See docs/engine-guide.md, "Model selection".

  • ADR 0014 now says who rebuilds an external pull request. Raised by external PR #2218, which no session was allowed to rebuild. The 2026-09-17 amendment gave the PR Reviewer both the micro filter and the reconstruction, while that session's own rules make it review-only, so no session could rebuild a pull request that had passed both filters. The PR Reviewer keeps the micro filter. Reconstruction goes to a reconstruction lane: a Builder-class session whose checkout does not serve the live install, working in a fresh worktree, started only by the Operator's direct instruction in that lane's own terminal. Review, merge and the contributor reply are unchanged. docs/dependency-bump-audit.md names the same lane for a Dependabot rebuild.

  • ADR 0014 records how its 2026-10-07 amendment was actually ratified. The amendment said the Operator would merge it personally and that the pull request's record shows who merged. The ProjectManager reports that it merged it, and that the Operator had instructed it directly, naming PR #2227. Every session uses one GitHub account, so the pull request's record cannot show who merged. The Ratification line now gives the merge commit, the Operator's words as the ProjectManager reported them, the Architect's ruling that this was a one-merge variance, and what counts as evidence of who merged and of who approved. The Status line says the amendment is in force.