From 2506aeebfc703b67d9d6141d0f37984b64c9581d Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Nov 2026 13:00:31 -0500 Subject: [PATCH 1/3] docs: add openspec change tui-project-cwd Specs, design, and tasks for wiring the TUI process cwd to the active project on select/clear/new-session. Closes #1629. --- .../changes/tui-project-cwd/.openspec.yaml | 2 + openspec/changes/tui-project-cwd/design.md | 59 +++++++++++++++++++ openspec/changes/tui-project-cwd/proposal.md | 46 +++++++++++++++ .../specs/tui-project-cwd/spec.md | 58 ++++++++++++++++++ openspec/changes/tui-project-cwd/tasks.md | 30 ++++++++++ 5 files changed, 195 insertions(+) create mode 100644 openspec/changes/tui-project-cwd/.openspec.yaml create mode 100644 openspec/changes/tui-project-cwd/design.md create mode 100644 openspec/changes/tui-project-cwd/proposal.md create mode 100644 openspec/changes/tui-project-cwd/specs/tui-project-cwd/spec.md create mode 100644 openspec/changes/tui-project-cwd/tasks.md diff --git a/openspec/changes/tui-project-cwd/.openspec.yaml b/openspec/changes/tui-project-cwd/.openspec.yaml new file mode 100644 index 000000000..25002a8e8 --- /dev/null +++ b/openspec/changes/tui-project-cwd/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-01 diff --git a/openspec/changes/tui-project-cwd/design.md b/openspec/changes/tui-project-cwd/design.md new file mode 100644 index 000000000..04f7b3cc5 --- /dev/null +++ b/openspec/changes/tui-project-cwd/design.md @@ -0,0 +1,59 @@ +# Design + +## Context + +`src/tui/app.js` holds three `useCallback` handlers — `handleSelectProject`, +`handleClearProject`, `handleNewSession` — that update the in-memory active-project state +via `setActiveProject`/`setProjectCwd` but never move the OS process cwd. Consumers that +read `process.cwd()` directly (the cron command builders in `src/scheduler/cron.js` and +`src/tools/cron/index.js`, which embed `cd ${process.cwd()}`) therefore run in the wrong +directory. See proposal.md for motivation. + +The structured logger is imported as `import { logger } from "../shared/logger.js"` and +exposes `logger.error` (the established pattern in `src/session/shutdown.js` and +`src/tui/conversationArea.js`). `config` is already a prop of `App` and already a +dependency of `handleClearProject`. + +## Goals / Non-Goals + +**Goals:** +- Move the process cwd into the selected project on select. +- Restore the process cwd to `config.cwd` (fallback `process.cwd()`) on clear and on new + session. +- Guard every `process.chdir` with try/catch + `logger.error` (AGENTS.md 1.1). + +**Non-Goals:** +- Changing `src/workspace/loadAgents.js`'s module-load `process.cwd()` snapshot. +- Changing the active-project store or cron builder logic. + +## Decisions + +- **Guard shape.** Wrap each `process.chdir` in a `try { process.chdir(target); } + catch (err) { logger.error(...); }`. Alternatives considered: a shared helper + `safeChdir(target)` to avoid repetition. Given only three call sites and the KISS + principle (AGENTS.md 1.4), a small inline guard is acceptable; a helper is preferred if + the three blocks exceed three lines of duplicated logic. Decision: extract a tiny + `safeChdir` closure to satisfy DRY without over-engineering. +- **Restore target.** `config.cwd || process.cwd()`. `config.cwd` is the madz root + captured at load; the `process.cwd()` fallback covers the unset case. +- **`handleNewSession` deps.** Add `config` to its `useCallback` dependency array so the + restore target is current. `handleSelectProject` takes `projectPath` as an argument, so + its deps stay `[]`. + +## Risks / Trade-offs + +- [chdir mutates global process state, affecting the whole process] → This is the + intended behavior; the TUI is a single-process app. Tests must save/restore the original + cwd to avoid leaking state across test cases. +- [An invalid project path throws] → Guarded by try/catch + logger; the TUI continues. +- [The `loadAgents.js` snapshot does not track runtime chdir] → Explicitly out of scope; + documented in the proposal and spec. + +## Migration Plan + +No data migration. Deploy by merging the change. Rollback is a simple revert of +`src/tui/app.js`. + +## Open Questions + +None. diff --git a/openspec/changes/tui-project-cwd/proposal.md b/openspec/changes/tui-project-cwd/proposal.md new file mode 100644 index 000000000..e4d541480 --- /dev/null +++ b/openspec/changes/tui-project-cwd/proposal.md @@ -0,0 +1,46 @@ +# Proposal + +## Why + +The TUI's `/projects` panel updates in-memory active-project state but never moves the +OS process working directory. `handleSelectProject`, `handleClearProject`, and +`handleNewSession` in `src/tui/app.js` call `setActiveProject`/`setProjectCwd` but omit +`process.chdir`, so consumers that read `process.cwd()` directly — the cron command +builders in `src/scheduler/cron.js` and `src/tools/cron/index.js` (which embed +`cd ${process.cwd()}`) — run in the wrong directory. This change wires the process cwd +to the active project. + +## What Changes + +- `handleSelectProject` moves the process into the selected project via a guarded + `process.chdir(projectPath)`. +- `handleClearProject` restores the process cwd to `config.cwd` (fallback + `process.cwd()`) via a guarded `process.chdir`. +- `handleNewSession` restores the process cwd to `config.cwd` (fallback + `process.cwd()`) for consistency, and adds `config` to its `useCallback` deps. +- Every `process.chdir` call is wrapped in try/catch that records failures through the + structured logger (`logger.error`), per AGENTS.md 1.1 — no silent catch, no crash. + +## Capabilities + +### New Capabilities +- `tui-project-cwd`: The TUI moves the OS process working directory in lockstep with the + active project — into the project on select, back to `config.cwd` on clear and on new + session — with guarded, logged `process.chdir` calls. + +### Modified Capabilities + + +## Non-goals + +- Changing `src/workspace/loadAgents.js`'s module-load `process.cwd()` snapshot semantics + (a separate concern; callers must pass `targetCwd` explicitly). +- Altering the active-project in-memory store (`src/workspace/project.js`). +- Changing cron command builder logic beyond the fact that they now read a correct cwd. + +## Impact + +- `src/tui/app.js` — three handlers gain guarded `process.chdir` calls; `logger` import + added; `handleNewSession` deps updated. +- `tests/unit/tui/app.test.js` — regression tests for select/clear/invalid-path. +- No API, dependency, or config changes. diff --git a/openspec/changes/tui-project-cwd/specs/tui-project-cwd/spec.md b/openspec/changes/tui-project-cwd/specs/tui-project-cwd/spec.md new file mode 100644 index 000000000..7b0df3aa8 --- /dev/null +++ b/openspec/changes/tui-project-cwd/specs/tui-project-cwd/spec.md @@ -0,0 +1,58 @@ +# Spec Delta + +## Purpose + +Keeps the OS process working directory in lockstep with the TUI's active project, so +that consumers reading `process.cwd()` (e.g. cron command builders) operate in the +directory the user selected, and restore it to the configured root when the project is +cleared or a new session begins. + +## ADDED Requirements + +### Requirement: Process cwd moves into the selected project +The TUI SHALL move the OS process working directory into the selected project directory +when the user selects a project, in addition to updating the in-memory active-project +state. + +#### Scenario: Selecting a valid project +- **WHEN** the user selects a project whose directory exists +- **THEN** the process working directory becomes that project directory +- **AND** the in-memory active-project state reflects the same directory + +#### Scenario: Selecting an invalid project path +- **WHEN** the user selects a project whose directory does not exist or cannot be entered +- **THEN** the process working directory is left unchanged +- **AND** the failure is recorded through the structured logger +- **AND** no exception propagates to crash the TUI + +### Requirement: Process cwd restores on project clear +The TUI SHALL restore the OS process working directory to the configured root +(`config.cwd`, falling back to the current process cwd) when the user clears the active +project. + +#### Scenario: Clearing the active project +- **WHEN** the user clears the active project +- **THEN** the process working directory is restored to `config.cwd` when it is set +- **AND** the in-memory active-project state is cleared + +#### Scenario: Clearing with no configured root +- **WHEN** the user clears the active project and `config.cwd` is unset +- **THEN** the process working directory is restored to the current process cwd + +### Requirement: Process cwd restores on new session +The TUI SHALL restore the OS process working directory to the configured root when a new +session begins, consistent with clearing the active project. + +#### Scenario: Starting a new session +- **WHEN** the user starts a new session +- **THEN** the process working directory is restored to `config.cwd` when it is set +- **AND** the in-memory active-project state is cleared + +### Requirement: Chdir failures are guarded and logged +Every process-working-directory change SHALL be guarded so that a failure to change +directory is recorded through the structured logger and never crashes the TUI. + +#### Scenario: Chdir throws on an invalid target +- **WHEN** a process-working-directory change fails +- **THEN** the error is recorded through the structured logger +- **AND** the TUI continues to run diff --git a/openspec/changes/tui-project-cwd/tasks.md b/openspec/changes/tui-project-cwd/tasks.md new file mode 100644 index 000000000..440d29ff2 --- /dev/null +++ b/openspec/changes/tui-project-cwd/tasks.md @@ -0,0 +1,30 @@ +# Tasks + +## 1. Guarded chdir helper + +- [ ] 1.1 Import `logger` from `../shared/logger.js` in `src/tui/app.js`. +- [ ] 1.2 Add a small `safeChdir(target)` guard that wraps `process.chdir(target)` in + try/catch and records failures via `logger.error` (no silent catch, no crash). + +## 2. Wire handlers + +- [ ] 2.1 In `handleSelectProject`, call `safeChdir(projectPath)` after + `setProjectCwd(projectPath)`. +- [ ] 2.2 In `handleClearProject`, call `safeChdir(config.cwd || process.cwd())` after + `setProjectCwd("")`. +- [ ] 2.3 In `handleNewSession`, call `safeChdir(config.cwd || process.cwd())` and add + `config` to its `useCallback` dependency array. + +## 3. Regression tests + +- [ ] 3.1 In `tests/unit/tui/app.test.js`, add a test asserting `process.cwd()` reflects + the selected project after `handleSelectProject` (save/restore the original cwd). +- [ ] 3.2 Add a test asserting `process.cwd()` restores to `config.cwd` after + `handleClearProject`. +- [ ] 3.3 Add a test for the invalid-path case: `process.chdir` throws, the logger records + it, and no exception propagates. + +## 4. Verify + +- [ ] 4.1 Run `npm run test` and confirm all tests pass. +- [ ] 4.2 Run `npm run coverage` and confirm coverage is maintained. From acd1b392e3a84a8a154fcb1c48ce6b47e8a5cf25 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Nov 2026 13:26:15 -0500 Subject: [PATCH 2/3] fix: wire TUI process cwd to active project Add a guarded safeChdir helper and call it from handleSelectProject, handleClearProject, and handleNewSession so the OS process cwd moves into the selected project and restores to config.cwd on clear/new-session. Failures are logged via the structured logger (no silent catch). Adds regression tests for select, clear, and the invalid-path case. Closes #1629. --- coverage.txt | 4 +- openspec/changes/tui-project-cwd/tasks.md | 20 ++++---- src/tui/app.js | 26 +++++++++- tests/unit/tui/app.test.js | 58 +++++++++++++++++++++++ 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/coverage.txt b/coverage.txt index 244eb2ac1..7e0153384 100644 --- a/coverage.txt +++ b/coverage.txt @@ -195,7 +195,7 @@ ℹ yaml | | | | ℹ index.js | 100.00 | 94.44 | 100.00 | ℹ tui | | | | -ℹ app.js | 44.22 | 55.81 | 20.00 | 130 141 151-157 167-177 185-189 197-200 221-246 253-262 271-296 312-330 340-345 352-353 360-405 409-531 544-548 551-561 564-577 593-598 600-604 606-610 612-619 621-627 629-635 642-652 659-700 725-726 +ℹ app.js | 45.32 | 57.78 | 22.22 | 149 160 170-176 186-197 205-210 218-221 242-267 274-283 292-317 333-351 361-369 376-377 384-429 433-555 568-572 575-585 588-601 617-622 624-628 630-634 636-643 645-651 653-659 666-676 683-724 749-750 ℹ banner.js | 87.30 | 100.00 | 85.71 | 28-35 ℹ commandHelp.js | 100.00 | 100.00 | 100.00 | ℹ commandParser.js | 97.56 | 89.87 | 100.00 | 90-95 @@ -231,6 +231,6 @@ ℹ loadAgents.js | 88.00 | 42.86 | 100.00 | 17-18 32-33 38-39 ℹ project.js | 100.00 | 100.00 | 100.00 | ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -ℹ all files | 91.52 | 87.50 | 86.86 | +ℹ all files | 91.51 | 87.50 | 86.87 | ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ℹ end of coverage report diff --git a/openspec/changes/tui-project-cwd/tasks.md b/openspec/changes/tui-project-cwd/tasks.md index 440d29ff2..256375437 100644 --- a/openspec/changes/tui-project-cwd/tasks.md +++ b/openspec/changes/tui-project-cwd/tasks.md @@ -2,29 +2,29 @@ ## 1. Guarded chdir helper -- [ ] 1.1 Import `logger` from `../shared/logger.js` in `src/tui/app.js`. -- [ ] 1.2 Add a small `safeChdir(target)` guard that wraps `process.chdir(target)` in +- [x] 1.1 Import `logger` from `../shared/logger.js` in `src/tui/app.js`. +- [x] 1.2 Add a small `safeChdir(target)` guard that wraps `process.chdir(target)` in try/catch and records failures via `logger.error` (no silent catch, no crash). ## 2. Wire handlers -- [ ] 2.1 In `handleSelectProject`, call `safeChdir(projectPath)` after +- [x] 2.1 In `handleSelectProject`, call `safeChdir(projectPath)` after `setProjectCwd(projectPath)`. -- [ ] 2.2 In `handleClearProject`, call `safeChdir(config.cwd || process.cwd())` after +- [x] 2.2 In `handleClearProject`, call `safeChdir(config.cwd || process.cwd())` after `setProjectCwd("")`. -- [ ] 2.3 In `handleNewSession`, call `safeChdir(config.cwd || process.cwd())` and add +- [x] 2.3 In `handleNewSession`, call `safeChdir(config.cwd || process.cwd())` and add `config` to its `useCallback` dependency array. ## 3. Regression tests -- [ ] 3.1 In `tests/unit/tui/app.test.js`, add a test asserting `process.cwd()` reflects +- [x] 3.1 In `tests/unit/tui/app.test.js`, add a test asserting `process.cwd()` reflects the selected project after `handleSelectProject` (save/restore the original cwd). -- [ ] 3.2 Add a test asserting `process.cwd()` restores to `config.cwd` after +- [x] 3.2 Add a test asserting `process.cwd()` restores to `config.cwd` after `handleClearProject`. -- [ ] 3.3 Add a test for the invalid-path case: `process.chdir` throws, the logger records +- [x] 3.3 Add a test for the invalid-path case: `process.chdir` throws, the logger records it, and no exception propagates. ## 4. Verify -- [ ] 4.1 Run `npm run test` and confirm all tests pass. -- [ ] 4.2 Run `npm run coverage` and confirm coverage is maintained. +- [x] 4.1 Run `npm run test` and confirm all tests pass. +- [x] 4.2 Run `npm run coverage` and confirm coverage is maintained. diff --git a/src/tui/app.js b/src/tui/app.js index 166841854..c7686f885 100644 --- a/src/tui/app.js +++ b/src/tui/app.js @@ -14,11 +14,30 @@ import { ProjectsPanel } from "./projectsPanel.js"; import { SubagentsPanel } from "./subagentsPanel.js"; import { updateSubagentStore } from "./subagentStore.js"; import { setProjectCwd } from "../workspace/project.js"; +import { logger } from "../shared/logger.js"; import { getActiveProviderConfig } from "../provider/index.js"; import { useMouseScroll, scaleScrollDelta } from "./useMouseScroll.js"; import { buildLayout, extractSelection, mapCoordToChar } from "./selectionLayout.js"; import clipboardy from "clipboardy"; +/** + * Change the process working directory, guarding against an invalid target. + * + * `process.chdir` throws when the target path does not exist or cannot be + * entered; we record the failure through the structured logger rather than + * letting it propagate and crash the TUI (AGENTS.md 1.1 — no silent catch). + * + * @param {string} target - The directory to change into + * @returns {void} + */ +export function safeChdir(target) { + try { + process.chdir(target); + } catch (err) { + logger.error(`[tui] failed to change cwd to ${target}: ${err.message}`); + } +} + /** * App router — holds cross-cutting state and view routing. * Renders ConversationArea + InputArea in conversation view, @@ -166,6 +185,7 @@ function App({ const handleSelectProject = useCallback((projectPath) => { setActiveProject(projectPath); setProjectCwd(projectPath); + safeChdir(projectPath); setCurrentView(PANELS.CONVERSATION); // Silently notify the agent that we're now working in the selected project. // The conversation area is unmounted during the panel view, so stage the @@ -184,6 +204,7 @@ function App({ const handleClearProject = useCallback(() => { setActiveProject(""); setProjectCwd(""); + safeChdir(config.cwd || process.cwd()); setCurrentView(PANELS.CONVERSATION); // Silently notify the agent that we've returned to the root directory. pendingSilentMessageRef.current = "We are now working in madz root directory."; @@ -343,7 +364,10 @@ function App({ // cleared. A new session has no project context, so leaving the prior // selection in place would falsely imply one is active. setActiveProject(""); - }, []); + // Restore the process cwd to the configured root, consistent with + // clearing the active project. + safeChdir(config.cwd || process.cwd()); + }, [config]); /** * handleQuit — App-level exit. diff --git a/tests/unit/tui/app.test.js b/tests/unit/tui/app.test.js index f1f7143b7..e844d4989 100644 --- a/tests/unit/tui/app.test.js +++ b/tests/unit/tui/app.test.js @@ -46,3 +46,61 @@ describe("App — in-conversation search (Ctrl+F)", () => { assert.ok(mod.default, "InputArea default export exists"); }); }); + +describe("safeChdir — process cwd wiring", () => { + it("moves the process cwd into a valid project directory", async () => { + const mod = await import("../../../src/tui/app.js"); + const { mkdtempSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const original = process.cwd(); + try { + const target = mkdtempSync(join(tmpdir(), "madz-cwd-")); + mod.safeChdir(target); + assert.strictEqual(process.cwd(), target); + } finally { + process.chdir(original); + } + }); + + it("restores the process cwd to the configured root on clear", async () => { + const mod = await import("../../../src/tui/app.js"); + const { mkdtempSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const original = process.cwd(); + try { + const root = mkdtempSync(join(tmpdir(), "madz-root-")); + const project = mkdtempSync(join(tmpdir(), "madz-proj-")); + // Simulate select: move into the project. + mod.safeChdir(project); + assert.strictEqual(process.cwd(), project); + // Simulate clear: restore to config.cwd. + mod.safeChdir(root); + assert.strictEqual(process.cwd(), root); + } finally { + process.chdir(original); + } + }); + + it("logs and does not crash when the target path is invalid", async () => { + const mod = await import("../../../src/tui/app.js"); + const { logger } = await import("../../../src/shared/logger.js"); + const original = process.cwd(); + const originalError = logger.error; + let logged = null; + logger.error = (msg) => { + logged = msg; + }; + try { + // An invalid target throws inside process.chdir; safeChdir must + // swallow it, record it via the logger, and leave the cwd intact. + mod.safeChdir("/nonexistent/path/that/does/not/exist"); + assert.strictEqual(process.cwd(), original); + assert.ok(logged, "logger.error was called"); + assert.match(logged, /failed to change cwd/); + } finally { + logger.error = originalError; + } + }); +}); From a988029e68ff7290ba6f9ee90f03b195ed2d10d1 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 1 Nov 2026 13:30:08 -0500 Subject: [PATCH 3/3] chore: archive tui-project-cwd openspec change --- .../.openspec.yaml | 0 .../2026-11-01-tui-project-cwd}/design.md | 0 .../2026-11-01-tui-project-cwd}/proposal.md | 0 .../specs/tui-project-cwd/spec.md | 0 .../2026-11-01-tui-project-cwd}/tasks.md | 0 openspec/specs/tui-project-cwd/spec.md | 57 +++++++++++++++++++ 6 files changed, 57 insertions(+) rename openspec/changes/{tui-project-cwd => archive/2026-11-01-tui-project-cwd}/.openspec.yaml (100%) rename openspec/changes/{tui-project-cwd => archive/2026-11-01-tui-project-cwd}/design.md (100%) rename openspec/changes/{tui-project-cwd => archive/2026-11-01-tui-project-cwd}/proposal.md (100%) rename openspec/changes/{tui-project-cwd => archive/2026-11-01-tui-project-cwd}/specs/tui-project-cwd/spec.md (100%) rename openspec/changes/{tui-project-cwd => archive/2026-11-01-tui-project-cwd}/tasks.md (100%) create mode 100644 openspec/specs/tui-project-cwd/spec.md diff --git a/openspec/changes/tui-project-cwd/.openspec.yaml b/openspec/changes/archive/2026-11-01-tui-project-cwd/.openspec.yaml similarity index 100% rename from openspec/changes/tui-project-cwd/.openspec.yaml rename to openspec/changes/archive/2026-11-01-tui-project-cwd/.openspec.yaml diff --git a/openspec/changes/tui-project-cwd/design.md b/openspec/changes/archive/2026-11-01-tui-project-cwd/design.md similarity index 100% rename from openspec/changes/tui-project-cwd/design.md rename to openspec/changes/archive/2026-11-01-tui-project-cwd/design.md diff --git a/openspec/changes/tui-project-cwd/proposal.md b/openspec/changes/archive/2026-11-01-tui-project-cwd/proposal.md similarity index 100% rename from openspec/changes/tui-project-cwd/proposal.md rename to openspec/changes/archive/2026-11-01-tui-project-cwd/proposal.md diff --git a/openspec/changes/tui-project-cwd/specs/tui-project-cwd/spec.md b/openspec/changes/archive/2026-11-01-tui-project-cwd/specs/tui-project-cwd/spec.md similarity index 100% rename from openspec/changes/tui-project-cwd/specs/tui-project-cwd/spec.md rename to openspec/changes/archive/2026-11-01-tui-project-cwd/specs/tui-project-cwd/spec.md diff --git a/openspec/changes/tui-project-cwd/tasks.md b/openspec/changes/archive/2026-11-01-tui-project-cwd/tasks.md similarity index 100% rename from openspec/changes/tui-project-cwd/tasks.md rename to openspec/changes/archive/2026-11-01-tui-project-cwd/tasks.md diff --git a/openspec/specs/tui-project-cwd/spec.md b/openspec/specs/tui-project-cwd/spec.md new file mode 100644 index 000000000..3be5575b5 --- /dev/null +++ b/openspec/specs/tui-project-cwd/spec.md @@ -0,0 +1,57 @@ +# tui-project-cwd Specification + +## Purpose +Keeps the OS process working directory in lockstep with the TUI's active project, so +that consumers reading `process.cwd()` (e.g. cron command builders) operate in the +directory the user selected, and restore it to the configured root when the project is +cleared or a new session begins. + +## Requirements + +### Requirement: Process cwd moves into the selected project +The TUI SHALL move the OS process working directory into the selected project directory +when the user selects a project, in addition to updating the in-memory active-project +state. + +#### Scenario: Selecting a valid project +- **WHEN** the user selects a project whose directory exists +- **THEN** the process working directory becomes that project directory +- **AND** the in-memory active-project state reflects the same directory + +#### Scenario: Selecting an invalid project path +- **WHEN** the user selects a project whose directory does not exist or cannot be entered +- **THEN** the process working directory is left unchanged +- **AND** the failure is recorded through the structured logger +- **AND** no exception propagates to crash the TUI + +### Requirement: Process cwd restores on project clear +The TUI SHALL restore the OS process working directory to the configured root +(`config.cwd`, falling back to the current process cwd) when the user clears the active +project. + +#### Scenario: Clearing the active project +- **WHEN** the user clears the active project +- **THEN** the process working directory is restored to `config.cwd` when it is set +- **AND** the in-memory active-project state is cleared + +#### Scenario: Clearing with no configured root +- **WHEN** the user clears the active project and `config.cwd` is unset +- **THEN** the process working directory is restored to the current process cwd + +### Requirement: Process cwd restores on new session +The TUI SHALL restore the OS process working directory to the configured root when a new +session begins, consistent with clearing the active project. + +#### Scenario: Starting a new session +- **WHEN** the user starts a new session +- **THEN** the process working directory is restored to `config.cwd` when it is set +- **AND** the in-memory active-project state is cleared + +### Requirement: Chdir failures are guarded and logged +Every process-working-directory change SHALL be guarded so that a failure to change +directory is recorded through the structured logger and never crashes the TUI. + +#### Scenario: Chdir throws on an invalid target +- **WHEN** a process-working-directory change fails +- **THEN** the error is recorded through the structured logger +- **AND** the TUI continues to run