Skip to content

Release v0.20.0 (candidate): workspace, background tasks, native modules - #392

Draft
raiseCatError wants to merge 87 commits into
release/v0.19.0from
release/v0.20.0
Draft

raiseCatError wants to merge 87 commits into
release/v0.19.0from
release/v0.20.0

Conversation

@raiseCatError

@raiseCatError raiseCatError commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Release candidate — do not merge. Based on release/v0.19.0 (#390), which is itself an unpublished candidate: this PR shows only the v0.20 delta. Once v0.19.0 is published it retargets to master. No tag or release without maintainer physical QA and explicit approval.

Integration (merge commits, history preserved)

Commit What
9f8f1085 merge integration/v020-modules 8f079208 (#370 → #372 → #373, #374, #369, #381, #379, #380, #375 → #376/#377/#378, #383 → #384). Ruby/PHP overlap with #368 resolved per #385: #370's runtimes.ts, pack and test, module count 16; v0.19's "a Ruby declaration never spans lines" regression kept; coverage doc updated
7377af4f merge #347 ebdb1ad9 (clean)
207de26b merge #349 66b60f3c: TranscriptPresenter keeps v0.19's NO_COLOR restore and adds word wrap; CHANGELOG entry carried over
fe60bdc4 workspace sidebar/panel wired through one column plan; /sidebar, /panel, /keys
1c418629 security-gate reconciliation (#379 × #347), see below
960e925f background tasks: session-owned supervisor and protocol
5d547246 background tasks: frontend, task bar, /background, submission hook
84730131 related PRs in /worktrees from the last /github listing
62c6fad0 release metadata, docs/testing/v020-physical-qa.md
44f4c232 /keys host profile moved out of TerminalApp (hostIntegration defect found by CI)
00163da7 security review of 1c418629: the gate now refuses eight demonstrated ways around it (shell by path, extra namespace import, any executable, non-plugin argv, args changed after review, direct execFile, createRequire, a process helper imported from outside the folder), with a self-test; plugin ids and the validated path can no longer reach Claude's argv as options
1861666a Codex adapter: a cancel made before Codex names the turn is sent when it does (real defect found by the local canonical run; adapter still unwired)
c71a4284 background supervisor test: the leader exits only after the sleeper ignores the hangup (test race found by the local canonical run; supervisor unchanged)

Decisions for maintainer review

  1. Portable mod gate (Portable Mods: executable mods stay disabled behind a documented, tested gate (#341) #379) vs Agent workspace: native /model and /effort, Claude mods, context panel #347. Dispatch CI on the merge-only head (207de26b, run 38608819740) failed no code-loading path exists under src/agents/mods: Agent workspace: native /model and /effort, Claude mods, context panel #347's claudePlugins.ts runs Claude's own plugin list|details|validate|enable|disable, and view.ts used inline import('…') type annotations. Resolution keeps every loader check, allows only an execFile import without a shell, and pins each argv to four reviewed forms verbatim; view.ts now uses ordinary type imports. This changes a security test: please confirm. Security review (2026-10-15): acceptable with conditions. The static gate could not establish what it claimed (eight bypasses accepted, now refused in 00163da7). It still cannot establish that claude plugin validate/details never run plugin code, how claude plugin list treats an untrusted repository's settings, or live enable/disable behavior; conditions: 00163da7, physical item 4.2 with a disposable account, and your sign-off.
  2. Background suggestions default off (changed from Background tasks core: supervised PTY tasks, detection, preferences, bounded output (#382) #383's ask), so Enter on tail -f, npm run dev, ping keeps running in the foreground as before. Background starts only via /background run, a family set to Always, or after /background suggestions on.
  3. Task output stays out of the transcript: one NMSh line on start and on failure; output only in the bar, view and /background.
  4. Detail panel shows sidebar selections only; module diagnostics stay in /modules (the canonical Module observability: explain visibility, provenance, collection timings and cache state #365 view).
  5. Codex app-server adapter ships unwired and unadvertised; Claude mod enable/disable is unverified against a live account.
  6. Claude plugin enable/disable without live verification: (a) run physical 4.2 with a disposable account, else (b) defer the toggle for v0.20 (inventory and Inspect stay), or (c) ship with the known limitation. Recommended (a), else (b).
  7. Checklist 1.4 /keys naming tmux prefix/bind -n keys is not implemented (the design never promised it): defer and reword (recommended) or implement. 1.6: clicks focus a region, rows are not selected and the wheel scrolls the transcript (both deferred, both CHANGELOG known limitations); proposed wording added, nothing removed.
  8. Open unexplained local failures (see Gates): whether a passing canonical run on the final head suffices with them recorded open.

Preserved boundaries

Workspace keys are routed only after every panel, program reader, startup and search handler (derived owner; protected owners never intercepted). dimensions() is the center width, so wrapping, ScreenPlan, hit testing and PTY size share one plan; drawers never resize the shell; passthrough gets every column. The background hook runs only on routeComposer's direct submission from an idle composer, never for queued entries, program input, startup or other submit() callers. Up precedence: keymap → agent shelf → task bar → history.

Gates (head c71a4284)

  • PR CI 38723507272: success, attempt 1 (Fedora and timing smokes skipped by design on PR)
  • workflow_dispatch CI 38723501877: every job success, attempt 1, including Fedora, both timing smokes and Node 22
  • Local canonical npm run verify:release (run 3, macOS arm64 8 GB, Node 26.10.0): 2612 tests, 2611 pass, 0 fail, 1 skipped (opt-in real Claude); bench typecheck and timing smoke pass; clean before/after
  • Report/compare/clipboard security subset (v0.19's exact command): 77/77, 0 skipped, no leaked roots
  • Supplementary v0.20 security tests (mods gate, plugin argv, session protocol and tasks, workspace routing, background integration, Codex adapter): 72/73, 1 skipped (opt-in real Claude), no leaked roots
  • Bounded security review of the integration (fe60bdc4..44f4c232): no issue in task ownership/protocol, suggestion hook, focus/routing, tmux, worktree cd, related-PR action or transcript isolation; the mods gate finding is fixed in 00163da7
  • Physical QA (docs/testing/v020-physical-qa.md): not performed. Runbook: docs/development/release-qa-runbook.md on docs/release-train-v019-v020

History (all preserved, none reclassified as flaky):

  • 44f4c232: PR CI 38650280119 and dispatch 38650273295 passed. Local canonical run 1 failed 2/2609 (absolutePathLive fish ownership-probe timeout, codexAdapter cancellation); run 2 failed 5/2609 (backgroundSupervisor ×2, codexAdapter, configuredCompletion ×2). The earlier note "under heavy unrelated load" was not established: the suite alone drives load 10–70 on this host.
  • Causes: codexAdapter was a real defect (deterministic reproduction; fixed in 1861666a); backgroundSupervisor was a test race (the hangup ends a member that has not yet ignored it; fixed in c71a4284); configuredCompletion (also on 62c6fad0), copyFolding (62c6fad0) and absolutePathLive (run 1) are not reproduced and not explained; they stay open records.
  • Earlier: merge-only gate failure (207de26b), hostIntegration defect (62c6fad0).

Ledger: docs/development/release-train-v019-v020.md on docs/release-train-v019-v020.

…son transport

Records the initialize metadata (model catalog, commands, agents), get_context_usage,
mcp_status, set_model, apply_flag_settings (effort), set_permission_mode and the
runtime facts that confirm each change, in a disposable workspace with customizations
disabled and a budget cap. Account identity values are never printed.
… and bounded rows

Agent replies, provider command output and GitHub bodies are Markdown from outside
NMSh. This renders them as terminal rows that always fit their width: headings,
nested and task lists, quotes, GFM tables that wrap cells (records when far too
narrow), fenced code with a gutter and language-aware highlighting from the
person's syntax roles, and rules. Links become OSC 8 hyperlinks only for http(s)
targets and always show where they go; refused targets stay visible as text.
Safe glyphs and NO_COLOR keep the structure with ASCII markers and backticks.
The authored-only /help renderer is unchanged.
…keep parsing linear

A security review of the untrusted-Markdown renderer found three problems, fixed here:

- Link text could hide a different destination when the target's host was only a
  substring of the text (example.com -> le.com). Links are now judged on their whole
  text: no destination is revealed only when the text names exactly the target's host
  as a whole hostname and no other; internationalized (punycode) hosts are always
  shown.
- Numeric entities decoded after the caller's scrubber, so ‮ and similar could
  reintroduce bidi overrides or invisible characters. Entities now refuse controls,
  format characters, separators and private use, and the renderer scrubs its own
  input instead of trusting callers.
- Several recognizers backtracked quadratically on long lines (heading, fence, rule,
  hard and soft breaks, YAML keys, code spans, emphasis, link brackets, long-word
  breaking). They are linear scans or bounded searches now, with timing tests on
  pathological inputs.
The adapter now speaks the documented Agent SDK control requests over its existing
stream-json channel, verified against Claude Code 2.1.295 by the control probe:

- set_model, apply_flag_settings (session-only effortLevel), set_permission_mode,
  get_context_usage, mcp_status/toggle/reconnect and stop_task, each correlated by
  request id with a timeout, waiting for the initialize handshake. Bypassing every
  permission check is never sent; refusals are reported as the provider worded them.
- The initialize response is kept: the model catalog with per-model effort levels,
  the provider's commands (built-in or not), agents, output styles and account plan.
- Telemetry with provenance (provider, requested, observed, estimate): runtime facts
  from each turn's init, usage and cost, rate limits (shared only with targets of
  the same launch profile, labelled), compaction, retries, tasks, the TodoWrite plan
  and the files tools read and edited, with the provider's own patch hunks.
- Structured tool results (patches, reads, command output, searches, plans,
  subagent reports), bounded, never keeping whole file contents.
- Permission requests carry the provider's reason and its own rule offers; an
  accepted offer is returned verbatim, never composed by NMSh.
- Streaming text (--include-partial-messages) is presentation only and is replaced
  by the completed message; a profile's effort is a launch flag.
…s are exact and session-only

An MCP tool returning a result shaped like an edit could have appeared as a diff and a
file activity. Structured results are now bound to the built-in tool that produced them.
Permission rule offers are rebuilt from exactly the fields shown, limited to this session,
allow-only, and never switch to a mode that bypasses checks.
In a managed Claude composer, /model and /effort open NMSh's own picker in the
composer's place instead of going to Claude's prompt. Rows are what Claude published:
its model catalog, and only the effort levels the running model supports (none for a
model without levels, which the picker says). Up/Down choose, typing filters, Enter
applies through set_model or apply_flag_settings, Esc backs out. /model sonnet and
/effort high apply directly on an exact match; a leading space sends text verbatim.

Nothing is claimed without evidence: a model change reads "requested" until Claude
reports it; effort is shown as acknowledged, set at launch, read from the account's
settings, or the model default (unreported) - never invented. The header shows model
and effort with that provenance. A refusal keeps the picker open with the reason.
A FIFO or very large file at a project's .claude/settings.json could block or flood
the effort lookup. The path is checked with lstat first.
…and, live reload

/claude mods and /mods now show each account's Claude plugins as they are:
- Type from the plugin's own manifest files, read as data (nothing is imported or
  run): Mod (hook modules), Plugin, Settings hook or Skill, with its components.
- The effective state for this directory. The old inventory read projectEnabled,
  which is false for nearly every installed plugin, so everything showed disabled;
  project and local enabledPlugins overrides are now read and named instead.
- Conflicts (also installed at another scope, other version), dependencies and
  options, in words; project installations of other directories are not shown.
- What it does in an NMSh-managed session, per Claude's documented behavior for
  claude -p: a mod's hooks run, nothing it draws appears; skills, agents, commands,
  hooks, MCP and LSP load. "Loaded in" names targets whose own report lists it.

Space proposes a change and Enter confirms it: claude plugin enable|disable at the
plugin's installed scope and account (CLAUDE_CONFIG_DIR), --json. Only ids from the
current listing are sent, since Claude's command writes an unknown id into settings.
The listing is read again as evidence, then running managed sessions on that account
reload plugins (reload_plugins) and the result is reported, errors included.
Standalone skills and settings hooks are listed without a switch, with the reason.
i inspects through claude plugin details and validate (static). No sandbox is implied.
…and the project

From telemetry already collected (nothing read on the render path): context pressure
with its source and age (Claude's own count with headroom to auto-compact and the
largest categories, or an estimate from the last turn, never 0% when unreported),
model, effort and permission mode with provenance, the plan when Claude emits one,
running subagents and tasks, files read and edited with their line changes,
compactions, usage limits only once close, and the session's estimated cost.

It sits beside the conversation from 120 columns; /panel shows or hides it (72
columns at least), and /context asks Claude for its breakdown. Safe glyphs draw
ASCII marks and bars; NO_COLOR keeps every distinction in words.
Mechanisms chosen for each change, what is operational versus a representation of
provider facts versus unsupported, the research conclusions that shaped the panel,
and the next steps (left sidebar, per-turn diffs, MCP status, prompt navigation).
… real Claude

Runs the picker logic and the documented control requests against a logged-in
Claude Code in safe mode (no plugins, hooks or mods run, no session persistence),
and proves the account's settings.json is unchanged. Verified with 2.1.295: the
catalog opens the picker, set_model reads as requested until the next turn reports
the model, effort is acknowledged (never claimed as confirmed), and an unknown model
is refused in Claude's own words.
…able configuration

Installs a code-free probe plugin from a local marketplace into a disposable,
separately logged-in Claude configuration, starts a managed session that loads it,
then disables and re-enables it through NMSh's own path (listing, setPluginEnabled,
reload_plugins) and checks Claude's own reports: plugin list, offered commands, load
errors. Refuses any non-temp or account-looking directory and hashes the real
accounts' settings before and after.
…on through any path

The plugin probe now accepts only /tmp/nmsh-cc-probe, resolved through symlinks, and
refuses a directory that equals, contains or lies inside ~/.claude, ~/.claude-account1,
~/.claude-account2 or an inherited CLAUDE_CONFIG_DIR. Checked before any claude
command runs, since even auth status writes into a fresh directory.
…ounts

The plugin probe's guard and settings hashes listed two personal account
directories by name; it now discovers the default ~/.claude, every
~/.claude-* sibling and an inherited CLAUDE_CONFIG_DIR, so it refuses any
real identity on any machine. The picker probe's usage line no longer
points at a real account.
/copy confirmations, notices and completion rows split mid-word at narrow
widths ('5 l' / 'ines'). Lines NMSh writes itself now break after a space,
with continuation rows hanging under the text after a leading glyph; a word
wider than the row still breaks by character. Each continuation row's
indent covers only added padding, so a selection copies the line exactly.
Shell output keeps the terminal's column wrapping.

Live tests that read joined screen rows match these messages across a
word break.
…t differ without color

Controls are semantic ({id, label, state}); the renderer draws only what its caller says is available, explains a disabled one, folds non-essential controls into a count on narrow windows, and returns regions so the pointer reaches the same action as the key. States, styles (outline, filled, soft, plain) and geometry (square, rounded) are independent; marks carry every state, so NO_COLOR and Safe/ASCII lose nothing. The worktree confirmation uses it (removal is the destructive control).
…e project's own files and the install location

Same pattern as the existing runtimes: the requested version from the project's files (.ruby-version/Gemfile, .php-version/composer.json, global.json, .swift-version/Package.swift, build.zig.zon/.zigversion, .dvmrc, plus .tool-versions and mise), the active version from the install path, never by running the tool. Minimum versions (global.json, swift-tools-version, minimum_zig_version) are floors and are not judged as mismatches. A binary inside the workspace is not inspected.
- Package.swift: swift-tools-version comes from the first line only, as
  SwiftPM reads it, and manifests larger than 4 KiB are no longer skipped.
- build.zig.zon: read up to the standard 64 KiB metadata bound.
- go.mod/go.work: directives stay on one line (CRLF and a trailing comment
  allowed); a match can no longer span line breaks.
- .tool-versions: asdf's .NET plugin name (dotnet-core) is recognized.
- Install layouts: RVM and chruby/ruby-install rubies report their version.

The linear-time regression now asserts the parsed values and a 250 ms bound
that the previous Gemfile pattern (about 0.8 s on the same input) fails. The
hostile-workspace test plants php, dotnet, swift and zig bait, hostile Ruby,
.NET, Swift, Zig, Deno and PHP metadata, and checks every runtime's active
version stays unknown for workspace binaries.
… with its control

- The reason line is measured: the label yields first (down to half the
  width), then the whole line is cut, so narrow windows never overflow.
- A reason is listed only for a disabled control that is drawn; a control
  folded into "+N" is explained by describeActions, not an orphan line.
- The tightest layout keeps the label's first letter (">C<") instead of an
  ellipsis that names nothing.
…re recommended from their project files

The six modules named icon ids the core icon set did not have, so they
showed their text label even with Nerd icons on. The code points match
Starship's Nerd Font preset and Nerd Fonts' glyph names (provenance in
icons.ts). Each module is now recommended when its own project files are
present (Gemfile, composer.json, global.json, Package.swift, build.zig,
deno.json and their version files); recommendations still enable nothing.

A test keeps every first-party pack module on a core icon.
…tate, without running hg or jj

- vcs.hg: the nearest .hg directory, found upward from the working
  directory (Mercurial-only repositories have no Git root, so the search is
  its own: it stops at home when it starts below it, and is bounded to 32
  levels). .hg/branch (absent means `default`) and .hg/bookmarks.current;
  oversized or symlinked files are not names. hgrc and hooks are never read.
- vcs.jj: marks a Jujutsu repository, whether Git is colocated, and whether
  this is the main workspace (.jj/repo is the store) or a secondary one
  (.jj/repo is a pointer file). jj's protobuf working-copy state and
  operation log are not parsed, so no change id or bookmark is guessed.
- nmsh.vcs (now "Version control extras") gains hg and jj modules; they
  start hidden like other non-cloud pack modules, so nothing is demanded
  until a person chooses them, and are recommended when .hg or .jj exists.
…ent the six modules

The planted-binary checks passed home as the workspace, and a workspace
that contains home is deliberately not a workspace (it holds the user's own
tools), so the refusal path was never reached; the planted binaries also had
no install layout to report. They now use a home outside the workspace and
a Homebrew/rbenv-shaped layout that would report 6.6.6 if the boundary did
not hold, and assert the refusal note.

The Context Modules guide lists the new capabilities, what each reads, and
which declarations are floors.
…and resource model

Shared groundwork for the sidebar (#331), detail panel (#332) and keymap
(#335) under #330, as standalone modules; the running app is unchanged.

- layout.ts: the column counterpart of ScreenPlan. Docked, compact, drawer
  and hidden modes by measured width; a docked region never squeezes the
  center below 60 cells (it becomes a drawer), drawers overlay the center
  without resizing the PTY, and passthrough/idle suppress both regions.
- focus.ts: LEFT_SIDEBAR and RIGHT_PANEL extend #329's InputOwner rather
  than adding a dispatcher. Protected owners (passthrough, approval,
  choice, search, agent message, panels) are never intercepted; in the
  composer only opt-in bindings apply, only with an empty draft, never to
  typing. Closing a focused region returns focus to the composer; focus
  resumes after passthrough.
- keymap.ts: key -> semantic action per context, with slash-command paths
  and help derived from the bindings. The defaults bind nothing in the
  composer.
- resources.ts: stable ids; activatable/informational/unsupported with an
  activation intent required for the first; selected, focused, active,
  attention and stale kept apart; hostile text sanitized; trees bounded.

docs/design/workspace.md records the integration contract for
TerminalApp.ts, which waits for the workflow track's stabilization
checkpoint (#359).
…nd manager sections

#331, with the tmux navigation part of #333; stacked on the workspace
foundation. Standalone controller and renderer: the running app is
unchanged until the integration step.

- SidebarController: selection by id across refreshes (a vanished one moves
  to the row in its place), tree expansion kept across refreshes, parent on
  Left, a `/` filter that shows matches with their ancestors. Moving only
  proposes a preview; activation returns an intent and only for an
  activatable resource; anything else says why.
- renderSidebar: exactly rows x width; focus, selection, active, attention,
  exited and stale each have their own mark in Unicode and Safe glyphs, so
  NO_COLOR loses nothing; hostile names are inert.
- tmux.ts: one bounded `list-panes -a` on the server NMSh runs in ($TMUX),
  fields separated by U+001F and every row validated; session -> window ->
  pane with NMSh's own pane marked active; navigation with tmux's own
  select-pane/select-window/switch-client on ids, never names, as argv.
- sources.ts: worktrees (open = the /worktrees cd intent; missing or bare
  paths listed with a reason) and links to /github, /agents, /sessions.

Tests include a real tmux server on a private socket: listing, selecting a
window with the navigation argv, and a missing server reported as an error.
…own files and the SDK's metadata

- Elixir: mix.exs's `elixir:` requirement (read as text, never evaluated),
  .tool-versions/mise; active from the install path (Homebrew, asdf, mise)
  or the release's lib/elixir/ebin/elixir.app. bin/elixir is never run.
- Dart: pubspec.yaml environment.sdk (failsafe YAML: aliases bounded, tags
  and non-string values ignored); active from the SDK's own `version` file.
  Flutter's bin/dart wrapper reads the bundled bin/cache/dart-sdk/version,
  and that metadata wins over an FVM or cask install path, which names
  Flutter's version, not Dart's.
- Flutter: .fvmrc (FVM 3) or .fvm/fvm_config.json, pubspec
  environment.flutter, .tool-versions; only for a package that depends on
  the Flutter SDK or pins one with FVM. Active from the install's
  bin/cache/flutter.version.json or `version`. An SDK linked inside the
  project (.fvm/flutter_sdk) is workspace-controlled and refused.
- Install layouts: FVM (~/fvm/versions/<v>) and Homebrew casks.
- Pack modules elixir, dart and flutter (show-on-command, Context Rail),
  with icons (Seti elixir/dart, Devicons flutter) and recommendations from
  mix.exs, pubspec.yaml and FVM files.

Tests use sentinel executables that fail if run, a planted workspace SDK
with version metadata that would be reported if the boundary did not hold,
and hostile pubspec/mix.exs inputs (alias bomb, tags, whitespace runs).
…on and Make

project.build reports the build system of the nearest directory with a build
file (up to the repository root) and the version the project pins for it:

- Gradle and Maven: the wrapper properties' distributionUrl (never fetched).
- Bazel: .bazelversion, as Bazelisk reads it.
- CMake and Meson: cmake_minimum_required / meson_version, matched as text
  and shown as a minimum (≥); a subproject without a declaration uses the
  nearest CMakeLists.txt that has one.
- Make: presence only.

No wrapper (gradlew, mvnw), Bazelisk or build file is run or evaluated.
Versions are read only when demanded. The `build` pack module is
show-on-command (gradle, mvn, cmake, meson, ninja, bazel, make...) and is
recommended from the build files; one module instead of one per tool.

Tests: wrapper sentinels that fail if run, subproject lookups, the
case-insensitive Makefile spelling, control payloads in properties,
symlinked metadata and a whitespace-run CMakeLists.txt.
…ion/v020-modules

# Conflicts:
#	docs/architecture/context-modules.md
#	tests/contextCapabilities.test.ts
#	tests/contextPacks.test.ts
Brings #370 -> #372 -> #373, #374, #369, #381, #379, #380,
#375 -> #376/#377/#378 and #383 -> #384 onto the v0.19.0 release
candidate (9e85a91).

Ruby/PHP overlap with #368, resolved as recorded in #385: #370's
runtimes.ts, nmsh.project.json and Ruby/PHP test (canonical, linear
Gemfile parsing); module count 16 from #373. Kept the v0.19 regression
that a Ruby declaration never spans lines, which #370's tests did not
assert, and marked #370/#372/#373/#374 done in
docs/design/native-module-coverage.md.
TranscriptPresenter: keeps v0.19's NO_COLOR stripping of restored NMSh
rows and adds #349's word wrapping for NMSh metadata lines; program
output still wraps by column with its original SGR.

CHANGELOG: #349 was based on a pre-release v0.18 changelog; kept the
release branch's file and added #349's one entry under Unreleased.
dimensions() is the workspace center whenever a region is open, so
transcript wrapping, ScreenPlan, panels, hit testing and the PTY size
follow the same plan; drawers never change it. Sidebar, panel and drawer
cells are composed at the final paint (after effects and caret), with
the Status Strip row at full width and erase-in-line removed from center
rows.

Keyboard ownership is derived from the frontend's state: startup, a
program reading hidden or single-key input, passthrough, the agent view,
panels and searches are protected and never intercepted. Workspace keys
are routed after all of them; an opt-in keymap binding from an idle
composer comes before the agent shelf and history. Pointer events are
translated once: a click focuses a region, center events continue with
center-relative columns.

Activation goes through existing paths: a worktree stages its cd in an
empty composer, managers open as their commands do, tmux navigates by
id through tmux from fixed system locations. Adds /sidebar [focus],
/panel [focus] and /keys [reload], with help and Ask coverage.
Found by dispatch CI 38608819740 on the merge-only head 207de26: #379's
gate allowed one process use under src/agents/mods (claudeDiscovery.ts
listing plugins), and #347 moved that listing into claudePlugins.ts and
added details, validate and enable/disable at the installed scope.
Neither PR's own CI could see the other.

The gate stays strict: every loader check is unchanged, the only
child_process import allowed is execFile, a shell is refused, and each
argv given to Claude's CLI must be one of the four reviewed forms
verbatim. None of them loads or runs mod code in NMSh.

view.ts used inline type imports (import('./controller.js').X), which
the loader check rightly cannot tell from a runtime import(); they are
ordinary type imports now. No runtime change.
Each session owns its background tasks beside its shell, in the session
service or in the in-process client: SessionTasks wraps the #383
supervisor, tasks keep running while a window is detached, a reattached
window receives the list and each task's recent output (never a restart),
and ending the session ends its tasks. A shell switch keeps them.

Protocol: the background-tasks feature on both sides; task (start, stop,
remove) from the frontend; tasks and task-output from the owner, only to
a frontend that listed the feature and only after created/attached and
the replay. Every request, record and update is validated on arrival.
Task output travels separately from the shell's stream and never
reaches its PTY; an update carries at most 1 MiB, newest lines first.

A start carries the live shell's PATH; the owner supplies the session
shell and its launch environment, which is used for the spawn only.
TaskOutput gains receive() so a frontend mirror keeps the owner's line
numbers under the same limits.
…ission hook

The task bar and a waiting suggestion sit above the composer with the
other notices. Up from an idle composer goes to an opt-in keymap
binding, then a shown agent shelf, then the task bar, then history;
Shift+Up enters the bar from an empty composer. The bar, the manager
and the output view are protected owners for the workspace router.

Only routeComposer's direct submission from an idle composer consults
the detector; queued entries, program input, startup, history reruns,
install retries and presets never do. Suggestions are now off by
default, so a recognized command runs in the foreground exactly as
before unless the person turns them on or sets its family to Always;
/background run is the explicit path. Any key other than a choice
returns the draft and keeps typing.

A task's output is mirrored from its owner and shown only in the bar,
the output view and /background; the transcript gets one NMSh line when
a task starts and when it ends (failures by default), so copies,
reports, comparisons and failure navigation never see task output.
/background t3 opens a task's output; /bg is the short form.
/worktrees gets pullRequests from the PR list /github last loaded, only
when the shell's github.com origin is the same repository; the manager
still never fetches. openPullRequest closes the manager and opens that
pull request's detail in /github.
0.20.0 section marked as an unreleased candidate built on the unpublished
v0.19.0 candidate, with compatibility, upgrade notes and limitations.
README, ROADMAP and AGENTS describe it as a release candidate; the latest
published release pointers stay at v0.18.0.
Found by CI 38649080918/38649082204 and the local canonical run on
62c6fad: /keys derived the host profile with TERM_PROGRAM and the
Ghostty profile inside TerminalApp, which the general frontend must
never branch on (hostIntegration). keymapHostProfile(env) now lives
beside hostNotes; TerminalApp only asks for it.
…ay arguments

Security review of 1c41862. The gate accepted, as fixtures show, a
shell given by path, a second namespace import of child_process, any
executable, an argv not starting with 'plugin', args changed after its
reviewed literal, a direct execFile call, createRequire, and a process
helper imported from outside src/agents/mods. It now refuses each of
them, and a self-test runs every one through it.

Runtime: a plugin id from Claude's listing could start with '-' and
reach the CLI in option position, and validate was given the listing's
install path unchecked. Ids now start with a word character, and only
an absolute install path is validated.

No loader check is removed and executable Portable Mods stay disabled.
… it does

Found by the local canonical run on 44f4c23 ("cancellation is
acknowledged only when the turn completes as interrupted", 10.4 s) and
reproduced deterministically: send() queues the text while the thread
starts, and cancel() returned silently without a thread and turn id, so
the turn then ran with no interrupt and no word to the person. The test
waited 300 ms and usually won the race; under CPU contention it failed
2 of 2.

The request now waits and is sent once, as soon as the turn/start answer
or turn/started names the turn; it is forgotten when the turn settles,
is refused or the session fails, and a cancel with nothing running is
not kept for a later turn. Only Codex's own interrupted result still
counts as a cancellation. The adapter remains unwired.
…s the hangup

Found by the local canonical run on 44f4c23 (both leftover tests, 1.4 s
and 10.3 s) and an earlier dev run. The tests assumed the background
Node sleeper had installed its SIGHUP handler before the leader's
`sleep 1` / `sleep 0.2` ended. Node alone took 0.3-0.5 s to start on
this host under moderate load, and a member that has not ignored the
hangup by then is ended by it (plain sleep 2 of 2, a late trap 3 of 4);
the supervisor then correctly reports no leftovers. With an 800 ms slow
start the old shape lost the member 1 time in 10, the new one 0 in 10.

The sleeper now creates a file once the hangup is ignored and the leader
waits for it (bounded at about ten seconds) before its unchanged exit.
No assertion, deadline or supervisor code changes.
@raiseCatError

Copy link
Copy Markdown
Owner Author

Exact-head verification, c71a42846da4a3832e578f3fb9c28f192510d08e (2026-10-15). This PR stays a draft; nothing merged, tagged or retargeted.

Automated gates on this head: all pass.

  • Local canonical verify:release run 3: 2611/2612, 1 skipped (opt-in real-Claude read); bench typecheck and timing smoke pass.
  • PR CI 38723507272 and dispatch CI 38723501877: both pass on attempt 1. On the PR run, Fedora and the timing smokes are skipped by design; dispatch ran every job.
  • Report/compare/clipboard security subset: 77/77, no leaked roots.
  • Supplementary v0.20 security tests: 72/73, 1 skipped.

What changed since 44f4c232:

  • 00163da7: portable-mod gate hardening and plugin argv hardening (security review).
  • 1861666a: real Codex adapter cancel defect.
  • c71a4284: background supervisor test race.

Failures, all preserved. On 44f4c232, run 1 failed 2/2609 and run 2 failed 5/2609. Two causes were found and fixed. configuredCompletion, copyFolding and absolutePathLive were not reproduced and are not explained; they stay open even though run 3 passed.

Still open:

  • Physical QA, not performed. Runbook: release-qa-runbook.md.
  • Maintainer sign-off on decisions 1–8 in the description. Most pressing:
    • the portable-mod gate (acceptable with conditions);
    • Claude plugin enable/disable without live verification;
    • the /keys tmux clause of checklist 1.4.
  • v0.19 publication first (Release v0.19.0 (candidate): terminal workflow #390), then the ancestry-preserving reconciliation of this branch with master and re-verification.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant