Skip to content

Releases: mikeparcewski/wicked-studio

v0.5.12

Choose a tag to compare

@mikeparcewski mikeparcewski released this 17 Sep 12:42
e4b3b45

Built against wicked-crew-api-types 0.38.0 (unchanged since 0.5.11).

  • System seat cards: opencode provider login + Log out action (#293). not_required seats that carry a login_invocation now offer provider Sign in (the free_tier note demoted to a small secondary line below the row); a new Log out action runs the command-in-terminal through the same terminal modal, with the logout line derived client-side from login_invocation (trailing loginlogout, env prefix preserved; underivable ⇒ no button). UI-only — no new daemon route.

v0.5.11

Choose a tag to compare

@mikeparcewski mikeparcewski released this 17 Sep 12:42
866abe0

Built against wicked-crew-api-types 0.38.0 (unchanged since 0.5.10). FIX-IT-ALL wave 3 for the studio: #290 (L8 — escalation copy table, repo-check classification render, wire-fact readers, full-diff merge-base, one Send predicate, workflow round-trip, System auth rows) · #291 (L5 — the picker reads the daemon's chat_admission, an ok reply is the answer, Retry re-seats a refused seat). Bundled by wicked-crew 0.7.36.

  • Chat: an ok reply is the answer; the seat picker offers only the seats the daemon would seat; a refused seat gets Retry on the live chat (F-W1-004 / F-W1-005, wave-1 P6 gate; R-L5-2, R-L5-3).

    • finalizePending: an ok: true chatReply IS the bubble (core-ts ≥ 0.7.27 makes chatReply.text the block after the seat's last tool call — the "Let me explore… Now let me read…" narration streamed as deltas and is not repeated); a NOT-ok reply (an eviction's budget sentence + partial) keeps retainOnFinalize's longer-text rule, so nothing streamed before a cut is lost (E4). On an older engine the reply ⊇ the stream — unchanged.
    • [+ Add] and the default chips read the daemon's chat_admission verdict for the CURRENT scope mode (crew ≥ 0.7.36 GET /roster; the same predicate its POST /chats pre-filter runs — one source of truth, no client-side copy of the rule; the F-W1-003 decision lands in that verdict daemon-side). Seats the daemon would refuse are not offered; one line under the picker names them with the daemon's reason. A pristine selection re-seeds when the scope mode changes before the first send. Without the verdict (older daemon) the picker behaves as before: every seat offered, the incapable ones labeled "no chat config".
    • A failed seat chip on a live chat carries RetryPOST /chats/:id/seats (the engine's per-seat ensure in the recorded scope; same chat id, same pool key) — the chip folds the answer (ready / failed with the daemon's reason, narrated), a re-seated seat rejoins the next send's audience; a daemon without the route says so on the chip.
  • Escalation gates say what happened, from the engine's own class × denying layer (fixall L8-8E(i);
    crew #559 / F-RC1-047 = F-RC2-061; DES-L8 r2 §5 PR-8E).
    One copy table (denialCopy.ts escalationCopy(condition, denialSource, facts)) shared by the narrator feed and the run timeline:
    boundary_deny × {input_governance, ""} (the hook-veto arm whose source the engine folds away) reads
    "a command was refused by governance: <cmd>" — a governance-refused ls no longer reads "tried to
    write outside its workspace"; evaluator_mutated_worktree branches on restored (restored + N paths
    discarded + the suggestion ref, or "could NOT be restored; inspect the worktree"); dead_seat,
    floor_failed × {repo_checks, repo_checks_timeout, pinned_validator, substance, deliverables},
    verdict_not_pass × {agent_validator, worker_failure, evaluator_verdict} each have their sentence;
    defGate / outputCaptured become footnotes; an unknown pair keeps today's wording (nothing is
    guessed). denialSourceLabel learns repo_checks_timeout, dead_seat, evaluator_verdict.

  • Repository checks render the engine's head-vs-base classification (D3 of the benchmark review;
    api-types 0.38.0 RepoCheckRun.classification/preExisting/regressions).
    GateFloorCheck carries
    the three fields; checkOutcome reads regression as "regression — N new failure(s)" (red) and
    pre_existing_in_sandbox (or the legacy floor_env_mismatch) as "failed on head AND base —
    pre-existing, not this change" (not red); a frame without the field reads exactly as before.

  • Wire facts that had no reader now render: the unit header gains discipline: <name> §<role> from
    unitDispatched.baseSkill, with "discipline named only (not handed)" ONLY on handed === false
    (absent ⇒ unknown, core #479 / studio #275); sandboxPosture (F-E2E-039) and worktreeRetained
    (F-RC1-064) frames get timeline rows; the narrator reads the watchdog's workerStalled.quietForMs
    when stalledSecs is absent (no more "quiet for ?s") and narrates workerStallEscalated — "Needs
    you — worker silent N min — automatic recoveries spent" (studio #284 / F-BM-006, the minimal live
    render; the reload-safe needs-you card needs crew to persist the frames — coordinator to place).

  • Full diff asks for base=merge-base (fixall L8-8E(ii); studio #244; BC-54). api.getRunDiff
    gains a third argument and the run page's Full-diff viewer sends it: committed + uncommitted run work
    vs the fork point, instead of the worktree vs HEAD (which showed only uncommitted files).

  • Send and its line agree by construction (fixall L8-8E(iii); F-089 = F-E2E-035, F-RC2-005/024/045).
    The composer's "Ready / Not ready to send" prefix reads the SAME canSubmit predicate that disables
    the button. "Revise PR #N" pre-fill (DES-L9 §5): RetryPrefill.revisesPr deposits the PR; the
    body carries revisesPr ONLY when GET /health.capabilities.revisesPr === true (else the chip reads
    "this daemon cannot revise a PR — upgrade wicked-crew" and nothing is sent), with deliver: 'pr'.

  • Saving a workflow round-trips skill_ref / allowed_skills / required_deliverables /
    validator_pin (fixall L8-8E(iv); F-RC1-093).
    One def→builder mapper (builderPhaseOf) feeds the
    editor and the JSON import; buildDef writes the four fields back verbatim instead of nulling them.

  • System reads seat standing off auth, offers Re-authenticate, and names the daemon's settings
    path (fixall L8-8E(v); F-E2E-040 = F-RC2-043, F-004/013/010, studio #280 item 4; BC-56).
    Seat rows
    use the health rail's seatStandingWord fold (signed_in / signed_out / not_required (free tier)
    / unknown); a seat whose own stderr reported the failure (auth_source: 'seat-stderr') reads
    "sign-in failed: " and its button says Re-authenticate; the page shows GET /settings.path (crew 0.7.36) instead of a hard-coded home-relative literal, and says when the
    daemon does not report it.

  • Run clocks prefer the run's own record (crew #496 / studio #230; BC-52). runWhenWord takes
    AgentSession.created_at (the sidebar and the command palette pass it); runEndedWord renders
    ended_at as "finished N ago" and null for an undated run — never derived from now.

  • Code Graph tile reads "150 of 5,470 shown" (crew #505 / F-RC1-100 / F-E2E-022; BC-53) from
    CodeGraphData.totals when the daemon sends it; the slice alone on an older daemon.

  • Housekeeping: the Testing page's gap hint drops the embedder clause (F-E2E housekeeping, TestingPage).

v0.5.10

Choose a tag to compare

@github-actions github-actions released this 14 Sep 13:15
1085cb4

The published bundle is built against wicked-crew-api-types 0.38.0 — the exact
devDependency pin bumped from 0.37.0 in #283 (the wave-1 train's one api-types release; additive:
ChatDetailResponse.messages, UnitDistributedEvent.distinctnessFallback,
GateEvaluatedEvent.evaluatorVerdict, the skills unchanged / claude-dispatch / baseSkill shapes),
with both wire mirrors re-vendored from the published index.d.ts. FIX-IT-ALL wave 1 for the studio:
#283 · #285 · #286 · #287 · #288 — plus #273, #281 and #282, landed since 0.5.9. wicked-crew 0.7.35
bundles this dist as its default local skin.

Added

  • Documents root control on the project dashboard (#279). The one lever that isolates a project's documents — interactiveRoot — was API-only. The header's meta region now shows the project's binding (or "the daemon's default root — this project's own partition") with Set… / Change… / Clear, through the new setProjectInteractiveRoot (api/wave6-wire.ts) over crew's existing PATCH /projects/:id {interactiveRoot}; the daemon's refusal is shown at the control and the docs tile re-lists off the new root. The default project is read-only here (the route refuses it).

Fixed

  • The "Capture learnings" card files its run under the ambient project (FIX-IT-ALL L4-⑩; F-RC1-049 /
    F-E2E-015).
    From a project page the capture-learnings launch now carries projectId: <ambient project> on the existing launchRun wire (LaunchRunBody.projectId, api-types 0.38.0), so
    the run lands under /p/<proj>/… like every other launch from that page; from the flat /repos
    page no projectId is sent and the run stays Unfiled honestly (there is no repo→project map to
    guess from). The button title says "filed under the current project" when it applies.
  • Skills page: Publish re-reads the engine line, and an unchanged publish says so (fixall L6-4a; F-RC1-017 / crew#547 item 3; DES-L6 r2 §5 PR-L6-4). After an applied Publish the page now calls loadEngine() explicitly: the catalog re-read refreshed the engine line only when it succeeded, so a failed re-read left "generation N" on the previous generation until a page reload. A publish that answers unchanged: true (api-types 0.38.0 — the daemon minted nothing because the tree hashes to the current generation) reads "Unchanged — generation N is still current (…); nothing was re-published." instead of announcing a new generation. The claude-dispatch portability copy (typed ahead of its detector in 0.38.0) is edited to the operator wording the design pins — "invokes a Claude-only tool (Task/Skill/AskUserQuestion) — a dispatch no other seat can follow" — not re-added (review-L8-283 N3).
  • Seatless-run failure card: headline truncated at (Failed): and "sign a seat in" remedy shown for tool-only failures (F-E2E-014, refs #272).
    cleanPrompt split on the first [, which in a triage-escalation prompt is the opening bracket of the engine's cause — so the cause was demoted to the collapsed "why this gate fired" disclosure (starting mid-token, since slice(bracketIdx + 1) stripped the bracket) and the headline stopped at (Failed):. The ReassignControl lever rendered for any failure escalation regardless of assigned_cli, so a run that never had a seat was told to retry on another seat / sign one in.
    • cleanPrompt now keeps the leading [ in the extracted footnote text (slice(bracketIdx, …) not slice(bracketIdx + 1, …)).
    • SteeringGate skips footnote extraction entirely for escalation prompts (isFailureEscalation): the full prompt — cause included — renders in the headline, which now carries overflow-wrap: anywhere so a long unbroken cause wraps instead of overflowing.
    • isSeatFailure(escalation, failedCli) (new predicate in gateVerdictModel.ts) gates ReassignControl in both SteeringGate and CenterDashboard: a PROVEN seatless escalation (failedCli === null — the unit is known and has no seat) renders no seat lever; a seat failure keeps the existing lever and Approve label unchanged; a host that cannot resolve the unit's seat (failedSeatOfundefined: no units passed — the steering-author and testing-launch panels, the landing inbox before the run is loaded) keeps the lever as before, never reading "unknown" as "seatless" (#274, found by the independent review of the first cut).
  • Chat: the send targets the seat chips minus the seats refused at open, so an evicted seat is re-seated by the next message; chatSeatRefused renders; a rejoin replays the persisted transcript; a reply shows what it cost (DES-L5 wave 1 — studio#277 / F-RC1-114, studio#237 / F-RC1-115, F-RC1-111).
    • GroupChat posts targets on every POST /chats/:id/messages (client.ts already accepted them): the header's seat chips minus refusedRef — the 201's refused[] + ok: false outcomes, GET /chats/:id.refused on a rejoin, and every chatSeatRefused frame. A seat the engine EVICTED (a turn over its budget, a dropped session) stays a greyed chip and, named in targets, is re-warmed by the engine on that send — was: the warm pool only, so an evicted seat never came back (P6: claude evicted twice, the chat quietly went on with one seat).
    • NEW case 'chatSeatRefused' in the frame switch: "not seated: ()" as a fail-tone narration line with the seat chip, the chip greyed with the same reason, the seat out of the audience.
    • Rejoin replays GET /chats/:id.messages (api-types 0.38.0, crew ≥ 0.7.35) as bubbles — turn ordinals by first-seen turnId, the send counter continues — and the boundary note reads "Rejoined — N earlier messages restored"; a seat that spoke but is no longer warm keeps a failed chip wearing its last reason. An older daemon (field absent) keeps today's wording and an empty log.
    • chatReply.usage (additive; null on pi/agy) lands on the SeatMsg and renders as the bubble's footer (12.3k in · 800 out · $0.04; no $ when costUsd is null; cache counts on the title). The composer carries the static budget copy once seats are warm ("Replies are budgeted per turn; …", no number — the engine's WICKED_CHAT_TURN_SECS is env-only).
  • Project dashboard DOCUMENTS tile stayed "0 — No documents yet" for any project without its own root binding (#233, F-048; DES-L7 §5 I3).
    ProjectDashboard returned early from the listDocs effect when interactiveRootOf(project) was null — but the daemon resolves every project's root itself (the binding, else WICKED_INTERACTIVE_ROOT, else the project's own partition of the default root), so a default-partition project's documents were listed by the bridge and invisible on its page. The effect now waits only for the project row.
  • A recording that fails over the bus is now visible on the storyboard (#278, the live half).
    video-record-error rendered only the POST's own catch; the thread store already folds the bridge's status.posted {state:"error"} into lastError (live over /ws, or hydrated from GET /api/conversation after a reload), and the storyboard never read it. VideoStoryboard now renders lastError[key] unconditionally beside the request error (same data-testid="video-record-error", data-source="thread"), hidden only while a new attempt is in flight. The persistence half (the owning bridge writing the line) landed in interactive 0.9.3.
  • Disclose distinctnessFallback: 'creator_seat' beside unit routing and in the run-head note, even when no seats were benched (#276). Include any degraded reason; older daemons with an absent or null fallback retain existing rendering.

Security

  • site: patch Astro AVIF/SVG advisory chain (#227). site/package.json lifts the Astro
    constraint from ^7.1.3 to ^7.2.8; npm resolves to 7.3.2, patching
    GHSA-26w7-cxv4-gfx2 (Astro < 7.2.8 Sharp/libheif
    AVIF RCE). The updated Astro tree also pulls svgo 4.1.0, patching
    GHSA-w27v-7q3p-w38r and
    GHSA-4vpr-x523-8j87 (svgo removeScripts
    SVG sanitisation bypasses). npm audit reports 0 vulnerabilities after the update.

v0.5.9

Choose a tag to compare

@github-actions github-actions released this 12 Sep 17:55
37aa93c

The published bundle is built against wicked-crew-api-types 0.37.0 — the exact
devDependency pin bumped from 0.36.0 in #270 (the deliver-gate wire: HealthResponse.capabilities.deliverGate,
LaunchRunBody.deliverGate, AgentSession.auto_deliver); this cut lands the Archive control (#268, the
harness-delivered fix for #219) and the composer's deliver-gate posture (#269, F-E2E-030).

Fixed

  • Archive control for terminal runs in the run header and on WorkPage rows (#219, refs #211).
    Studio could unarchive a run (crew#265) but never archive one — archiveRun(id, false) was the
    wire's only caller, so a terminal run left the active work list only through the API and the
    seed-surfaces suite's RUN-ARC journey (#211) had to run [SUBSTITUTE]. The run header of every
    terminal run (completed / failed / cancelled, on both /runs/:id and /p/:pid/build/:runId)
    now carries Archive (run-archive) in the slot Cancel occupies on a live run: confirm-gated
    (run-archive-confirm, Yes / Keep, Escape = Keep), refusals surfaced inline as role="alert"
    (run-archive-error) with the confirm held open, and on success the run index refreshes and the
    view navigates back so the run leaves the list. Every terminal Work row — the Completed / Failed /
    Cancelled groups on the All tab and the filtered Completed / Failed / Cancelled tab views — gets
    an inline Archive button (run-archive-row) beside the run, the shipped Unarchive-row pattern;
    Active rows and live runs get nothing. Both paths POST /runs/:id/archive {archived: true};
    Unarchive is unchanged.
    • Review findings landed in-wave (independent review of #268, M-1 / L-1 / L-2): this entry; the
      header offers no Archive on an already-archived run (session.archived_at set — reachable
      through the Archived chip), so there is nothing to re-archive; and the WorkPage tests now cover
      the filtered tab views and the Failed / Cancelled groups, not only Completed.
  • The composer promised a push with no gate (acceptance finding F-E2E-030). Under the default
    "First gate" posture the deliver notice read "When this finishes it pushes its branch → opens a
    PR" — and that is exactly what run 0ab5ccb8 did, unattended. The engine now gates the deliver
    phase by default (wicked-core#456) and crew accepts deliverGate: 'human' | 'auto'
    (wicked-crew#543). The composer says WHEN the push happens: the default postures read "pauses
    at the deliver gate; approve it and the run pushes its branch → opens a PR on ", the
    confirm line gains deliver: after you approve the deliver gate (launch-confirm-deliver), and
    the body sends NO opt-out. Only the explicitly unattended postures — Autonomous, or the gate
    option now labelled "No gates · auto-deliver" — send deliverGate: 'auto', and their notice
    says "with NO deliver gate — this posture is auto-deliver". The intake plan's deliver row names
    the gate from session.auto_deliver ("human gate before it pushes its branch + opens the PR" /
    "auto-deliver — … no gate"; intake-plan-deliver-gate) and stays silent on an engine that
    predates the gate, so no promise is made that the engine cannot keep. The composer makes the
    same promise only when the DAEMON can keep it: it reads GET /health.capabilities.deliverGate
    (crew ≥ 0.7.33) and, against a daemon without it, says "this daemon delivers WITHOUT a deliver
    gate (upgrade crew to 0.7.33+ to confirm the push first)", offers no auto-deliver option and
    never sends deliverGate (the older launch schema rejects it). "No gates" is labelled
    auto-deliver only where the select is honoured (not in Ask mode, where every unit is gated).

Changed

  • Pin wicked-crew-api-types 0.37.0 (the deliver-gate wire, crew#543) and re-vendor both wire
    mirrors from the installed index.d.ts. Built against api-types 0.37.0: HealthResponse
    (GET /health, previously undeclared) with capabilities.deliverGate, LaunchRunBody.deliverGate: 'human' | 'auto', and AgentSession.auto_deliver — the three shapes #269 hand-declared as
    "≥ 0.37" in src/api/client.ts (getHealth) and src/api/types.ts (LaunchBodyWithDeliver) now
    come from the package, and tests/deliverGateWire.test.ts pins them to the installed index.d.ts
    (compile-time satisfies + the declaration lines) so a pin that loses them fails the suite. 0.37.0
    is ADDITIVE and touches neither the skills block nor the wave-6 shapes: all 16 VERBATIM regions of
    src/api/skills-wire.ts (+ its fixture, now tests/fixtures/api-types-0.37.0-skills.d.ts;
    index.d.ts:1995-2445 / 4720-4779) and src/api/wave6-wire.ts are byte-identical to 0.36.0 and
    relabelled by line range only (+26 above the LaunchRunBody addition, +38 below it).

v0.5.8

Choose a tag to compare

@github-actions github-actions released this 12 Sep 02:21
cb1e672

The published bundle is built against wicked-crew-api-types 0.36.0 — the exact
devDependency pin, unchanged from 0.5.7 (#264); this cut lands the fix that reads the test_sets
that wire actually declares (#266).

Fixed

  • The Test landing reads the daemon's top-level test_sets (api-types 0.36.0) — counts, PLAN and
    PR per produced set.
    The 0.5.7 landing (CampaignsPage, the Home "Test" door, campaignStats)
    read a PROVISIONAL row-level Campaign.test_set / RunGroup.test_set join that 0.36.0 never
    declared, so against a 0.36.0 daemon no card showed a produced set. The daemon serves the sets as
    CampaignsListResponse.test_sets: TestSet[] (snake_case, run_id-keyed, tagged with the
    qe-tests-<repo> label an authoring run is filed under); listCampaigns now normalizes them
    (testSets: null = a pre-0.36 daemon — absence, never a fabricated zero), the store holds them,
    and the fold joins them onto each campaign / label-group card by run_id (and by label for a
    group). Each set renders its verified chip, produced · executed · passed · failed (plus
    "· N not executed" when the verify phase left tests unrun), the PLAN (opens the producing run)
    and the engine's PR (isPrUrl-gated); the Tests tile's context and the Home door append the
    registered sets once the wire carries them. The provisional join, its testSetOf row reader and
    the dead POST /testing/recon + workflow ladder rung (qe-author-tests shipped together with
    POST /testing/author, so no daemon lists the workflow without the route; 0.36.0's
    TestingReconBody declares no workflow key) are deleted — tests/wave6Wire.test.ts now guards
    that neither shape returns. Fixtures (tests/fixtures/wave6.ts, e2e/uxfix_fixture.py) serve the
    real 0.36.0 shape; the testid inventory gains campaign-card-testset-{verified,pr,more}.
    • Review findings landed in-wave (independent review of #266, F-1..F-4, R2-1): the Tests
      tile's context now LEADS with the sets word — painted as 1 set · 11/11 passed, sized to clear
      the glyph at 1440 px, with the unabridged 1 test set · 11/11 passed … line as the span's
      hover title — the redundant "N ad-hoc group" word is gone, and every StatTile context
      carries its full text as title (stat-context; a contextTitle prop when the painted line
      is an abridgement); sets no card can show are said as · N unattributed and test_sets rows served without a run_id as · N malformed (never
      folded away — listCampaigns now returns malformedTestSets); a 0.36 daemon's real zero
      renders as no test sets registered yet on the tile and N tests · 0 test sets on the Home
      door, while a pre-0.36 daemon still says nothing about sets; the launch panel reads
      /testing/author's runs[].label and says filed under qe-tests-<repo> on the Test landing — the set fills in when the verify phase registers it (testing-launch-filed-label) instead
      of "appears … when the run registers its test set". The Chrome rig asserts the sets word is
      VISIBLE (glyph box inside the context span), not merely present in textContent.

v0.5.7

Choose a tag to compare

@github-actions github-actions released this 11 Sep 23:46
15fd4a8

The published bundle is built against wicked-crew-api-types 0.36.0 — the exact
devDependency pin on this cut (#264), and the wire the bundle's mirrors and satisfies checks are
typed against: both wire mirrors (src/api/skills-wire.ts, src/api/wave6-wire.ts) are byte-pinned
to it. Two wire gaps (a row-level Campaign.test_set / RunGroup.test_set join;
TestingReconBody.workflow) stay studio-worded and test-guarded — see Changed below.

Added

  • New test launches the governed qe-author-tests workflow; the Test landing shows the produced
    set; honest UNGATED / degraded gates; a files view once the worktree is gone
    (wave 6 — the
    governed testing journey: acceptance findings F-075 / F-076 / F-7R2-003 / -005 / -006 / -008 /
    -009 / -010 / -011 / -012 / -013 / -014 / -017, studio half). Develops against a PROVISIONAL wire
    mirror (src/api/wave6-wire.ts) spelled exactly as the wave-6 briefs name the fields; every
    reader is null-safe, so an older daemon changes nothing. tests/wave6Wire.test.ts pins the
    posture: a pin bump to ≥ 0.36.0 without a VERBATIM re-vendor fails the suite (the #257 pattern).
    • Routes (F-075 / F-7R2-009): /testing and the retired /testing/harness land on the TEST
      landing (/testing/campaigns), as src/api/testing.ts documented all along; Evals keeps
      /testing/evals as a sub-page; the rail heading follows. Home's "Run recon" verb is now
      New test and opens the launch panel (?new=test). "Add with chat" on the landing is
      labelled Add testing rules — it authors testing STEERING RULES, not tests.
    • New test is governed (F-7R2-003 / -004 / -012): the panel reads GET /workflows on mount;
      a daemon that lists qe-author-tests gets the governed launch (the chip names the five
      phases: recon → author → verify → review → deliver — the ENGINE's deliver phase opens the PR,
      never the worker), and launchGovernedTest walks the wire ladder, each step only when the
      previous wire is ABSENT: POST /testing/authorPOST /testing/recon + workflow (a strict
      schema naming it unrecognized ⇒) → one POST /runs {workflow, repoRef, projectId, humanConfirm: 'before:1'[, groupLabel]} per resolved repo. A daemon that lists no such
      workflow shows the honest banner "this daemon has no governed test workflow — plain run"
      BEFORE the launch and takes today's free-text recon. Every named refusal surfaces untouched.
    • Project chips are droppable (F-076 / F-7R2-010): "attach the project, drop repos". A
      narrowed project launches one POST /runs per remaining repo — repoRef scopes, projectId
      FILES — so an explicit single repo keeps its project_id (the pinned recon body's projectId
      would union the dropped members back in); the dropped line names them and offers "restore
      all"; every member dropped is refused on the button, before any wire call.
    • After launch (F-7R2-011): the panel LINKS every launched run (testing-launch-fanout-run,
      the single run too), names the workflow, the wire it rode (testing-launch-route) and the
      test/group label, then the waiting line; the intake gate arrives on the app's one /ws fold.
    • The intake card shows the PLAN (F-7R2-008): on the pre-run gate for the run's first unit —
      the panel's copy of the card and the run page's — IntakePlan lists every planned phase with
      its executor (agent / tool), skill, writes code, evaluator ≠ creator, and seat (the one
      routed, else "council picks from "), read once off GET /runs/:id when the gate arrives.
    • The Test landing shows the produced set (F-7R2-014): a card's test_set (the campaign
      registration a completed run lands) renders "N test files · T tests · E executed · P passed ·
      F failed" — the counts the VERIFY phase re-derived, with "K never executed" when
      executed < tests — plus the PLAN path; the workflow chip reads qe-author-tests off the live
      runs from launch. A pre-0.36 row renders no counts (absence, never a fabricated zero). The
      empty state says what fills it.
    • Honest UNGATED gates (F-7R2-005 / -017): gateEvaluated.ungated / ungatedReason win over
      the card's fold — the gate card reads "UNGATED — no eligible judge seat" (the floor that DID
      run is still listed; the judge axis is said not held), the run-page verdict card carries the
      same line, and the narrator says "Gate UNGATED on — ; repository checks ran,
      no distinct judge" — never "Checks ran — pass" for a judge-less gate.
    • Degraded councils (F-7R2-006 / F-4R2-007): unitDistributed.degradedReason renders on the
      run head (run-degraded: "council degraded: 4 of 5 seats benched: …", with the affected-unit
      count) and on the routing line in the feed. The narrator now reads the camelCase
      agreementPct the engine actually emits (api-types ≤ 0.35.0 declared agreement_pct, which
      the wire never carried — so the pct was always missing); the snake_case read stays as the
      fallback until the 0.36.0 pin declares camelCase.
    • The remote-write fence (F-7R2-012): a workerToolCallDenied (a creator/evaluator seat's
      git push / gh pr create …) renders in the feed with the seat, role, the refused command as
      code and the remedy — the engine's, or "delivery is performed by the run's deliver phase".
    • Files view once the worktree is gone (F-7R2-013): GET /runs/:id/diff answering
      source: "branch" is labelled as the run branch vs its base (committed work shown; an empty
      branch says so); a pre-0.36 daemon's 409 cause card names the run branch and the upgrade. The
      run page's Files section offers Full diff on EVERY state — the empty one included, which
      was exactly the completed run with no files view.
    • Tests: the launch ladder (22), the panel (14), the gate model + cards (9), the narrator (13),
      the files view (6), the landing card (8), the run head (6), the mirror posture (7); the
      Playwright loopback rig e2e/governed_testing_test.py (fixture switch governed_testing /
      governed_testing_workflow_absent: GET /workflows, POST /testing/author + the intake gate
      over /ws, GET /campaigns with test_set, the completed run's degraded / UNGATED / refused-write
      trail, the branch-source diff) at 1440x700 and 400px; testid inventory regenerated. Wire gaps
      recorded for the crew PR: the recon body's projectId cannot express a narrowed project; the
      campaign registration shape (test_set) and the /testing/author route are provisional names.
    • Pin: wicked-crew-api-types 0.35.0 exact (crew#533 — published while this landed; 0.36.0,
      the wave-6 wire, was not). The skills mirror + tests/fixtures/api-types-0.35.0-skills.d.ts are
      re-vendored by label (the 0.34.0 skills and diagnostics.skills blocks are byte-identical in
      0.35.0, shifted to index.d.ts:1768-2186 / 4223-4272); two additive catch-ups — the Health
      rail names the widened info finding severity, the wave-2 fixture's legacyOutbox carries the
      new required scope. The wave-6 mirror stays PROVISIONAL under 0.35.0 (none of its names are
      declared there — tests/wave6Wire.test.ts asserts exactly that).
    • Skills page recovery from GET /skills 503 (acceptance findings F-A45-001 HIGH / F-A45-002
      MEDIUM — the F-083 stale-rules refusal "current does not point at a valid published snapshot …
      re-publish or remove the link"). The unavailable card used to offer only Refresh (a second 503);
      the remedy the finding names was unreachable. It now carries the engine's word — GET /diagnosticsskills.state + every findings[] message (skills-recovery-finding) — and two
      controls with pending/result states: Refresh baseline (POST /skills/refresh-baseline) and
      Publish (POST /skills/publish). Every mutation is CAS-guarded by the revision the 503
      withholds, so the page learns it through POST /skills/analyze (the dry run reads the MANIFEST,
      not current) and says so when analyze 503s too (the manifest itself is unreadable — the
      daemon host's job). After a Refresh the baseline is STAGED and the catalog still answers 503
      until Publish: the result renders inline ("garden 12.33.0 staged (… taken · kept · added ·
      removed · conflicts) — publish to activate"), the engine line is re-read, the catalog is NOT
      (F-A45-002). A Publish that writes a snapshot re-reads the catalog and flips the page to the
      loaded state with the note; a blocked publish renders its findings on the card. A 409 says the
      catalog moved and re-learns the revision on the next click.
    • One roster story on the composer and the rail (F-A45-006 studio half). The composer's seat
      warning derived from the signed_in file/env heuristic alone, so it said "codex + opencode
      aren't signed in" while the Health rail — reading crew#533's auth / council_eligible /
      free_tier — showed opencode green "no sign-in needed". Both now read the rail's
      seatStandingWord: auth: not_required is never a sign-in problem, auth: signed_out warns
      even when the heuristic is null, a daemon-declared council_eligible: false gets its own
      sentence with the daemon's reason (ineligible-warning); a pre-0.35 roster keeps the heuristic.
    • /vibe and the Home door count what the daemon serves — without spawning a bridge per project
      (F-A45-008 MEDIUM, bounded by the independent review of #263, F-1/F-2). The corpus listed only
      "projects opened this session" (the docs cache's deposits), so a fresh browser on a daemon holding
      three documents read "DOCUMENTS 0" and Home said "Vibe 0 documents". A per-project docs GET
      (GET /projects/:id/interactive/api/docs, the only per-project route) MATERIALIZES the project's
      partition and cold-starts one wicked-interactive bridge (~60 s) — so NOTHING fans out on mount.
      The one request the corpus su...
Read more

v0.5.6

Choose a tag to compare

@github-actions github-actions released this 11 Sep 16:11
7493380

The published bundle is built against wicked-crew-api-types 0.34.0 — the exact
devDependency pin on this cut, and the wire the bundle's mirrors and satisfies checks are typed
against. The crew#533 roster fields named below (auth / free_tier / council_eligible /
council_ineligible_reason, api-types 0.35.0) are read defensively when a daemon sends them;
the pin itself moves in a later release.

Added

  • Reassign to + retry on a failure-escalation gate (phase7-r2 acceptance finding
    F-7R2-007, HIGH). At every "Unit N failed and triage escalated" gate the card offered Approve — a
    retry on the SAME dead seat — Approve + steer, Reject and Cancel; recovery was
    POST /api/v1/runs/:id/reassign {cli} by hand, five times, racing the re-dispatch window. Both
    gate cards (the run page's SteeringGate, the landing inbox's card) now carry ReassignControl:
    the run's OTHER seats (session.clis minus the seat that failed the unit) with the roster's word
    on each — signed-in first, a seat with no sign-in observed hedged as "may fail or be benched"
    (today's roster carries no council-eligibility field; crew#533's auth / council_eligible /
    council_ineligible_reason / free_tier are read when a daemon sends them — seatStanding),
    inactive last, daemon-declared ineligible after that — and one action that approves the retry
    (the steer text rides it), waits for the run to resume (GET /runs/:id until executing, 30 s
    bounded — the daemon reassigns only an executing run), then calls the existing
    POST /runs/:id/reassign {cli} (api.reassignRun). Every step is stated
    (steering-reassign-status); a refused reassign leaves the approve standing, shows the daemon's
    sentence and offers the reassign alone again. Plain Approve is relabelled "Approve (retry on
    )" on that gate. A host without the run view (the steering-author and testing-launch panels)
    reads the run once for its pool on a failure escalation only. Recorded on the steering timeline as
    reassign. (Wire gap, recorded: the reassign route refuses an awaiting_human run, so the approve
    must precede it.)
  • The wicked-core#431 wire on the gate, the delivery card, the run head and the feed (#250/#431
    consumer follow-through — pins wicked-crew-api-types 0.33.0, the wire wicked-crew#527 publishes).
    Every field is read off the frames the daemon sends and rendered only when present, so an older
    daemon changes nothing.
    • Gate card — the judge seat (gateEvaluated.judgeCli / judgeDistinct): the verdict header
      names WHO judged (· judge: codex); judgeDistinct: false adds a same-seat warning — the judge
      fell back to the single default runner, so evaluator ≠ creator is not held on that verdict.
    • Denial card — the restored tree (evaluatorMutatedWorktree.restored + worktreeRestored):
      a worktree-guard denial the engine already remedied says "the evaluator's edit was discarded and
      the creator's verified tree restored", lists the discarded paths from the restore record, and
      gives git show refs/wicked/suggestions/<run>/<ord>/<attempt> as copyable code — with a real,
      keyboard-reachable copy button beside it — when the edit was pinned (an honest "not pinned"
      otherwise). Approve is relabelled "Retry against the restored tree" (and "Retry + steer") on
      exactly that gate, on the run page's gate card AND the landing inbox's card, from one predicate
      that mirrors the engine's own guard (denial.source === 'worktree_guard' and restored) — keyed
      on the evidence frames, not on the prompt, which the engine also changed ("confirm to retry the phase" → "Approve to retry the phase
      against the restored tree"; the card's NOT PASS match holds for both spellings). A failed
      restore (restored: false) is said, with the engine's error, and the manual remedy stands.
    • Delivery card / deliver gate — the lift (deliverLiftEvaluated, the deliver ord's
      repoChecksEvaluated, the deliver unit's deliver: refusal): the rail's Delivery body and a
      gate opened on the deliver unit render what the pre-push lift did — unchanged / lifted
      (base and tree before → after, the re-verify per check with the forced-install source
      package-lock.json (forced: lockfile drift)) / conflict (the files, "nothing was rebased and
      nothing was pushed", the LIFT-CONFLICT remedy) / skipped / failed — and the engine's
      refusal as the wire carries it (stepFailed.detail is a head+tail excerpt; the elision marker
      renders dimmed between the kept words), once: the gate card omits its copy when the engine's
      triage-escalate prompt already quotes it, the rail when the rejected unit's framed
      denial_reason does. A red check exposes its recorded stderr / stdout tail (RepoCheckRun,
      declared since 0.31.0) as a collapsed, monospace, phone-width-wrapping block — on the deliver
      lift and on the gate card's floor. A deliver unit refused BEFORE the lift (a HEAD off the run branch)
      has no lift frame and renders its deliver: text on its own; a passed: false re-verify over
      all-green rows is explained as the checks having CHANGED the worktree.
    • Run head / timeline — the base (runBaseResolved): a base row on the run's context card and
      a based on head row on the evidence timeline — "origin/main @ f57069d · 5 behind · lifted to
      the tip" — plus timeline rows and detail panels for the restore, the lift and a refused write.
    • Feed: narration lines for the run base, the creator-tree restore, each lift outcome, a refused
      write-class tool call (evaluatorToolCallDenied) and the one deliberate ACP reroute
      (acpFallback.fallbackKind: 'read_only_requires_wrapped' — routing, not a failure); every other
      acpFallback kind stays silent as before.
    • Tests: unit suites over synthetic frames in the wire's exact spelling (tests/fixtures/wire433.ts,
      mirroring wicked-crew's wire-contract literals) for each surface — every frame declared satisfies
      its 0.33.0 named type, and tests/wire433.shapes.test.ts re-derives the key-set and union diff
      against the installed index.d.ts at run time; the deliver fixtures carry what the WIRE carries
      (stepFailed.detail as the engine's 150/250 head+tail excerpt, the triage-escalate prompt quoting
      the 450/750 excerpt, denial_reason framed as Worker FAILED on unit N …); the loopback rig
      e2e/wire433_test.py (fixture switch wire433) drives the three surfaces in a real browser.

Changed

  • Skills page: a badge per KIND of portability reason, and the claude-only KPI split (#256,
    F-079 — pins wicked-crew-api-types 0.34.0, whose additive SkillEntry.portability
    {portable, reasons[], evidence?[]} is the publisher's per-reason verdict, crew#531). The one
    claude-only badge lumped "the author used a Claude-only path" together with "this skill needs
    the Claude harness" and hid the fix. Now any AUTHORING reason (plugin-root, skill-dir-var,
    cwd-script, relative-link, cross-skill-path) renders not portable
    (skills-not-portable-badge; the hover title lists the reasons and the first file:line
    anchor, and is the badge's accessible name) while requires-harness:claude alone renders
    needs Claude harness (skills-needs-claude-badge; title = the reason). The wrapper keeps
    skills-claude-only-badge for one release so existing selectors resolve; a daemon that predates
    the field (no portability) falls back to portable alone — the generic not portable badge
    with the previous sentence, never a fabricated reason. The Portable tile's context reads
    N not portable · M need Claude harness (the value stays the portable count); the chips
    not-portable + needs-claude replace claude-only; the drawer gains a Portability line —
    every reason with one clause of "why", and every file:line anchor as monospace text that wraps
    at phone width. skillCounts gains notPortable / needsClaude (portable + notPortable + needsClaude === total). The wire mirror (src/api/skills-wire.ts) and its parity fixture move
    to the 0.34.0 block (picking up the 0.29.0 installer-copy source kind and the skills.source /
    skills.manifest diagnostics findings the 0.27.0 mirror lagged). The Reach KPI group is as wide
    as the two-tile groups so the context line never ellipsizes, the badge's ink is --ink-high on the
    amber fill so it reads in both themes, and the page header + verbs wrap at phone width. Review
    follow-through: a non-portable analyze/publish finding wears its portabilityReason as a chip
    (hover = the reason's one clause), and a portability verdict that disagrees with portable is
    named — data-contradiction on the row and badge, a hint line in the drawer — never swallowed and
    never a different badge (portable stays the admission key).

Fixed

  • Document thread hardening (phase4-r2 acceptance findings F-4R2-003 / -005 / -006 / -014 / -016).
    • Export bar — readiness per format (F-4R2-016): ExportMenu looked up the FIRST ready answer for
      the version, so an un-consumed HTML download shadowed the PDF that finished after it — the PDF
      button spun for 120 s while the file already sat in the thread. Each format button now asks for
      its own (version, format) answer and flips the moment its own reply lands. The bridge's additive
      layout report (wicked-interactive#219: layout, layout_source, page_size, pages) is read
      null-safely off the export response AND the export.generated echo and rendered where present —
      "PDF ready — 2 pages · A4 portrait" under the row, on the anchor's hover text, and on the thread
      line; an older bridge that sends none of it changes nothing.
    • Heartbeat narration (F-4R2-005): crew's seams re-emit the current phase's line every ≤15 s, so
      one draft read as 39 narration rows ("Crew phase 2/3: wr...
Read more

v0.5.5

Choose a tag to compare

@github-actions github-actions released this 11 Sep 03:31
271bef0

Added

  • Wave-2 consumers: the repo card renders the engine's findings, the Health rail renders
    diagnostics.governance, and New Chat carries a scope control
    (#251, #246, #248 — pins
    wicked-crew-api-types 0.32.0, the wave-2 crew release).
    • Repo findings (#251, wicked-core#406 via crew#517): RepoEntry.findings[] renders on the
      Repositories fleet card, the repo detail header and the project page's repo rows — one labelled
      row per finding with the engine's own message. An in-tree .codegraph/ beside a live graph is a
      tidy-up warning; an in-tree graph with NO live graph (the F-024 checkouts after the upgrade) is an
      error whose row carries Re-run onboarding, wired to each surface's existing onboard trigger
      (POST /repos/:id/onboard); code_graph_root_unresolvable is an error naming the daemon-
      environment fault. Silent for a clean checkout and for a daemon that predates the field.
    • Governance in Health (#246, crew#495 / F-022): expanding the rail's Health section also reads
      GET /diagnostics and renders the governance block — the store path and which rule chose it,
      the record counts (an honest "engine cannot count" for null, never 0), the dead-letter fold
      (count as a floor when truncated, by type / by reason, the timestamp range, the outbox, the
      pre-fix HOME outbox) and every finding as a severity-styled row whose message carries the
      wicked-crew governance replay … recipe. A store: null boot and any error finding turn the
      heart red (and show the collapsed-header dot) and degrade the home board's Governed tile
      (fail-coloured, the reason underneath — never a clean percentage over evidence that is not
      landing); a warning degrades the heart to amber. A daemon without the block, or without the
      route, reads "not reported by this daemon".
    • Scoped New Chat (#248, crew#502 / F-067): the create window carries a Scope control —
      All project repos (the default once a project is bound: projectId alone rides the open and
      the daemon scopes to every crew.repo member), Choose repos… (a multi-select from
      GET /repos, loaded on that gesture; sends repoRefs by id) and Unscoped (an explicit click).
      An Unfiled chat with no choice does not open on send — the gap is stated on the row, nothing is
      posted, the draft stays. A scoped open with the default (untouched) seat chips omits clis, so
      the daemon admits only governed seats and the 201 re-seeds the chips; an edited selection rides
      as asked and refused seats say why. The opened chat states ChatOpenResponse.scope under the header: the
      repositories (names; paths on hover), read-only, whether a code graph grounds the seats and the
      daemon's reason when not, and any project member the registry no longer knows; a rejoin states
      ChatDetailResponse.scope, and a daemon that said nothing is reported as "not stated". The
      route's refusals render as inline sentences by status — 404 (every missing ref named), 400
      (an ambiguous name → name it by id), 409 (a daemon-side conflict), 501 (the engine predates chat
      scope, with a Continue unscoped fallback that mints a fresh unscoped chat outside the shell).

Fixed

  • Gate cards state the evaluator verdict they are asking about (#250, acceptance finding
    F-3R2-006 — the UI half of wicked-core F-036/F-039). The verify pre-run gate ("Approve unit 4
    before it runs") and the deliver gate showed only their prompt while the fix phase's PASS —
    criterion, deterministic floor, the judge's reasoning — sat in the already-hydrated event log;
    the DENIED gate said "Unit 4 verdict is NOT PASS — confirm to retry…" while the reason
    (evaluator≠creator, the changed path, the restore command) was only in an expandable thread
    line. SteeringGate now renders a gate-verdict block from the run's own gateEvaluated (the
    last one at or below the gate's ord), with the F-039 repoChecksEvaluated floor per check
    (name · exit code · duration · manifest source, plus what was skipped) and the F-036
    evaluatorMutatedWorktree record (seat, phase, changed paths, tree ids) attached from the SAME
    fold — a retry's verdict never inherits the previous attempt's evidence. A denial names the
    layer (denial.source: worktree guard, repository checks, …) and quotes the engine's reason
    verbatim, backticked commands rendered as copyable code. An ungated phase is labelled a
    default-allow, never a pass (FINDING-025); no evaluation yet ⇒ no block, never a verdict
    fabricated from the prompt. Zero new requests. wicked-crew-api-types 0.30.0 → 0.31.0
    (purely additive: UnitDenial, GateEvaluatedEvent.denial, RepoChecksEvaluatedEvent,
    EvaluatorMutatedWorktreeEvent). The judge SEAT is not on this wire, so the card claims none.
  • The landing's delivery strip no longer counts onboarding runs as "Vacuous — needs retry"
    (#250, F-3R2-018). The daemon stamps delivery: 'vacuous' on every completed repo-scoped run
    whose worktree is untouched — the DESIGNED outcome of onboarding and the other system
    workflows — so a fresh install read "9 Vacuous — needs retry" and buried the one real signal.
    deliveryCounts (shared by the strip and the KPI ribbon's Review tile) now licenses the
    vacuous bucket with the Delivery section's own canDeliver rule — a deliver unit on the run, or
    a workflow positively known not to be a system one (is_system, the one budgeted
    GET /workflows) — and the cell reads "Vacuous — no change to deliver": the condition, not a
    prescription. The licence can only withhold a count, never invent one.
  • .codegraph/estate.db is no longer tracked (#220). A fresh clone shipped the operator repo's code-graph
    identity, so onboarding the clone failed with REPO COLLISION (and an older wicked-core wrote into the
    tracked file); the graph is per-checkout, built by wicked-estate index under the daemon state home
    (wicked-core#406). The file is untracked (local copies are left on disk) and .codegraph/ is ignored.

v0.5.4

Choose a tag to compare

@github-actions github-actions released this 10 Sep 17:16
a7da6a1

Fixed

  • Document thread — bare status frames file under the doc's MOUNTED thread (acceptance finding
    F-045, belt and braces).
    Crew's own interactive seams narrated their governed runs with
    document_id alone, so every heartbeat was filed under the Unfiled mount while the project-bound
    thread heard nothing and, 90 s into a live run, showed "no worker has picked this up — the
    generation service may be down" with a Retry that would have injected a duplicate. Crew now stamps
    project_id on every seam emit (wicked-crew, F-045); independently, docThread.ingest files a
    frame that names a doc but no project under the project a DocumentThread is currently MOUNTED for
    that doc (bindDoc/unbindDoc, registered by the component — never inferred from retained
    history, which a previous same-slug thread would poison). The composer claims the doc for its
    project the moment the create is SENT — under the bridge's CANONICAL id (docSlug, the bridge's
    own DOC_NAME + slugify rule replicated byte for byte and pinned against observed ids), which
    is what every frame carries — as a pending binding the mounting thread adopts (released on a
    refused create), so a frame that beats the bridge's answer files on the project thread; a frame
    with no binding at all is HELD and released exactly once onto the thread that binds, expiring to
    Unfiled only when nothing is bound for 10 s (never while a pending create or an ambiguous pair
    is open; an unmount that leaves one thread releases the held frames to it). The stall banner
    therefore appears only after a genuine 90 s silence.

Added

  • Launch composer — what the document (or demo) is ABOUT and in what format (F-046, studio half).
    DocSubjectPicker offers the project's crew.repo members by name as toggles on BOTH the Document
    and the Video launch composer (sent on the create as repo_refs; crew validates them against the
    project and grounds the governed draft/demo run on THOSE repositories instead of the project's first
    member — the demo wizard carries them through demoDraftBody) and the bridge's four formats on
    both composers (sent as style, the demo flow included; "from the brief" sends nothing and lets
    crew infer it from the brief's format words, so a print/A4 brief reaches the bridge's print
    instructions). Discovery has visible
    loading / error states with a retry: the composer refuses to submit while the repositories are
    unknown, unless the user explicitly chooses to create without repository grounding — which the
    thread then records. The picks reset when the launch context changes and after a create.
  • Create body typed from the shared wire declaration. CreateDocBody is now
    wicked-crew-api-types 0.30.0's InteractiveDocCreateRequest (and DemoStepDraft its
    InteractiveDemoStepDraft) — no local mirror to drift. wicked-crew-api-types is pinned to
    0.30.0 exactly; the package publishes from wicked-crew on the F-045/F-046 merge.

v0.5.3

Choose a tag to compare

@github-actions github-actions released this 10 Sep 15:22
81d02e5

Fixed

  • Launch composer: a multi-repo project must be told which repo a build run works in (F-028).
    Choosing a project auto-attached every one of its repos as chips and the launch body took
    repoRef = repoRefs[0], so an explicit tick on the repo the operator meant was silently outranked
    by whichever project member happened to be listed first — acceptance run 1f12f9ab dispatched a
    studio bug fix into wicked-core with four councils voting before it could be stopped. The chips are
    now context: the one repo the run works in is derived by resolveLaunchTarget
    (src/components/launchTarget.ts, the single definition the Send guard, the wire body, the deliver
    notice and the pre-send summary all read) — Target-repo choice > explicit popover tick > the lone
    attached repo > for build-kind work with several candidates ambiguous, no default (a
    required launch-target-repo select with launch-target-reason; Send disabled, Cmd+Enter fires
    nothing; the deliver notice reads no-target); non-build launches keep the first repo as context, as
    before. Auto-attached chips keep their (from project) marker after a tick (data-auto-attached
    is now per chip) and the target chip carries data-target="true". The deliver notice names the
    repo the PR lands on — → opens a PR on owner/repo off the registered git_url, the registered
    name otherwise (repoSlugOf; data-deliver-repo). A pre-send confirmation step (launch-confirm with launch-confirm-workflow / -target / -gate;
    data-workflow / data-target / data-gate) reads workflow + target repo + gate posture before
    Send. The operator's latest act stands: a tick made after a Target choice wins ("select A,
    then tick B" sends B), and removing the chosen repo drops the choice. The popover's gate select is
    now launch-gate (its former launch-confirm testid names the confirmation step). Wire unchanged
    (LaunchRunBody.repoRef). Tests: tests/launchTarget.test.ts,
    tests/ChatInput.target.test.tsx; seed suite: LNCH-T (e2e/seed_surfaces_test.py, authored,
    not yet executed — see docs/testing/seed-surfaces-plan.md §3).
  • Run page: Cancel run for every non-terminal status, outside gates (F-029). A run in
    distributing had no cancel anywhere on /runs/:idsteering-cancel lives inside a gate card
    that had not opened, and the header's stop control was an unlabelled icon that read as decoration —
    so a mis-bound run burned seats until the operator hit POST /runs/:id/cancel by hand. The header
    now carries a labelled run-cancel button for planning / distributing / executing / awaiting_human;
    it asks first (run-cancel-confirm: run-cancel-yes / run-cancel-keep, Escape keeps), speaks the
    wire directly (api.cancelRun) and refreshes the run index; a refusal stays on screen
    (run-cancel-error, role="alert") instead of being swallowed. Terminal runs offer none. ChatPanel no longer
    takes an onKill prop (the header speaks the wire itself; the Ctrl/⌘+Shift+K shortcut and the
    palette verb are unchanged). Tests: tests/ChatPanel.cancel.test.tsx; seed suite: RUN-CXL
    (observed at TST-1's gate on a second page; authored, not yet executed).