nfo-cli 0.0.4-improve-prompting → 0.0.6-a89844d-dev

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.
Files changed (178) hide show
  1. package/dist/claude-command.js +6 -1
  2. package/dist/claude-command.js.map +1 -1
  3. package/dist/claude-trust.js +46 -0
  4. package/dist/claude-trust.js.map +1 -0
  5. package/dist/cli.js +5 -4
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/attach.js +8 -8
  8. package/dist/commands/attach.js.map +1 -1
  9. package/dist/commands/launch.js +3 -6
  10. package/dist/commands/launch.js.map +1 -1
  11. package/dist/commands/restore.js +6 -10
  12. package/dist/commands/restore.js.map +1 -1
  13. package/dist/commands/tui.js +17 -1
  14. package/dist/commands/tui.js.map +1 -1
  15. package/dist/mcp/handlers.js +5 -0
  16. package/dist/mcp/handlers.js.map +1 -1
  17. package/dist/mcp/tool-defs.js +10 -0
  18. package/dist/mcp/tool-defs.js.map +1 -1
  19. package/dist/musicians/dismiss.js +1 -1
  20. package/dist/musicians/dismiss.js.map +1 -1
  21. package/dist/musicians/reconcile.js +27 -0
  22. package/dist/musicians/reconcile.js.map +1 -0
  23. package/dist/musicians/roles.js +15 -0
  24. package/dist/musicians/roles.js.map +1 -0
  25. package/dist/musicians/spawn.js +53 -18
  26. package/dist/musicians/spawn.js.map +1 -1
  27. package/dist/permission.js +6 -0
  28. package/dist/permission.js.map +1 -1
  29. package/dist/prompts/musician-role.js +2 -1
  30. package/dist/prompts/musician-role.js.map +1 -1
  31. package/dist/prompts/orchestrator-role.js +18 -6
  32. package/dist/prompts/orchestrator-role.js.map +1 -1
  33. package/dist/prompts/tool-discipline.js +7 -3
  34. package/dist/prompts/tool-discipline.js.map +1 -1
  35. package/dist/tmux.js +23 -0
  36. package/dist/tmux.js.map +1 -1
  37. package/dist/tui/components/App.js +8 -12
  38. package/dist/tui/components/App.js.map +1 -1
  39. package/dist/tui/components/Help.js +1 -1
  40. package/dist/tui/components/Help.js.map +1 -1
  41. package/dist/tui/keymap.js +1 -1
  42. package/dist/tui/keymap.js.map +1 -1
  43. package/package.json +18 -8
  44. package/assets/agent-screen.png +0 -0
  45. package/assets/main-screen.png +0 -0
  46. package/assets/orche-clawd.png +0 -0
  47. package/dist/tui/App.js +0 -428
  48. package/dist/tui/App.js.map +0 -1
  49. package/dist/tui/AppView.js +0 -13
  50. package/dist/tui/AppView.js.map +0 -1
  51. package/dist/tui/Auditorium.js +0 -17
  52. package/dist/tui/Auditorium.js.map +0 -1
  53. package/dist/tui/ConcertHall.js +0 -11
  54. package/dist/tui/ConcertHall.js.map +0 -1
  55. package/dist/tui/Help.js +0 -49
  56. package/dist/tui/Help.js.map +0 -1
  57. package/dist/tui/OrchestratorPane.js +0 -34
  58. package/dist/tui/OrchestratorPane.js.map +0 -1
  59. package/dist/tui/SidebarHeader.js +0 -6
  60. package/dist/tui/SidebarHeader.js.map +0 -1
  61. package/dist/tui/StatusBar.js +0 -6
  62. package/dist/tui/StatusBar.js.map +0 -1
  63. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  64. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  65. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  66. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  67. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  68. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  69. package/plan-explorer-musician-hardening.md +0 -56
  70. package/src/claude-command.ts +0 -35
  71. package/src/claude-detect.ts +0 -42
  72. package/src/cli.ts +0 -197
  73. package/src/commands/attach.ts +0 -24
  74. package/src/commands/dashboard-window.ts +0 -33
  75. package/src/commands/kill.ts +0 -50
  76. package/src/commands/launch.ts +0 -134
  77. package/src/commands/list.ts +0 -43
  78. package/src/commands/mcp-server.ts +0 -18
  79. package/src/commands/notes.ts +0 -18
  80. package/src/commands/restore.ts +0 -153
  81. package/src/commands/tui.tsx +0 -22
  82. package/src/config.ts +0 -44
  83. package/src/dashboard.ts +0 -1
  84. package/src/mcp/config.ts +0 -39
  85. package/src/mcp/handlers.ts +0 -141
  86. package/src/mcp/server.ts +0 -50
  87. package/src/mcp/tool-defs.ts +0 -151
  88. package/src/musicians/dismiss.ts +0 -60
  89. package/src/musicians/ids.ts +0 -21
  90. package/src/musicians/lookup.ts +0 -13
  91. package/src/musicians/message-log.ts +0 -152
  92. package/src/musicians/message.ts +0 -99
  93. package/src/musicians/query.ts +0 -19
  94. package/src/musicians/spawn.ts +0 -139
  95. package/src/notes.ts +0 -39
  96. package/src/notify.ts +0 -62
  97. package/src/orchestrator/report-back.ts +0 -33
  98. package/src/permission.ts +0 -30
  99. package/src/project-key.ts +0 -12
  100. package/src/prompts/musician-role.ts +0 -22
  101. package/src/prompts/orchestrator-role.ts +0 -84
  102. package/src/prompts/tool-discipline.ts +0 -41
  103. package/src/repo.ts +0 -14
  104. package/src/shell-quote.ts +0 -7
  105. package/src/state-updaters.ts +0 -132
  106. package/src/state.ts +0 -49
  107. package/src/state.types.ts +0 -67
  108. package/src/tmux.ts +0 -226
  109. package/src/tui/activity-line.ts +0 -16
  110. package/src/tui/components/App.tsx +0 -534
  111. package/src/tui/components/AppView.tsx +0 -98
  112. package/src/tui/components/Auditorium.tsx +0 -56
  113. package/src/tui/components/ConcertHall.tsx +0 -31
  114. package/src/tui/components/Help.tsx +0 -63
  115. package/src/tui/components/OrchestratorPane.tsx +0 -98
  116. package/src/tui/components/SidebarHeader.tsx +0 -34
  117. package/src/tui/components/StatusBar.tsx +0 -42
  118. package/src/tui/detect-permission.ts +0 -93
  119. package/src/tui/embedded-session-lifecycle.ts +0 -44
  120. package/src/tui/embedded-terminal.ts +0 -325
  121. package/src/tui/format-time.ts +0 -25
  122. package/src/tui/keymap.ts +0 -104
  123. package/src/tui/poll-activity.ts +0 -25
  124. package/src/tui/poll-idle.ts +0 -149
  125. package/src/tui/poll-permission.ts +0 -50
  126. package/src/tui/status-icon.ts +0 -35
  127. package/src/tui/terminal-input.ts +0 -136
  128. package/src/tui/watch-state.ts +0 -43
  129. package/src/worktree.ts +0 -41
  130. package/tests/claude-command.test.ts +0 -30
  131. package/tests/claude-detect.test.ts +0 -14
  132. package/tests/commands/attach.test.ts +0 -60
  133. package/tests/commands/kill.test.ts +0 -66
  134. package/tests/commands/launch.test.ts +0 -75
  135. package/tests/commands/list.test.ts +0 -47
  136. package/tests/commands/notes.test.ts +0 -53
  137. package/tests/commands/restore.test.ts +0 -126
  138. package/tests/helpers/tmp-config.ts +0 -16
  139. package/tests/helpers/tmp-repo.ts +0 -29
  140. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  141. package/tests/mcp/handlers.test.ts +0 -163
  142. package/tests/mcp/tool-defs.test.ts +0 -35
  143. package/tests/musicians/dismiss.test.ts +0 -102
  144. package/tests/musicians/message.test.ts +0 -159
  145. package/tests/musicians/query.test.ts +0 -65
  146. package/tests/musicians/spawn.test.ts +0 -125
  147. package/tests/notes.test.ts +0 -56
  148. package/tests/notify.test.ts +0 -80
  149. package/tests/orchestrator/report-back.test.ts +0 -18
  150. package/tests/permission.test.ts +0 -39
  151. package/tests/project-key.test.ts +0 -33
  152. package/tests/prompts/tool-discipline.test.ts +0 -25
  153. package/tests/repo.test.ts +0 -38
  154. package/tests/state-updaters.test.ts +0 -126
  155. package/tests/state.test.ts +0 -85
  156. package/tests/tmux.test.ts +0 -126
  157. package/tests/tui/AppView.test.tsx +0 -92
  158. package/tests/tui/Auditorium.test.tsx +0 -67
  159. package/tests/tui/ConcertHall.test.tsx +0 -22
  160. package/tests/tui/Help.test.tsx +0 -38
  161. package/tests/tui/OrchestratorPane.test.ts +0 -30
  162. package/tests/tui/SidebarHeader.test.tsx +0 -20
  163. package/tests/tui/StatusBar.test.tsx +0 -51
  164. package/tests/tui/activity-line.test.ts +0 -21
  165. package/tests/tui/detect-permission.test.ts +0 -92
  166. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  167. package/tests/tui/embedded-terminal.test.ts +0 -80
  168. package/tests/tui/format-time.test.ts +0 -25
  169. package/tests/tui/keymap.test.ts +0 -93
  170. package/tests/tui/poll-activity.test.ts +0 -81
  171. package/tests/tui/poll-idle.test.ts +0 -159
  172. package/tests/tui/poll-permission.test.ts +0 -222
  173. package/tests/tui/status-icon.test.ts +0 -27
  174. package/tests/tui/terminal-input.test.ts +0 -113
  175. package/tests/tui/watch-state.test.ts +0 -54
  176. package/tests/worktree.test.ts +0 -73
  177. package/tsconfig.json +0 -19
  178. package/vitest.config.ts +0 -12
@@ -1,84 +0,0 @@
1
- import { ORCHESTRATOR_TOOL_DISCIPLINE } from "./tool-discipline.js";
2
-
3
- export const ORCHESTRATOR_ROLE_PROMPT_V1 = `You are the Orchestrator of an NFO orchestra.
4
-
5
- NFO (NoFluffOrchestra) is a TUI for multi-agent work on the user's repository.
6
- You coordinate Musicians (other Claude Code agents) via the NFO MCP tools.
7
-
8
- Available NFO tools (in addition to your normal Claude Code tools):
9
-
10
- spawn_musician({ name, task, worktree?, branch_from?, model? })
11
- Create a Musician with the given task. By default the Musician runs in a
12
- fresh git worktree off HEAD.
13
- Pass worktree=false for trivially isolated and research work (e.g., docs-only) that doesn't need an isolated branch. Returns the
14
- musician_id. Provide a model to be used by the Musician, otherwise it defaults to sonnet.
15
- For trivial tasks Haiku is a good choice; for complex coding work, Sonnet is better.
16
-
17
- message_musician({ musician_id, message })
18
- Send a message to a Musician. If the Musician is idle, NFO delivers it
19
- immediately. If the Musician is still working, NFO queues it and delivers
20
- it automatically on the next idle boundary.
21
-
22
- query_musician({ musician_id, lines? })
23
- Read the most recent visible output from the Musician's pane. Use this
24
- sparingly — capture-pane is heuristic and may include rendering artifacts.
25
-
26
- list_musicians()
27
- Return all currently-active Musicians with their status.
28
-
29
- dismiss_musician({ musician_id, archive_worktree? })
30
- Tear down a Musician. The worktree is archived under
31
- .../archive/<musician_id>/worktree (the branch is preserved). Pass
32
- archive_worktree=false to drop the worktree entirely. By default drop the worktree. Ask
33
- the user before archiving, as these can accumulate and consume disk space.
34
-
35
- note_write({ filename, content }) / note_read({ filename }) / note_list()
36
- Your private project memory under ~/.config/nfo/projects/<key>/notes/.
37
- On every fresh Orchestrator session, the contents of notes/overview.md
38
- and notes/decisions.md are loaded into your context automatically.
39
- Use these to record decisions, open questions, and durable project
40
- understanding the user would want you to remember next session.
41
-
42
- Coordination guidance:
43
-
44
- - ${ORCHESTRATOR_TOOL_DISCIPLINE.trim().replace(/\n/g, "\n ")}
45
- - For agent coordination, PREFER the NFO MCP tools over Claude Code's built-in
46
- Task tool. The user tracks Musician work through NFO; Task spawns are invisible
47
- to NFO.
48
- - Deploy research musicians that investigate the codebase for the given task. They are only allowed
49
- to research and report back findings, without modifying the codebase.
50
- - Before spawning a coding Musician, prepare a complete taks spec:
51
- Relevant paths, line numbers, the exact changes and why, acceptance criteria,
52
- and any constraints (e.g., "don't break the build", "only touch files in the /widget/ directory", "follow the existing style in this file").
53
- A well-scoped prompt is your primary output on coding requests.
54
- - Worktrees solve concurrent file-edit safety, not API coupling. If two
55
- Musicians' outputs need to be wired together, sequence the work, or spawn an
56
- integration Musician afterward.
57
- - The orchestra's permission level applies to every Musician you spawn.
58
- - Prefer concise follow-up nudges. NFO persists them in JSONL and batches any
59
- backlog before a Musician truly becomes idle again.
60
- - When a Musician calls \`report_done\` and no queued follow-up is waiting, NFO
61
- pushes that completion back into your Claude session. Review the report
62
- promptly and either dismiss the Musician or send the next iteration.
63
- - Project-level guidance in CLAUDE.md still applies; respect it.
64
- - You can use Superpowers if present but make sure that works are delegated to
65
- Musicians in the end if subagent driven development is picked by the user.
66
- - Coding task workflow (two stages):
67
-
68
- Stage 1 — Explorer Musician (Sonnet, worktree=false):
69
- Task the Explorer to find and report back:
70
- • Relevant file paths and line numbers for the change.
71
- • The existing pattern or convention to follow.
72
- • Any callers / dependents that may be affected (blast radius).
73
- • Anything that would block or constrain the implementation.
74
-
75
- Stage 2 — Coder Musician (Haiku preferred or Sonnet if really needed, fresh worktree if required):
76
- Build the task spec from the Explorer's findings. Include:
77
- • Exact files and line numbers to touch.
78
- • The change required and why (one sentence).
79
- • The pattern to follow (point to an existing example in the codebase).
80
- • Acceptance criteria (what done looks like).
81
- • Explicit constraints (don't break X, preserve Y interface).
82
- A well-scoped spec is your primary output for coding requests.
83
-
84
- `;
@@ -1,41 +0,0 @@
1
- export const ORCHESTRATOR_TOOL_DISCIPLINE = `Tool discipline (mandatory):
2
-
3
- - Use the NFO MCP tools for all musician coordination and orchestra-local memory.
4
- - Never merely say that you will spawn, message, query, list, dismiss, or note
5
- something later. Call the corresponding NFO tool in the same turn.
6
- - Do not use Claude Code's built-in Task tool for Musician coordination; those
7
- agents are invisible to NFO.
8
- - Never write, edit or refactor code yourself. All coding tasks must be delegated
9
- to a Musician via \`spawn_musician\`. Your task as an Orchestrator is to prepare
10
- and hand off work, not to execute it.
11
- - For any coding task, always spawn a Sonnet Explorer Musician first (worktree=false, model="sonnet"), Only
12
- after the Explorer reports back may you spawn a Coder Musician. Never read source files
13
- yourself to build a coding task spec.
14
- - When a Musician reports back, resolve it in the same turn with an NFO tool
15
- call (usually \`dismiss_musician\` or \`message_musician\`). A prose-only
16
- acknowledgement is non-compliant.
17
- `;
18
-
19
- export const MUSICIAN_TOOL_DISCIPLINE = `Tool discipline (mandatory):
20
-
21
- - Use NFO MCP tools for orchestra coordination. Plain-text status reports are
22
- not a valid handoff.
23
- - When your assigned task is complete and ready for Orchestrator review, your
24
- next action must be \`report_done({ summary, next_steps? })\`.
25
- - Do not end with "done", "finished", or similar prose instead of calling
26
- \`report_done\`.
27
- - After \`report_done\`, wait for the Orchestrator to send the next task or
28
- dismiss you.
29
- `;
30
-
31
- export function buildMusicianInitialPrompt(task: string): string {
32
- const trimmedTask = task.trim();
33
- const body = trimmedTask.length > 0 ? trimmedTask : task;
34
-
35
- return `${body}
36
-
37
- NFO operating contract (mandatory):
38
- - Use the NFO MCP tools for orchestra coordination.
39
- - When you finish this task and are ready to hand it back, call \`report_done({ summary, next_steps? })\` instead of replying with a plain-text completion message.
40
- - After \`report_done\`, wait for the Orchestrator to message you again or dismiss you.`;
41
- }
package/src/repo.ts DELETED
@@ -1,14 +0,0 @@
1
- import { execa } from 'execa';
2
-
3
- export async function resolveRepoRoot(cwd: string): Promise<string | null> {
4
- try {
5
- const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
6
- cwd,
7
- reject: false,
8
- });
9
- const trimmed = stdout.trim();
10
- return trimmed.length > 0 ? trimmed : null;
11
- } catch {
12
- return null;
13
- }
14
- }
@@ -1,7 +0,0 @@
1
- export function shellQuote(value: string): string {
2
- if (value.length === 0) {
3
- return "''";
4
- }
5
-
6
- return `'${value.replace(/'/g, `'\"'\"'`)}'`;
7
- }
@@ -1,132 +0,0 @@
1
- import { readState, writeState } from './state.js';
2
- import type {
3
- ArchivedMusician,
4
- Musician,
5
- MusicianReport,
6
- MusicianStatus,
7
- OrchestraState,
8
- } from './state.types.js';
9
-
10
- async function update(
11
- orchestraId: string,
12
- mutator: (s: OrchestraState) => void,
13
- ): Promise<void> {
14
- const state = await readState(orchestraId);
15
- if (!state) {
16
- throw new Error(`Unknown orchestra: ${orchestraId}`);
17
- }
18
- mutator(state);
19
- await writeState(orchestraId, state);
20
- }
21
-
22
- export async function addMusician(orchestraId: string, m: Musician): Promise<void> {
23
- await update(orchestraId, (s) => { s.musicians.push(m); });
24
- }
25
-
26
- export async function setMusicianStatus(
27
- orchestraId: string,
28
- musicianId: string,
29
- status: MusicianStatus,
30
- pendingPermission?: string | null,
31
- ): Promise<void> {
32
- await update(orchestraId, (s) => {
33
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
34
- if (!m) {
35
- throw new Error(`Unknown musician: ${musicianId}`);
36
- }
37
- m.status = status;
38
- if (pendingPermission !== undefined) {
39
- m.pending_permission = pendingPermission;
40
- }
41
- });
42
- }
43
-
44
- export async function setMusicianClaudeSessionId(
45
- orchestraId: string,
46
- musicianId: string,
47
- sessionId: string,
48
- ): Promise<void> {
49
- await update(orchestraId, (s) => {
50
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
51
- if (!m) {
52
- throw new Error(`Unknown musician: ${musicianId}`);
53
- }
54
- m.claude_session_id = sessionId;
55
- });
56
- }
57
-
58
- export async function setMusicianTmuxWindowId(
59
- orchestraId: string,
60
- musicianId: string,
61
- tmuxWindowId: string,
62
- ): Promise<void> {
63
- await update(orchestraId, (s) => {
64
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
65
- if (!m) {
66
- throw new Error(`Unknown musician: ${musicianId}`);
67
- }
68
- m.tmux_window_id = tmuxWindowId;
69
- });
70
- }
71
-
72
- export async function touchMusicianActivity(
73
- orchestraId: string,
74
- musicianId: string,
75
- timestamp?: string,
76
- ): Promise<void> {
77
- const ts = timestamp ?? new Date().toISOString();
78
- await update(orchestraId, (s) => {
79
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
80
- if (!m) {
81
- throw new Error(`Unknown musician: ${musicianId}`);
82
- }
83
- m.last_activity = ts;
84
- });
85
- }
86
-
87
- export async function setMusicianLatestReport(
88
- orchestraId: string,
89
- musicianId: string,
90
- report: MusicianReport | null,
91
- ): Promise<void> {
92
- await update(orchestraId, (s) => {
93
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
94
- if (!m) {
95
- throw new Error(`Unknown musician: ${musicianId}`);
96
- }
97
- m.latest_report = report;
98
- });
99
- }
100
-
101
- export async function setOrchestratorSessionId(
102
- orchestraId: string,
103
- sessionId: string,
104
- ): Promise<void> {
105
- await update(orchestraId, (s) => { s.orchestrator_session_id = sessionId; });
106
- }
107
-
108
- export interface ArchiveArgs {
109
- summary: string | null;
110
- dismissedAt?: string;
111
- }
112
-
113
- export async function archiveMusician(
114
- orchestraId: string,
115
- musicianId: string,
116
- args: ArchiveArgs,
117
- ): Promise<void> {
118
- await update(orchestraId, (s) => {
119
- const idx = s.musicians.findIndex((mu) => { return mu.id === musicianId; });
120
- if (idx === -1) {
121
- throw new Error(`Unknown musician: ${musicianId}`);
122
- }
123
- const [m] = s.musicians.splice(idx, 1);
124
- const archived: ArchivedMusician = {
125
- ...m,
126
- status: 'stopped',
127
- dismissed_at: args.dismissedAt ?? new Date().toISOString(),
128
- summary: args.summary,
129
- };
130
- s.archived_musicians.push(archived);
131
- });
132
- }
package/src/state.ts DELETED
@@ -1,49 +0,0 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
4
- import lockfile from 'proper-lockfile';
5
- import {
6
- notesDir,
7
- logsDir,
8
- messageLogsDir,
9
- worktreesDir,
10
- archiveDir,
11
- stateFile,
12
- orchestraDir,
13
- } from './config.js';
14
- import type { OrchestraState } from './state.types.js';
15
-
16
- export async function ensureOrchestraDir(projectKey: string): Promise<void> {
17
- await mkdir(orchestraDir(projectKey), { recursive: true });
18
- await mkdir(notesDir(projectKey), { recursive: true });
19
- await mkdir(logsDir(projectKey), { recursive: true });
20
- await mkdir(messageLogsDir(projectKey), { recursive: true });
21
- await mkdir(worktreesDir(projectKey), { recursive: true });
22
- await mkdir(archiveDir(projectKey), { recursive: true });
23
- }
24
-
25
- export async function readState(projectKey: string): Promise<OrchestraState | null> {
26
- const file = stateFile(projectKey);
27
- if (!existsSync(file)) return null;
28
- const buf = await readFile(file, 'utf8');
29
- return JSON.parse(buf) as OrchestraState;
30
- }
31
-
32
- export async function writeState(projectKey: string, state: OrchestraState): Promise<void> {
33
- const file = stateFile(projectKey);
34
- await mkdir(dirname(file), { recursive: true });
35
-
36
- // proper-lockfile needs the target file to exist before it can lock it.
37
- if (!existsSync(file)) {
38
- await writeFile(file, '{}', 'utf8');
39
- }
40
-
41
- const release = await lockfile.lock(file, { retries: { retries: 5, minTimeout: 50 } });
42
- try {
43
- const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
44
- await writeFile(tmp, JSON.stringify(state, null, 2), 'utf8');
45
- await rename(tmp, file);
46
- } finally {
47
- await release();
48
- }
49
- }
@@ -1,67 +0,0 @@
1
- import type { PermissionLevel } from "./permission.js";
2
-
3
- export type MusicianStatus =
4
- | "working"
5
- | "idle"
6
- | "awaiting_permission"
7
- | "stopped";
8
- export type SubagentModel = "sonnet" | "haiku";
9
-
10
- export interface MusicianReport {
11
- summary: string;
12
- next_steps: string | null;
13
- reported_at: string;
14
- }
15
-
16
- export interface Musician {
17
- id: string;
18
- name: string;
19
- task_summary: string;
20
- status: MusicianStatus;
21
- pending_permission?: string | null;
22
- tmux_window_id: string;
23
- claude_session_id: string | null;
24
- worktree_path: string | null;
25
- branch: string | null;
26
- spawned_at: string;
27
- last_activity: string;
28
- latest_report?: MusicianReport | null;
29
- model?: SubagentModel;
30
- }
31
-
32
- export interface ArchivedMusician extends Musician {
33
- dismissed_at: string;
34
- summary: string | null;
35
- }
36
-
37
- export interface OrchestraState {
38
- version: number;
39
- orchestra_id: string;
40
- project_path: string;
41
- created_at: string;
42
- permission_level: PermissionLevel;
43
- notify_on_permission?: boolean;
44
- orchestrator_session_id: string | null;
45
- musicians: Musician[];
46
- archived_musicians: ArchivedMusician[];
47
- }
48
-
49
- export function makeInitialState(args: {
50
- orchestraId: string;
51
- projectPath: string;
52
- permissionLevel: PermissionLevel;
53
- notifyOnPermission?: boolean;
54
- }): OrchestraState {
55
- const now = new Date().toISOString();
56
- return {
57
- version: 1,
58
- orchestra_id: args.orchestraId,
59
- project_path: args.projectPath,
60
- created_at: now,
61
- permission_level: args.permissionLevel,
62
- notify_on_permission: args.notifyOnPermission ?? false,
63
- orchestrator_session_id: null,
64
- musicians: [],
65
- archived_musicians: [],
66
- };
67
- }
package/src/tmux.ts DELETED
@@ -1,226 +0,0 @@
1
- import { execa } from 'execa';
2
- import { DASHBOARD_WINDOW_NAME } from './dashboard.js';
3
-
4
- const NFO_SESSION_MATCH = '#{m:^nfo-,#{session_name}}';
5
- const EMBED_SESSION_SUFFIX = '-embed';
6
- const REQUIRED_TERMINAL_FEATURES = [
7
- 'xterm*:extkeys',
8
- 'screen*:extkeys',
9
- 'tmux*:extkeys',
10
- ];
11
-
12
- export function sessionName(projectKey: string): string {
13
- return `nfo-${projectKey}`;
14
- }
15
-
16
- export function embeddedSessionName(projectKey: string): string {
17
- return `${sessionName(projectKey)}${EMBED_SESSION_SUFFIX}`;
18
- }
19
-
20
- export async function sessionExists(name: string): Promise<boolean> {
21
- const result = await execa('tmux', ['has-session', '-t', name], { reject: false });
22
- return result.exitCode === 0;
23
- }
24
-
25
- export async function createDetachedSession(
26
- name: string,
27
- cwd: string,
28
- width = 220,
29
- height = 50,
30
- ): Promise<void> {
31
- await execa('tmux', ['new-session', '-d', '-s', name, '-c', cwd, '-x', String(width), '-y', String(height)]);
32
- }
33
-
34
- export async function createDetachedWindow(
35
- session: string,
36
- windowName: string,
37
- cwd: string,
38
- command?: string,
39
- ): Promise<string> {
40
- const args = [
41
- 'new-window',
42
- '-t',
43
- session,
44
- '-n',
45
- windowName,
46
- '-c',
47
- cwd,
48
- '-d',
49
- '-P',
50
- '-F',
51
- '#{pane_id}',
52
- ];
53
- if (command) {
54
- args.push(command);
55
- }
56
- const { stdout } = await execa('tmux', args);
57
- return stdout.trim();
58
- }
59
-
60
- export async function killSession(name: string): Promise<void> {
61
- await execa('tmux', ['kill-session', '-t', name], { reject: false });
62
- }
63
-
64
- export async function createLinkedSession(
65
- sourceSession: string,
66
- linkedSession: string,
67
- cwd: string,
68
- ): Promise<void> {
69
- await execa('tmux', ['new-session', '-d', '-t', sourceSession, '-s', linkedSession, '-c', cwd]);
70
- }
71
-
72
- export async function ensureEmbeddedSession(
73
- sourceSession: string,
74
- linkedSession: string,
75
- cwd: string,
76
- ): Promise<void> {
77
- if (!(await sessionExists(linkedSession))) {
78
- await createLinkedSession(sourceSession, linkedSession, cwd);
79
- }
80
- await ensureNfoSessionUi(linkedSession);
81
- await selectWindow(linkedSession, '0');
82
- }
83
-
84
- export async function attachSession(name: string): Promise<void> {
85
- // Inherits stdio so the user's terminal becomes the tmux client.
86
- await execa('tmux', ['attach-session', '-t', name], { stdio: 'inherit' });
87
- }
88
-
89
- export async function detachCurrentClient(): Promise<void> {
90
- await execa('tmux', ['detach-client']);
91
- }
92
-
93
- export async function splitWindowHorizontal(
94
- target: string,
95
- columns: number,
96
- command?: string,
97
- ): Promise<void> {
98
- // Use -l (absolute column count) rather than -p (percent) because percent
99
- // requires an attached client with a known terminal size.
100
- const args = ['split-window', '-h', '-l', String(columns), '-t', target];
101
- if (command) args.push(command);
102
- await execa('tmux', args);
103
- }
104
-
105
- export async function sendKeys(target: string, text: string, withEnter: boolean): Promise<void> {
106
- // Use -l (literal) to avoid keystroke interpretation.
107
- await execa('tmux', ['send-keys', '-l', '-t', target, '--', text]);
108
- if (withEnter) {
109
- await execa('tmux', ['send-keys', '-t', target, 'Enter']);
110
- }
111
- }
112
-
113
- export async function respawnPane(target: string, command: string): Promise<void> {
114
- await execa('tmux', ['respawn-pane', '-k', '-t', target, command]);
115
- }
116
-
117
- export async function capturePane(target: string, lines: number): Promise<string> {
118
- const { stdout } = await execa('tmux', [
119
- 'capture-pane',
120
- '-p',
121
- '-t',
122
- target,
123
- '-S',
124
- `-${lines}`,
125
- ]);
126
- return stdout;
127
- }
128
-
129
- export async function captureVisiblePane(target: string): Promise<string> {
130
- const { stdout } = await execa('tmux', [
131
- 'capture-pane',
132
- '-p',
133
- '-N',
134
- '-t',
135
- target,
136
- '-S',
137
- '0',
138
- '-E',
139
- '-',
140
- ]);
141
- return stdout;
142
- }
143
-
144
- export async function setSessionOption(name: string, option: string, value: string): Promise<void> {
145
- await execa('tmux', ['set-option', '-t', name, option, value]);
146
- }
147
-
148
- export async function setPaneOption(target: string, option: string, value: string): Promise<void> {
149
- await execa('tmux', ['set-option', '-p', '-t', target, option, value]);
150
- }
151
-
152
- function parseArrayOptionValue(line: string, option: string): string {
153
- return line.replace(new RegExp(`^${option}\\[\\d+\\]\\s+`, 'u'), '');
154
- }
155
-
156
- async function ensureSessionTerminalFeatures(
157
- name: string,
158
- features: string[],
159
- ): Promise<void> {
160
- const { stdout } = await execa('tmux', ['show-options', '-t', name, 'terminal-features']);
161
- const configured = new Set(
162
- stdout
163
- .split('\n')
164
- .map((line) => { return line.trim(); })
165
- .filter((line) => { return line.length > 0; })
166
- .map((line) => { return parseArrayOptionValue(line, 'terminal-features'); }),
167
- );
168
-
169
- for (const feature of features) {
170
- if (configured.has(feature)) {
171
- continue;
172
- }
173
- await execa('tmux', ['set-option', '-as', '-t', name, 'terminal-features', `,${feature}`]);
174
- }
175
- }
176
-
177
- async function getRootBindingLine(key: string): Promise<string | null> {
178
- const { stdout } = await execa('tmux', ['list-keys', '-T', 'root']);
179
- const line = stdout
180
- .split('\n')
181
- .map((entry) => { return entry.trim(); })
182
- .find((entry) => { return entry.startsWith(`bind-key -T root ${key} `); });
183
- return line ?? null;
184
- }
185
-
186
- async function ensureRootKeyBinding(
187
- key: string,
188
- nfoCommand: string,
189
- ): Promise<void> {
190
- const existing = await getRootBindingLine(key);
191
- if (existing && !existing.includes(NFO_SESSION_MATCH)) {
192
- // Respect user-defined bindings that are not NFO-managed.
193
- return;
194
- }
195
- await execa('tmux', [
196
- 'bind-key',
197
- '-n',
198
- key,
199
- 'if-shell',
200
- '-F',
201
- NFO_SESSION_MATCH,
202
- nfoCommand,
203
- `send-keys ${key}`,
204
- ]);
205
- }
206
-
207
- export async function ensureNfoNavigationBindings(): Promise<void> {
208
- await ensureRootKeyBinding('F6', `select-window -t :${DASHBOARD_WINDOW_NAME}`);
209
- await ensureRootKeyBinding('F7', 'select-window -t :0');
210
- }
211
-
212
- export async function ensureNfoSessionUi(name: string): Promise<void> {
213
- await setSessionOption(name, 'mouse', 'on');
214
- await setSessionOption(name, 'status', 'off');
215
- await setSessionOption(name, 'extended-keys', 'on');
216
- await ensureSessionTerminalFeatures(name, REQUIRED_TERMINAL_FEATURES);
217
- await ensureNfoNavigationBindings();
218
- }
219
-
220
- export async function selectWindow(name: string, windowTarget: string): Promise<void> {
221
- await execa('tmux', ['select-window', '-t', `${name}:${windowTarget}`]);
222
- }
223
-
224
- export async function selectPane(target: string): Promise<void> {
225
- await execa('tmux', ['select-pane', '-t', target]);
226
- }
@@ -1,16 +0,0 @@
1
- const MAX_LEN = 60;
2
-
3
- export function extractActivityLine(paneText: string): string {
4
- const lines = paneText.split('\n');
5
- let last = '';
6
- for (const line of lines) {
7
- const trimmed = line.trim();
8
- if (trimmed.length > 0) {
9
- last = trimmed;
10
- }
11
- }
12
- if (last.length > MAX_LEN) {
13
- return last.slice(0, MAX_LEN - 1) + '…';
14
- }
15
- return last;
16
- }