nfo-cli 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-command.js +6 -1
- package/dist/claude-command.js.map +1 -1
- package/dist/claude-trust.js +46 -0
- package/dist/claude-trust.js.map +1 -0
- package/dist/cli.js +64 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/restore.js +0 -1
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/tui.js +6 -4
- package/dist/commands/tui.js.map +1 -1
- package/dist/mcp/handlers.js +5 -0
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/tool-defs.js +10 -0
- package/dist/mcp/tool-defs.js.map +1 -1
- package/dist/musicians/dismiss.js +1 -1
- package/dist/musicians/dismiss.js.map +1 -1
- package/dist/musicians/roles.js +15 -0
- package/dist/musicians/roles.js.map +1 -0
- package/dist/musicians/spawn.js +53 -18
- package/dist/musicians/spawn.js.map +1 -1
- package/dist/permission.js +14 -8
- package/dist/permission.js.map +1 -1
- package/dist/prompts/musician-role.js +2 -1
- package/dist/prompts/musician-role.js.map +1 -1
- package/dist/prompts/orchestrator-role.js +42 -8
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/prompts/tool-discipline.js +10 -0
- package/dist/prompts/tool-discipline.js.map +1 -1
- package/dist/tui/{App.js → components/App.js} +20 -20
- package/dist/tui/components/App.js.map +1 -0
- package/dist/tui/components/AppView.js +13 -0
- package/dist/tui/components/AppView.js.map +1 -0
- package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
- package/dist/tui/components/Auditorium.js.map +1 -0
- package/dist/tui/components/ConcertHall.js.map +1 -0
- package/dist/tui/{Help.js → components/Help.js} +0 -8
- package/dist/tui/components/Help.js.map +1 -0
- package/dist/tui/components/OrchestratorPane.js.map +1 -0
- package/dist/tui/components/SidebarHeader.js +6 -0
- package/dist/tui/components/SidebarHeader.js.map +1 -0
- package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/package.json +8 -1
- package/assets/agent-screen.png +0 -0
- package/assets/main-screen.png +0 -0
- package/assets/orche-clawd.png +0 -0
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/AppView.js +0 -13
- package/dist/tui/AppView.js.map +0 -1
- package/dist/tui/Auditorium.js.map +0 -1
- package/dist/tui/ConcertHall.js.map +0 -1
- package/dist/tui/Help.js.map +0 -1
- package/dist/tui/OrchestratorPane.js.map +0 -1
- package/dist/tui/SidebarHeader.js +0 -6
- package/dist/tui/SidebarHeader.js.map +0 -1
- package/dist/tui/StatusBar.js.map +0 -1
- package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
- package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
- package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
- package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
- package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
- package/docs/specs/2026-05-29-nfo-design.md +0 -468
- package/src/claude-command.ts +0 -35
- package/src/claude-detect.ts +0 -42
- package/src/cli.ts +0 -164
- package/src/commands/attach.ts +0 -24
- package/src/commands/dashboard-window.ts +0 -33
- package/src/commands/kill.ts +0 -50
- package/src/commands/launch.ts +0 -134
- package/src/commands/list.ts +0 -43
- package/src/commands/mcp-server.ts +0 -18
- package/src/commands/notes.ts +0 -18
- package/src/commands/restore.ts +0 -153
- package/src/commands/tui.tsx +0 -16
- package/src/config.ts +0 -44
- package/src/dashboard.ts +0 -1
- package/src/mcp/config.ts +0 -39
- package/src/mcp/handlers.ts +0 -141
- package/src/mcp/server.ts +0 -50
- package/src/mcp/tool-defs.ts +0 -151
- package/src/musicians/dismiss.ts +0 -60
- package/src/musicians/ids.ts +0 -21
- package/src/musicians/lookup.ts +0 -13
- package/src/musicians/message-log.ts +0 -152
- package/src/musicians/message.ts +0 -99
- package/src/musicians/query.ts +0 -19
- package/src/musicians/spawn.ts +0 -139
- package/src/notes.ts +0 -39
- package/src/notify.ts +0 -62
- package/src/orchestrator/report-back.ts +0 -33
- package/src/permission.ts +0 -30
- package/src/project-key.ts +0 -12
- package/src/prompts/musician-role.ts +0 -22
- package/src/prompts/orchestrator-role.ts +0 -60
- package/src/prompts/tool-discipline.ts +0 -35
- package/src/repo.ts +0 -14
- package/src/shell-quote.ts +0 -7
- package/src/state-updaters.ts +0 -132
- package/src/state.ts +0 -49
- package/src/state.types.ts +0 -67
- package/src/tmux.ts +0 -226
- package/src/tui/App.tsx +0 -532
- package/src/tui/AppView.tsx +0 -96
- package/src/tui/Auditorium.tsx +0 -56
- package/src/tui/ConcertHall.tsx +0 -31
- package/src/tui/Help.tsx +0 -72
- package/src/tui/OrchestratorPane.tsx +0 -98
- package/src/tui/SidebarHeader.tsx +0 -32
- package/src/tui/StatusBar.tsx +0 -44
- package/src/tui/activity-line.ts +0 -16
- package/src/tui/detect-permission.ts +0 -93
- package/src/tui/embedded-session-lifecycle.ts +0 -44
- package/src/tui/embedded-terminal.ts +0 -325
- package/src/tui/format-time.ts +0 -25
- package/src/tui/keymap.ts +0 -104
- package/src/tui/poll-activity.ts +0 -25
- package/src/tui/poll-idle.ts +0 -149
- package/src/tui/poll-permission.ts +0 -50
- package/src/tui/status-icon.ts +0 -35
- package/src/tui/terminal-input.ts +0 -136
- package/src/tui/watch-state.ts +0 -43
- package/src/worktree.ts +0 -41
- package/tests/claude-command.test.ts +0 -30
- package/tests/claude-detect.test.ts +0 -14
- package/tests/commands/attach.test.ts +0 -60
- package/tests/commands/kill.test.ts +0 -66
- package/tests/commands/launch.test.ts +0 -75
- package/tests/commands/list.test.ts +0 -47
- package/tests/commands/notes.test.ts +0 -53
- package/tests/commands/restore.test.ts +0 -126
- package/tests/helpers/tmp-config.ts +0 -16
- package/tests/helpers/tmp-repo.ts +0 -29
- package/tests/integration/orchestrator-spawn.test.ts +0 -108
- package/tests/mcp/handlers.test.ts +0 -163
- package/tests/mcp/tool-defs.test.ts +0 -35
- package/tests/musicians/dismiss.test.ts +0 -102
- package/tests/musicians/message.test.ts +0 -159
- package/tests/musicians/query.test.ts +0 -65
- package/tests/musicians/spawn.test.ts +0 -125
- package/tests/notes.test.ts +0 -56
- package/tests/notify.test.ts +0 -80
- package/tests/orchestrator/report-back.test.ts +0 -18
- package/tests/permission.test.ts +0 -29
- package/tests/project-key.test.ts +0 -33
- package/tests/prompts/tool-discipline.test.ts +0 -25
- package/tests/repo.test.ts +0 -38
- package/tests/state-updaters.test.ts +0 -126
- package/tests/state.test.ts +0 -85
- package/tests/tmux.test.ts +0 -126
- package/tests/tui/AppView.test.tsx +0 -92
- package/tests/tui/Auditorium.test.tsx +0 -67
- package/tests/tui/ConcertHall.test.tsx +0 -22
- package/tests/tui/Help.test.tsx +0 -38
- package/tests/tui/OrchestratorPane.test.ts +0 -30
- package/tests/tui/SidebarHeader.test.tsx +0 -20
- package/tests/tui/StatusBar.test.tsx +0 -51
- package/tests/tui/activity-line.test.ts +0 -21
- package/tests/tui/detect-permission.test.ts +0 -92
- package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
- package/tests/tui/embedded-terminal.test.ts +0 -80
- package/tests/tui/format-time.test.ts +0 -25
- package/tests/tui/keymap.test.ts +0 -93
- package/tests/tui/poll-activity.test.ts +0 -81
- package/tests/tui/poll-idle.test.ts +0 -159
- package/tests/tui/poll-permission.test.ts +0 -222
- package/tests/tui/status-icon.test.ts +0 -27
- package/tests/tui/terminal-input.test.ts +0 -113
- package/tests/tui/watch-state.test.ts +0 -54
- package/tests/worktree.test.ts +0 -73
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -12
- /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
- /package/dist/tui/{OrchestratorPane.js → components/OrchestratorPane.js} +0 -0
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
# NFO Phase 4 — Permission Prompt Detection Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Implement spec §5.2.1 — detect when a Musician's `claude` session is stuck on a permission prompt, persist that to `state.json`, surface it in the Auditorium, and give the user a one-keystroke jump to the prompting Musician. The state schema already carries `status: "awaiting_permission"` and `pending_permission`; Phase 4 just populates and renders them.
|
|
6
|
-
|
|
7
|
-
**Architecture:** A pure `detectPermissionPrompt(paneText)` function recognises claude's prompt signature and (best-effort) extracts the requested tool name. A `pollPermissions(state)` fanout runs the detector across each non-stopped Musician's pane and returns a list of *transitions* — only the deltas relative to current state, so we don't write `state.json` on every tick. A new 2 s `useEffect` in `App.tsx` runs the poller and applies transitions via the existing `setMusicianStatus` updater. The Auditorium renders `pending_permission` as the activity line when status is `awaiting_permission`; the StatusBar adds a `N awaiting · [p] jump` hint; a new `jump-to-pending` action in `reduceKey` selects the first Musician in that state.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** No new dependencies. All existing: TypeScript ESM, Ink/React, vitest, execa-driven tmux. Phase 4 only adds new modules under `src/tui/` and modifies the existing Auditorium / StatusBar / keymap / App.
|
|
10
|
-
|
|
11
|
-
**Reference spec:** `docs/specs/2026-05-29-nfo-design.md` §5.2.1 (Permission prompts — detection, UI signal, response, explicit non-behavior). Also §5.2 (permission levels — only `supervised` and `strict` actually surface prompts; `accept-edits` and `auto` do not, so the detector still runs but should rarely fire).
|
|
12
|
-
|
|
13
|
-
**MANDATORY code style (applies to every task):**
|
|
14
|
-
- Control flow uses explicit braced multi-line blocks. Never the brace-less single-line form (`if (c) { return x; }`, never `if (c) return x;`). Same for `for`/`while`/`else`/`switch`.
|
|
15
|
-
- Arrow functions use explicit `{ return ... }` bodies, never implicit-return expression bodies — EXCEPT React component definitions returning JSX, which use `(props) => { return (<JSX/>); }`. Array callbacks: `.map((m) => { return <Row .../>; })`, never `.map(m => <Row/>)`.
|
|
16
|
-
- Ternaries (`a ? b : c`) ARE allowed, including inside JSX (`{cond ? <A/> : <B/>}`).
|
|
17
|
-
- Component return type: `import type { ReactElement } from 'react';` then `export function Foo(props: FooProps): ReactElement { ... }`. Never the global `JSX.Element`.
|
|
18
|
-
- Commit trailer: `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`. Local git identity (Javier Furus <javierfurus@gmail.com>) is already configured — do not change it. Use the HEREDOC commit form.
|
|
19
|
-
- Every code task runs `npm run build` (tsc) AND `npm test` before commit — `npm test` already has a `pretest: tsc` hook, but run build explicitly too when iterating.
|
|
20
|
-
|
|
21
|
-
**Explicitly NOT in Phase 4 (must not creep in):**
|
|
22
|
-
- Bell / desktop notifications (`notify_on_permission` config flag from spec §5.2.1 final paragraph). Defer to Phase 5.
|
|
23
|
-
- `?` help overlay. Defer to Phase 5.
|
|
24
|
-
- Real `q` / Escape quit via `useApp().exit`. The right pane is meant to always render — quitting it would just leave a `remain-on-exit` shell. `q` continues to mean `focus-orchestrator`. Defer to Phase 5.
|
|
25
|
-
- Token usage hint in the status bar. Defer to Phase 5.
|
|
26
|
-
- Concert Hall orchestra-switching (Tab/Shift-Tab actually attaching a different session). Defer to Phase 5.
|
|
27
|
-
- Auto-approving prompts, parsing permission semantics, or sending keystrokes to the Musician's pane on the user's behalf (spec §5.2.1 "Explicit non-behavior"). NFO only detects and surfaces.
|
|
28
|
-
- Any change to the MCP server, musician primitives, or non-`status`/`pending_permission` parts of the state schema.
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## File Structure
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
src/
|
|
36
|
-
├── tui/
|
|
37
|
-
│ ├── detect-permission.ts # NEW: pure detectPermissionPrompt(paneText) → { pending, tool? }
|
|
38
|
-
│ ├── poll-permission.ts # NEW: pollPermissions(state) → PermissionTransition[]
|
|
39
|
-
│ ├── keymap.ts # MODIFY: add 'p' → jump-to-pending action
|
|
40
|
-
│ ├── StatusBar.tsx # MODIFY: optional pendingCount banner + [p] hint
|
|
41
|
-
│ ├── Auditorium.tsx # MODIFY: render pending_permission as activity when awaiting
|
|
42
|
-
│ ├── AppView.tsx # MODIFY: thread pendingCount + selectedIndex through
|
|
43
|
-
│ └── App.tsx # MODIFY: poll-permission effect + jump-to-pending handler
|
|
44
|
-
tests/
|
|
45
|
-
├── tui/
|
|
46
|
-
│ ├── detect-permission.test.ts # NEW
|
|
47
|
-
│ ├── poll-permission.test.ts # NEW
|
|
48
|
-
│ ├── keymap.test.ts # MODIFY: add 'p' cases
|
|
49
|
-
│ ├── StatusBar.test.tsx # MODIFY: pending banner render
|
|
50
|
-
│ ├── Auditorium.test.tsx # MODIFY: awaiting state rendering
|
|
51
|
-
│ └── AppView.test.tsx # MODIFY: pending props plumb-through
|
|
52
|
-
docs/
|
|
53
|
-
└── plans/
|
|
54
|
-
└── 2026-05-29-nfo-phase-4-permission-prompts.md # THIS FILE
|
|
55
|
-
README.md # MODIFY: status section reflects Phase 4
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
No changes to `package.json`, `tsconfig.json`, `vitest.config.ts`, state schema, MCP server, or musician primitives.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Task 1: Pure detector — `detectPermissionPrompt`
|
|
63
|
-
|
|
64
|
-
**Files:** `src/tui/detect-permission.ts`, `tests/tui/detect-permission.test.ts`
|
|
65
|
-
|
|
66
|
-
The detector is the load-bearing piece. It runs on captured pane text (last ~20 lines) and returns whether a permission prompt is on screen plus a best-effort tool summary. It MUST be pure — no I/O, no state — and conservative: prefer false negatives over false positives. A false positive would falsely lock a Musician in `awaiting_permission` and trigger UI noise; a false negative just delays detection by one 2 s tick (acceptable, since claude's prompt persists until answered).
|
|
67
|
-
|
|
68
|
-
**Signature**
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
export interface PermissionDetection {
|
|
72
|
-
pending: boolean;
|
|
73
|
-
tool: string | null; // null when pending=false OR when we can't parse a tool name
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function detectPermissionPrompt(paneText: string): PermissionDetection;
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**Detection heuristic (be conservative — combine multiple signals):**
|
|
80
|
-
|
|
81
|
-
claude's permission prompt in the terminal has this rough shape (varies slightly by version, but the structural signal is stable):
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
Allow Bash to run `rm -rf node_modules`?
|
|
85
|
-
|
|
86
|
-
1. Yes
|
|
87
|
-
2. Yes, and don't ask again for Bash commands
|
|
88
|
-
3. No, and tell Claude what to do differently (esc)
|
|
89
|
-
|
|
90
|
-
❯ 1
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
The detector should require ALL of:
|
|
94
|
-
1. A "Yes" line that starts (after leading whitespace) with `1.` or `1)`.
|
|
95
|
-
2. A "No" line that starts (after leading whitespace) with a small digit (`2.`/`3.`) and contains `No` (case-sensitive, since claude capitalises it).
|
|
96
|
-
3. The text matches one of these prompt-introduction patterns (case-insensitive, regex):
|
|
97
|
-
- `/allow\s+\S+/i`
|
|
98
|
-
- `/do you want to/i`
|
|
99
|
-
- `/permission required/i`
|
|
100
|
-
- `/use this tool/i`
|
|
101
|
-
|
|
102
|
-
Requiring (1) AND (2) AND (3) makes the detector resistant to incidental text that mentions "Yes/No" or "Allow" in passing (e.g. a code comment scrolled into the pane). All three signals together is a near-unmistakable claude prompt.
|
|
103
|
-
|
|
104
|
-
Scan the LAST 20 non-empty lines of `paneText` (claude redraws the prompt at the bottom of its pane). The detector takes the full text; let it split and search internally.
|
|
105
|
-
|
|
106
|
-
**Tool name extraction (best-effort, NEVER throw):**
|
|
107
|
-
|
|
108
|
-
When pending=true, try to extract a short tool descriptor:
|
|
109
|
-
|
|
110
|
-
- Match `/^Allow ([A-Z][A-Za-z]+)/m` against the prompt block — captures `Bash`, `Edit`, `Write`, etc.
|
|
111
|
-
- If that matches AND the same line contains backticked content (`` ` ... ` ``), include up to 40 chars of it: `` Bash: `rm -rf node_modules` ``.
|
|
112
|
-
- If only the tool name matches, return just that: `Bash`.
|
|
113
|
-
- If neither matches, return `null` (the UI will render a generic "tool" string).
|
|
114
|
-
|
|
115
|
-
Truncate the final string to 60 chars with `…` if longer.
|
|
116
|
-
|
|
117
|
-
**Edge cases (cover with tests):**
|
|
118
|
-
- Empty string → `{ pending: false, tool: null }`.
|
|
119
|
-
- Random output with the word "Allow" but no numbered choices → `pending: false`.
|
|
120
|
-
- A "Yes/No" question from claude's chat output (not a permission prompt) → `pending: false` because the strict numbered choice format is absent.
|
|
121
|
-
- Prompt with a really long command in backticks → tool is truncated to 60 chars + `…`.
|
|
122
|
-
- Prompt where the tool is just `Read` with no backticks → tool: `"Read"`.
|
|
123
|
-
|
|
124
|
-
**Tests** (`tests/tui/detect-permission.test.ts`):
|
|
125
|
-
|
|
126
|
-
- [ ] **Step 1: Write the detector**
|
|
127
|
-
- [ ] Create `src/tui/detect-permission.ts` with the `PermissionDetection` interface and `detectPermissionPrompt` function as specified.
|
|
128
|
-
- [ ] Implementation: split on `\n`, take last 20 non-empty lines, run the three signal checks. Use explicit braced blocks.
|
|
129
|
-
- [ ] Run `npm run build`.
|
|
130
|
-
- [ ] **Step 2: Write detector tests** — minimum cases:
|
|
131
|
-
- [ ] Empty input → `{ pending: false, tool: null }`.
|
|
132
|
-
- [ ] Realistic prompt sample (Bash with backticked command) → `pending: true`, `tool: "Bash: `rm -rf node_modules`"` (or close — assert `startsWith("Bash")` and contains the command).
|
|
133
|
-
- [ ] Same prompt without backticks → `pending: true`, `tool: "Bash"`.
|
|
134
|
-
- [ ] Edit-tool prompt → `pending: true`, `tool` starts with `"Edit"`.
|
|
135
|
-
- [ ] Pane with "Allow me to explain" in chat output, no numbered choices → `pending: false`.
|
|
136
|
-
- [ ] Pane with numbered choices but no "Allow"/"Do you want to" intro → `pending: false`.
|
|
137
|
-
- [ ] 200-char tool description → `tool.length <= 60`, ends with `…`.
|
|
138
|
-
- [ ] **Step 3: Run tests** — `npm test`.
|
|
139
|
-
- [ ] **Step 4: Commit** — `feat(tui): detect-permission — pure claude permission-prompt detector` with the 4.7 trailer.
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## Task 2: Permission poller — `pollPermissions`
|
|
144
|
-
|
|
145
|
-
**Files:** `src/tui/poll-permission.ts`, `tests/tui/poll-permission.test.ts`
|
|
146
|
-
|
|
147
|
-
Wraps the detector with the per-Musician fanout and produces *transitions only*. Returning transitions (deltas) instead of a full status map keeps `App.tsx` from writing `state.json` on every tick — which would chokidar-storm the watcher.
|
|
148
|
-
|
|
149
|
-
**Signature**
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
import { capturePane, sessionName } from '../tmux.js';
|
|
153
|
-
import { detectPermissionPrompt } from './detect-permission.js';
|
|
154
|
-
import type { OrchestraState } from '../state.types.js';
|
|
155
|
-
|
|
156
|
-
export interface PermissionTransition {
|
|
157
|
-
musicianId: string;
|
|
158
|
-
newStatus: 'awaiting_permission' | 'working';
|
|
159
|
-
pendingPermission: string | null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function pollPermissions(state: OrchestraState): Promise<PermissionTransition[]>;
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
**Logic per Musician:**
|
|
166
|
-
|
|
167
|
-
- Skip if `status === 'stopped'`.
|
|
168
|
-
- `capturePane(\`${session}:${m.tmux_window_id}\`, 20)` — request 20 lines.
|
|
169
|
-
- Run `detectPermissionPrompt(paneText)`.
|
|
170
|
-
- Compute transition:
|
|
171
|
-
- If `detected.pending && m.status !== 'awaiting_permission'` → emit `{ musicianId, newStatus: 'awaiting_permission', pendingPermission: detected.tool ?? 'tool' }`.
|
|
172
|
-
- If `!detected.pending && m.status === 'awaiting_permission'` → emit `{ musicianId, newStatus: 'working', pendingPermission: null }`.
|
|
173
|
-
- Otherwise (no change) → emit nothing for this Musician.
|
|
174
|
-
- `try/catch` per Musician; on error, swallow (same pattern as `pollActivity`). A dead window must not break the poll.
|
|
175
|
-
|
|
176
|
-
Use a regular `for` loop with `try { ... } catch { /* swallow */ }`, NOT `Promise.all` — the per-Musician failure isolation matters more than the parallel speedup (we poll at most a handful of windows).
|
|
177
|
-
|
|
178
|
-
**Tests** (`tests/tui/poll-permission.test.ts`):
|
|
179
|
-
|
|
180
|
-
The poller has a real I/O dependency (`capturePane`). Two strategies — choose whichever is simpler in this codebase's existing test style:
|
|
181
|
-
|
|
182
|
-
- **Strategy A (preferred): real tmux session.** Same pattern as `tests/tui/poll-activity.test.ts` (which is real-tmux). Create a session, create a window with text that matches a permission prompt, build a fake `OrchestraState` with one Musician pointing to that window. Assert one `awaiting_permission` transition. Then send keys to clear the prompt-like text, poll again, assert one `working` transition.
|
|
183
|
-
- **Strategy B (fallback): module mock.** `vi.mock('../../src/tmux.js', ...)`. Only use this if Strategy A turns out to be flaky.
|
|
184
|
-
|
|
185
|
-
Pick A. If you hit obstacles, document and fall back to B.
|
|
186
|
-
|
|
187
|
-
- [ ] **Step 1: Write the poller** with explicit braced control flow.
|
|
188
|
-
- [ ] **Step 2: Write the test** following the pattern of `poll-activity.test.ts`:
|
|
189
|
-
- [ ] Real tmux session created in `beforeEach`, killed in `afterEach`.
|
|
190
|
-
- [ ] One Musician fixture in a synthetic `OrchestraState`.
|
|
191
|
-
- [ ] Case 1: write prompt-shaped text into the window's pane, status=`working` → expect one transition to `awaiting_permission` with non-null `pendingPermission`.
|
|
192
|
-
- [ ] Case 2: clear the pane (`tmux send-keys -t ... 'clear' Enter`), status=`awaiting_permission` → expect transition back to `working`, `pendingPermission: null`.
|
|
193
|
-
- [ ] Case 3: status=`stopped` → no transition emitted regardless of pane content.
|
|
194
|
-
- [ ] Case 4: musician pointing at a window that doesn't exist → no transition (error swallowed).
|
|
195
|
-
- [ ] **Step 3: Run tests** — `npm test`.
|
|
196
|
-
- [ ] **Step 4: Commit** — `feat(tui): poll-permission — per-musician detector fanout with transition deltas`.
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## Task 3: Extend `reduceKey` with `jump-to-pending`
|
|
201
|
-
|
|
202
|
-
**Files:** `src/tui/keymap.ts`, `tests/tui/keymap.test.ts`
|
|
203
|
-
|
|
204
|
-
Add a new `'p'` keybinding that emits a `jump-to-pending` action. App.tsx resolves the target Musician (the first one whose `status === 'awaiting_permission'`) at handle time — the reducer is pure and doesn't know the musician list contents.
|
|
205
|
-
|
|
206
|
-
**Changes to `keymap.ts`:**
|
|
207
|
-
|
|
208
|
-
- Extend `KeyAction` union with `| { kind: 'jump-to-pending' }`.
|
|
209
|
-
- Insert in `reduceKey`, after the `q` branch, before the final `return { ui }`:
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
if (key.input === 'p') {
|
|
213
|
-
return { ui, action: { kind: 'jump-to-pending' } };
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Maintain the existing precedence ordering. `p` does NOT need a guard on musician count — the App can no-op if there's no pending Musician, but the action should be emitted unconditionally so the keymap reducer stays simple.
|
|
218
|
-
|
|
219
|
-
**Changes to test:**
|
|
220
|
-
|
|
221
|
-
- [ ] **Step 1: Update `KeyAction` type and reducer** as above. Keep all existing behavior intact.
|
|
222
|
-
- [ ] **Step 2: Add test cases** in `tests/tui/keymap.test.ts`:
|
|
223
|
-
- [ ] `p` with non-zero musician count → returns `{ kind: 'jump-to-pending' }`.
|
|
224
|
-
- [ ] `p` with zero musicians → still returns `{ kind: 'jump-to-pending' }` (App resolves and no-ops if there's nothing pending).
|
|
225
|
-
- [ ] **Step 3: Run tests** — `npm test`.
|
|
226
|
-
- [ ] **Step 4: Commit** — `feat(tui): keymap — add 'p' jump-to-pending action`.
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
## Task 4: Auditorium renders `pending_permission` as activity
|
|
231
|
-
|
|
232
|
-
**Files:** `src/tui/Auditorium.tsx`, `tests/tui/Auditorium.test.tsx`
|
|
233
|
-
|
|
234
|
-
When a Musician's `status === 'awaiting_permission'`, the activity line should show `pending_permission` (the tool descriptor) prefixed with `awaiting:` — NOT the captured pane line.
|
|
235
|
-
|
|
236
|
-
**Change to `Auditorium.tsx`:**
|
|
237
|
-
|
|
238
|
-
Inside the `.map((m, i) => { ... })` body, compute `line` as:
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
const line = m.status === 'awaiting_permission'
|
|
242
|
-
? `awaiting: ${m.pending_permission ?? 'tool'}`
|
|
243
|
-
: (props.activity[m.id] ?? '');
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
(Ternary inside an assignment is allowed by style — see the style rules at top.)
|
|
247
|
-
|
|
248
|
-
The status icon and color already flip to `⚠` / yellow via the existing `statusIcon`/`statusColor` from Phase 3 — no change there.
|
|
249
|
-
|
|
250
|
-
**Tests:**
|
|
251
|
-
|
|
252
|
-
- [ ] **Step 1: Modify Auditorium.tsx** as above. Keep the empty-state branch and all other behavior.
|
|
253
|
-
- [ ] **Step 2: Add a test case** to `tests/tui/Auditorium.test.tsx`:
|
|
254
|
-
- [ ] Render with one Musician whose `status: 'awaiting_permission'`, `pending_permission: 'Bash: `rm -rf foo`'` → `lastFrame()` contains `awaiting: Bash:` AND `⚠`.
|
|
255
|
-
- [ ] Render with the same Musician but `pending_permission: null` → contains `awaiting: tool`.
|
|
256
|
-
- [ ] **Step 3: Run tests** — `npm test`.
|
|
257
|
-
- [ ] **Step 4: Commit** — `feat(tui): Auditorium — render pending_permission for awaiting musicians`.
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
## Task 5: StatusBar shows pending count + `[p] jump`
|
|
262
|
-
|
|
263
|
-
**Files:** `src/tui/StatusBar.tsx`, `src/tui/AppView.tsx`, `tests/tui/StatusBar.test.tsx`, `tests/tui/AppView.test.tsx`
|
|
264
|
-
|
|
265
|
-
Add a `pendingCount: number` prop to `StatusBar`. When > 0, render a top line above the existing first row:
|
|
266
|
-
|
|
267
|
-
```
|
|
268
|
-
⚠ N awaiting permission · [p] jump to next
|
|
269
|
-
supervised · —
|
|
270
|
-
[↑↓] nav [⏎] enter [n] notes [d] dismiss [q] back
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
When `pendingCount === 0`, render exactly as today — no first line. The second line of key hints stays as-is (we are deliberately NOT advertising `[p]` in the bottom hints when there's nothing to jump to, to avoid teaching the user a key that does nothing in the common case).
|
|
274
|
-
|
|
275
|
-
**Change to `StatusBar.tsx`:**
|
|
276
|
-
|
|
277
|
-
```tsx
|
|
278
|
-
export interface StatusBarProps {
|
|
279
|
-
permissionLevel: string;
|
|
280
|
-
tokenHint: string;
|
|
281
|
-
pendingCount: number;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function StatusBar(props: StatusBarProps): ReactElement {
|
|
285
|
-
return (
|
|
286
|
-
<Box flexDirection="column" borderStyle="single" borderTop={true} paddingX={1}>
|
|
287
|
-
{props.pendingCount > 0 ? (
|
|
288
|
-
<Text color="yellow">⚠ {props.pendingCount} awaiting permission · [p] jump to next</Text>
|
|
289
|
-
) : null}
|
|
290
|
-
<Text>
|
|
291
|
-
{props.permissionLevel} · {props.tokenHint}
|
|
292
|
-
</Text>
|
|
293
|
-
<Text dimColor={true}>[↑↓] nav [⏎] enter [n] notes [d] dismiss [q] back</Text>
|
|
294
|
-
</Box>
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
**Change to `AppView.tsx`:**
|
|
300
|
-
|
|
301
|
-
Add `pendingCount: number` to `AppViewProps`. Plumb it down to `<StatusBar pendingCount={props.pendingCount} ... />`.
|
|
302
|
-
|
|
303
|
-
**Tests:**
|
|
304
|
-
|
|
305
|
-
- [ ] **Step 1: Modify StatusBar.tsx** to accept and conditionally render `pendingCount`.
|
|
306
|
-
- [ ] **Step 2: Modify AppView.tsx** to plumb the new prop.
|
|
307
|
-
- [ ] **Step 3: Update tests:**
|
|
308
|
-
- [ ] `tests/tui/StatusBar.test.tsx`: existing test passes `pendingCount: 0` and asserts the banner is ABSENT; a new test passes `pendingCount: 2` and asserts the banner IS present (`2 awaiting permission` substring).
|
|
309
|
-
- [ ] `tests/tui/AppView.test.tsx`: pass `pendingCount: 1`, assert the banner substring appears in `lastFrame()`.
|
|
310
|
-
- [ ] **Step 4: Run tests** — `npm test`.
|
|
311
|
-
- [ ] **Step 5: Commit** — `feat(tui): StatusBar/AppView — pending-permission banner + [p] hint`.
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## Task 6: App.tsx — poll permissions, write transitions, handle `jump-to-pending`
|
|
316
|
-
|
|
317
|
-
**Files:** `src/tui/App.tsx`
|
|
318
|
-
|
|
319
|
-
This is the integration task. Three changes to `App.tsx`:
|
|
320
|
-
|
|
321
|
-
**A. New `useEffect` for permission polling (2 s interval, like the activity poller):**
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
const tick = async (): Promise<void> => {
|
|
326
|
-
const s = await readState(props.orchestraId);
|
|
327
|
-
if (!s) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const transitions = await pollPermissions(s);
|
|
331
|
-
for (const t of transitions) {
|
|
332
|
-
try {
|
|
333
|
-
await setMusicianStatus(
|
|
334
|
-
props.orchestraId,
|
|
335
|
-
t.musicianId,
|
|
336
|
-
t.newStatus,
|
|
337
|
-
t.pendingPermission,
|
|
338
|
-
);
|
|
339
|
-
} catch {
|
|
340
|
-
// Musician may have been dismissed between poll and write; safe to swallow.
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
void tick();
|
|
345
|
-
const timer = setInterval(() => { void tick(); }, 2000);
|
|
346
|
-
return () => { clearInterval(timer); };
|
|
347
|
-
}, [props.orchestraId]);
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Place this AFTER the existing activity poll effect for clarity. The two pollers are independent — both run on a 2 s cadence; they could be folded but separation makes the responsibilities obvious and keeps `pollActivity` purely ephemeral while `pollPermissions` is durable.
|
|
351
|
-
|
|
352
|
-
Imports to add at the top:
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
import { pollPermissions } from './poll-permission.js';
|
|
356
|
-
import { setMusicianStatus } from '../state-updaters.js';
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
**B. Handle the new `jump-to-pending` action in the `useInput` cascade:**
|
|
360
|
-
|
|
361
|
-
After the `dismiss-musician` branch, before the no-op comment for `next/prev-orchestra`:
|
|
362
|
-
|
|
363
|
-
```ts
|
|
364
|
-
if (action.kind === 'jump-to-pending') {
|
|
365
|
-
const pending = musicians.find((m) => { return m.status === 'awaiting_permission'; });
|
|
366
|
-
if (pending) {
|
|
367
|
-
void selectWindow(session, pending.tmux_window_id);
|
|
368
|
-
}
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
**C. Compute and pass `pendingCount` to `<AppView>`:**
|
|
374
|
-
|
|
375
|
-
```ts
|
|
376
|
-
const pendingCount = musicians.filter((m) => { return m.status === 'awaiting_permission'; }).length;
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
Then pass it through: `<AppView ... pendingCount={pendingCount} />`.
|
|
380
|
-
|
|
381
|
-
- [ ] **Step 1: Add the new useEffect** in App.tsx after the activity poller.
|
|
382
|
-
- [ ] **Step 2: Add imports** for `pollPermissions` and `setMusicianStatus`.
|
|
383
|
-
- [ ] **Step 3: Extend the action cascade** with the `jump-to-pending` branch.
|
|
384
|
-
- [ ] **Step 4: Compute `pendingCount`** and thread it into `<AppView>`.
|
|
385
|
-
- [ ] **Step 5: Verify** — `npm run build` clean, `npm test` all pass.
|
|
386
|
-
- [ ] **Step 6: Commit** — `feat(tui): App — wire permission poller, jump-to-pending, pendingCount`.
|
|
387
|
-
|
|
388
|
-
App.tsx is the only file in this plan without a dedicated unit test (it's a thin integration container, mirroring Phase 3's decision). It's covered by the Task 7 manual smoke and by the components' unit tests.
|
|
389
|
-
|
|
390
|
-
---
|
|
391
|
-
|
|
392
|
-
## Task 7: Manual smoke test (deferred to user)
|
|
393
|
-
|
|
394
|
-
**Files:** none — runtime exercise.
|
|
395
|
-
|
|
396
|
-
Setup:
|
|
397
|
-
- `npm run build && npm link` (if not already linked since Phase 3).
|
|
398
|
-
- Run `nfo` in a throwaway repo to launch an orchestra at `supervised` level (the default).
|
|
399
|
-
|
|
400
|
-
Steps:
|
|
401
|
-
1. From the Orchestrator pane, ask claude to spawn a musician with a task that will hit a permission prompt: e.g. `spawn_musician({ name: "rm-tester", task: "Run `rm -rf .git/no-such` and report what happens" })`. With `supervised`, the Bash invocation should trigger a permission prompt in the musician's pane.
|
|
402
|
-
2. Watch the Auditorium in the right pane (within ~2 s):
|
|
403
|
-
- The musician's status icon flips to `⚠` (yellow).
|
|
404
|
-
- The activity line shows `awaiting: Bash: ...`.
|
|
405
|
-
- The StatusBar shows the yellow banner `⚠ 1 awaiting permission · [p] jump to next`.
|
|
406
|
-
3. Press `p` in the right pane — the tmux session should switch to the musician's window, showing claude's permission prompt.
|
|
407
|
-
4. Answer the prompt (`3` for No, or `1` to allow). Press `q` (or use prefix-arrow) to return to the orchestrator window; the right pane should still be there.
|
|
408
|
-
5. Within ~2 s, the Auditorium row flips back to `●` (green) and the StatusBar banner disappears.
|
|
409
|
-
|
|
410
|
-
If any step fails, the most likely culprits are the detector's pattern matching (Task 1 — adjust signals to match the actual claude version's prompt shape) or the chokidar/poll race in App.tsx (transitions race against the state watcher refresh).
|
|
411
|
-
|
|
412
|
-
- [ ] **Step 1: Run the manual smoke** as described. Document any deviation from expected behavior.
|
|
413
|
-
|
|
414
|
-
---
|
|
415
|
-
|
|
416
|
-
## Task 8: README update
|
|
417
|
-
|
|
418
|
-
**Files:** `README.md`
|
|
419
|
-
|
|
420
|
-
**Status section update.** Append to the "Status" paragraph:
|
|
421
|
-
|
|
422
|
-
> Phase 4 adds permission-prompt detection: when a Musician's claude session is stuck on a permission prompt (only possible in `supervised` or `strict` mode), the Auditorium flips that Musician to `⚠ awaiting permission`, the status bar shows a yellow banner, and `p` jumps you straight to that Musician's tmux window so you can answer claude's prompt. The bell/notification flag, `?` help overlay, real quit, token hint, and Concert Hall switching ship in a later phase.
|
|
423
|
-
|
|
424
|
-
**Keybindings list.** If you maintain a keybindings paragraph in README, add `p` → "jump to next Musician awaiting permission" (only meaningful when one or more are).
|
|
425
|
-
|
|
426
|
-
- [ ] **Step 1: Edit README.md** — status paragraph and (if applicable) keybindings list.
|
|
427
|
-
- [ ] **Step 2: Commit** — `docs: README for Phase 4`.
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
|
-
## Task 9: Final audit + tag
|
|
432
|
-
|
|
433
|
-
**Files:** none directly.
|
|
434
|
-
|
|
435
|
-
- [ ] **Step 1: Run the full suite** — `npm run build && npm test`. Both must be 100% clean.
|
|
436
|
-
- [ ] **Step 2: Self-audit checklist:**
|
|
437
|
-
- [ ] No `JSX.Element` in any Phase 4 file (all components use `ReactElement`).
|
|
438
|
-
- [ ] No shorthand control flow or implicit-return arrow callbacks in any Phase 4 file (`grep -nE "=>\s*[^{(]" src/tui/*.ts src/tui/*.tsx` should not produce hits in new code).
|
|
439
|
-
- [ ] All commits on `feat/ink-app` since the `phase-3-complete` tag use the 4.7 trailer.
|
|
440
|
-
- [ ] `pollPermissions` emits ZERO transitions when state is unchanged (no chokidar storm).
|
|
441
|
-
- [ ] The detector's three-signal AND rule actually fires only on real prompts — re-read Task 1's edge cases against your implementation.
|
|
442
|
-
- [ ] `jump-to-pending` no-ops gracefully when no Musician is pending.
|
|
443
|
-
- [ ] StatusBar banner is hidden when `pendingCount === 0` (no empty `⚠` row).
|
|
444
|
-
- [ ] Auditorium falls back to `awaiting: tool` when `pending_permission` is null.
|
|
445
|
-
- [ ] No Phase 4 file modifies state schema, MCP server, or musician primitives.
|
|
446
|
-
- [ ] `nfo tui --help` and `nfo --help` still render without errors (no accidental command-registration changes).
|
|
447
|
-
- [ ] **Step 3: Tag** — `git tag phase-4-complete`.
|
|
448
|
-
|
|
449
|
-
---
|
|
450
|
-
|
|
451
|
-
## Out-of-scope wrap-up (carried to Phase 5)
|
|
452
|
-
|
|
453
|
-
A future Phase 5 plan should cover the items deferred here:
|
|
454
|
-
|
|
455
|
-
- `notify_on_permission` config flag with terminal bell + `notify-send` / `osascript` desktop notification.
|
|
456
|
-
- `?` help overlay (toggle key showing all key bindings).
|
|
457
|
-
- Real quit binding (Ctrl+C is already handled by Ink; consider Escape or `Q` if there's value in an explicit user-facing quit beyond what tmux already provides).
|
|
458
|
-
- Token usage hint — parse claude's status line from the captured pane and surface to the StatusBar.
|
|
459
|
-
- Concert Hall orchestra-switching — Tab/Shift-Tab actually swapping the attached session.
|
|
460
|
-
- Re-evaluating whether the activity and permission pollers should be folded into a single pass (they currently capture the same pane twice).
|