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.
Files changed (173) 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 +64 -54
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/restore.js +0 -1
  8. package/dist/commands/restore.js.map +1 -1
  9. package/dist/commands/tui.js +6 -4
  10. package/dist/commands/tui.js.map +1 -1
  11. package/dist/mcp/handlers.js +5 -0
  12. package/dist/mcp/handlers.js.map +1 -1
  13. package/dist/mcp/tool-defs.js +10 -0
  14. package/dist/mcp/tool-defs.js.map +1 -1
  15. package/dist/musicians/dismiss.js +1 -1
  16. package/dist/musicians/dismiss.js.map +1 -1
  17. package/dist/musicians/roles.js +15 -0
  18. package/dist/musicians/roles.js.map +1 -0
  19. package/dist/musicians/spawn.js +53 -18
  20. package/dist/musicians/spawn.js.map +1 -1
  21. package/dist/permission.js +14 -8
  22. package/dist/permission.js.map +1 -1
  23. package/dist/prompts/musician-role.js +2 -1
  24. package/dist/prompts/musician-role.js.map +1 -1
  25. package/dist/prompts/orchestrator-role.js +42 -8
  26. package/dist/prompts/orchestrator-role.js.map +1 -1
  27. package/dist/prompts/tool-discipline.js +10 -0
  28. package/dist/prompts/tool-discipline.js.map +1 -1
  29. package/dist/tui/{App.js → components/App.js} +20 -20
  30. package/dist/tui/components/App.js.map +1 -0
  31. package/dist/tui/components/AppView.js +13 -0
  32. package/dist/tui/components/AppView.js.map +1 -0
  33. package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
  34. package/dist/tui/components/Auditorium.js.map +1 -0
  35. package/dist/tui/components/ConcertHall.js.map +1 -0
  36. package/dist/tui/{Help.js → components/Help.js} +0 -8
  37. package/dist/tui/components/Help.js.map +1 -0
  38. package/dist/tui/components/OrchestratorPane.js.map +1 -0
  39. package/dist/tui/components/SidebarHeader.js +6 -0
  40. package/dist/tui/components/SidebarHeader.js.map +1 -0
  41. package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
  42. package/dist/tui/components/StatusBar.js.map +1 -0
  43. package/package.json +8 -1
  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.map +0 -1
  48. package/dist/tui/AppView.js +0 -13
  49. package/dist/tui/AppView.js.map +0 -1
  50. package/dist/tui/Auditorium.js.map +0 -1
  51. package/dist/tui/ConcertHall.js.map +0 -1
  52. package/dist/tui/Help.js.map +0 -1
  53. package/dist/tui/OrchestratorPane.js.map +0 -1
  54. package/dist/tui/SidebarHeader.js +0 -6
  55. package/dist/tui/SidebarHeader.js.map +0 -1
  56. package/dist/tui/StatusBar.js.map +0 -1
  57. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  58. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  59. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  60. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  61. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  62. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  63. package/src/claude-command.ts +0 -35
  64. package/src/claude-detect.ts +0 -42
  65. package/src/cli.ts +0 -164
  66. package/src/commands/attach.ts +0 -24
  67. package/src/commands/dashboard-window.ts +0 -33
  68. package/src/commands/kill.ts +0 -50
  69. package/src/commands/launch.ts +0 -134
  70. package/src/commands/list.ts +0 -43
  71. package/src/commands/mcp-server.ts +0 -18
  72. package/src/commands/notes.ts +0 -18
  73. package/src/commands/restore.ts +0 -153
  74. package/src/commands/tui.tsx +0 -16
  75. package/src/config.ts +0 -44
  76. package/src/dashboard.ts +0 -1
  77. package/src/mcp/config.ts +0 -39
  78. package/src/mcp/handlers.ts +0 -141
  79. package/src/mcp/server.ts +0 -50
  80. package/src/mcp/tool-defs.ts +0 -151
  81. package/src/musicians/dismiss.ts +0 -60
  82. package/src/musicians/ids.ts +0 -21
  83. package/src/musicians/lookup.ts +0 -13
  84. package/src/musicians/message-log.ts +0 -152
  85. package/src/musicians/message.ts +0 -99
  86. package/src/musicians/query.ts +0 -19
  87. package/src/musicians/spawn.ts +0 -139
  88. package/src/notes.ts +0 -39
  89. package/src/notify.ts +0 -62
  90. package/src/orchestrator/report-back.ts +0 -33
  91. package/src/permission.ts +0 -30
  92. package/src/project-key.ts +0 -12
  93. package/src/prompts/musician-role.ts +0 -22
  94. package/src/prompts/orchestrator-role.ts +0 -60
  95. package/src/prompts/tool-discipline.ts +0 -35
  96. package/src/repo.ts +0 -14
  97. package/src/shell-quote.ts +0 -7
  98. package/src/state-updaters.ts +0 -132
  99. package/src/state.ts +0 -49
  100. package/src/state.types.ts +0 -67
  101. package/src/tmux.ts +0 -226
  102. package/src/tui/App.tsx +0 -532
  103. package/src/tui/AppView.tsx +0 -96
  104. package/src/tui/Auditorium.tsx +0 -56
  105. package/src/tui/ConcertHall.tsx +0 -31
  106. package/src/tui/Help.tsx +0 -72
  107. package/src/tui/OrchestratorPane.tsx +0 -98
  108. package/src/tui/SidebarHeader.tsx +0 -32
  109. package/src/tui/StatusBar.tsx +0 -44
  110. package/src/tui/activity-line.ts +0 -16
  111. package/src/tui/detect-permission.ts +0 -93
  112. package/src/tui/embedded-session-lifecycle.ts +0 -44
  113. package/src/tui/embedded-terminal.ts +0 -325
  114. package/src/tui/format-time.ts +0 -25
  115. package/src/tui/keymap.ts +0 -104
  116. package/src/tui/poll-activity.ts +0 -25
  117. package/src/tui/poll-idle.ts +0 -149
  118. package/src/tui/poll-permission.ts +0 -50
  119. package/src/tui/status-icon.ts +0 -35
  120. package/src/tui/terminal-input.ts +0 -136
  121. package/src/tui/watch-state.ts +0 -43
  122. package/src/worktree.ts +0 -41
  123. package/tests/claude-command.test.ts +0 -30
  124. package/tests/claude-detect.test.ts +0 -14
  125. package/tests/commands/attach.test.ts +0 -60
  126. package/tests/commands/kill.test.ts +0 -66
  127. package/tests/commands/launch.test.ts +0 -75
  128. package/tests/commands/list.test.ts +0 -47
  129. package/tests/commands/notes.test.ts +0 -53
  130. package/tests/commands/restore.test.ts +0 -126
  131. package/tests/helpers/tmp-config.ts +0 -16
  132. package/tests/helpers/tmp-repo.ts +0 -29
  133. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  134. package/tests/mcp/handlers.test.ts +0 -163
  135. package/tests/mcp/tool-defs.test.ts +0 -35
  136. package/tests/musicians/dismiss.test.ts +0 -102
  137. package/tests/musicians/message.test.ts +0 -159
  138. package/tests/musicians/query.test.ts +0 -65
  139. package/tests/musicians/spawn.test.ts +0 -125
  140. package/tests/notes.test.ts +0 -56
  141. package/tests/notify.test.ts +0 -80
  142. package/tests/orchestrator/report-back.test.ts +0 -18
  143. package/tests/permission.test.ts +0 -29
  144. package/tests/project-key.test.ts +0 -33
  145. package/tests/prompts/tool-discipline.test.ts +0 -25
  146. package/tests/repo.test.ts +0 -38
  147. package/tests/state-updaters.test.ts +0 -126
  148. package/tests/state.test.ts +0 -85
  149. package/tests/tmux.test.ts +0 -126
  150. package/tests/tui/AppView.test.tsx +0 -92
  151. package/tests/tui/Auditorium.test.tsx +0 -67
  152. package/tests/tui/ConcertHall.test.tsx +0 -22
  153. package/tests/tui/Help.test.tsx +0 -38
  154. package/tests/tui/OrchestratorPane.test.ts +0 -30
  155. package/tests/tui/SidebarHeader.test.tsx +0 -20
  156. package/tests/tui/StatusBar.test.tsx +0 -51
  157. package/tests/tui/activity-line.test.ts +0 -21
  158. package/tests/tui/detect-permission.test.ts +0 -92
  159. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  160. package/tests/tui/embedded-terminal.test.ts +0 -80
  161. package/tests/tui/format-time.test.ts +0 -25
  162. package/tests/tui/keymap.test.ts +0 -93
  163. package/tests/tui/poll-activity.test.ts +0 -81
  164. package/tests/tui/poll-idle.test.ts +0 -159
  165. package/tests/tui/poll-permission.test.ts +0 -222
  166. package/tests/tui/status-icon.test.ts +0 -27
  167. package/tests/tui/terminal-input.test.ts +0 -113
  168. package/tests/tui/watch-state.test.ts +0 -54
  169. package/tests/worktree.test.ts +0 -73
  170. package/tsconfig.json +0 -19
  171. package/vitest.config.ts +0 -12
  172. /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
  173. /package/dist/tui/{OrchestratorPane.js → components/OrchestratorPane.js} +0 -0
package/src/cli.ts DELETED
@@ -1,164 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { decideAction, createOrchestra } from './commands/launch.js';
4
- import { attachOrRestore } from './commands/attach.js';
5
- import { listOrchestras, formatOrchestraList, type OrchestraSummary } from './commands/list.js';
6
- import { isPermissionLevel, AUTO_CONFIRM_PHRASE, AUTO_WARNING, type PermissionLevel } from './permission.js';
7
- import { detectClaude } from './claude-detect.js';
8
- import { createInterface } from 'node:readline/promises';
9
-
10
- const program = new Command();
11
- program
12
- .name('nfo')
13
- .description('NoFluffOrchestra — TUI multi-agent orchestrator')
14
- .version('0.0.0');
15
-
16
- program
17
- .argument('[id]', 'Orchestra id to attach (optional)')
18
- .option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
19
- .action(async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
20
- await detectClaude();
21
- try {
22
- if (id) {
23
- await attachOrRestore(id);
24
- return;
25
- }
26
- const decision = await decideAction(process.cwd());
27
- switch (decision.kind) {
28
- case 'create': {
29
- const level = await promptPermissionLevel();
30
- await createOrchestra({
31
- repoRoot: decision.repoRoot,
32
- orchestraId: decision.orchestraId,
33
- permissionLevel: level,
34
- notifyOnPermission: opts.notifyOnPermission,
35
- });
36
- return;
37
- }
38
- case 'attach_existing':
39
- await attachOrRestore(decision.orchestraId);
40
- return;
41
- case 'pick': {
42
- const picked = await promptOrchestraPicker(decision.summaries);
43
- await attachOrRestore(picked);
44
- return;
45
- }
46
- case 'error':
47
- console.error(decision.message);
48
- process.exit(1);
49
- }
50
- } catch (err) {
51
- console.error(err instanceof Error ? err.message : String(err));
52
- process.exit(1);
53
- }
54
- });
55
-
56
- program
57
- .command('list')
58
- .description('List all known orchestras')
59
- .action(async () => {
60
- const summaries = await listOrchestras();
61
- console.log(formatOrchestraList(summaries));
62
- });
63
-
64
- program
65
- .command('restore <id>')
66
- .description('Force-restore a stopped orchestra')
67
- .option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
68
- .action(async (id: string, opts: { notifyOnPermission?: boolean }) => {
69
- const { restoreOrchestra } = await import('./commands/restore.js');
70
- await restoreOrchestra(id, undefined, opts.notifyOnPermission);
71
- });
72
-
73
- program
74
- .command('kill <id>')
75
- .description('Tear down an orchestra (state archived, notes preserved)')
76
- .option('-y, --yes', 'Skip confirmation prompt')
77
- .action(async (id: string, opts: { yes?: boolean }) => {
78
- const { killOrchestra } = await import('./commands/kill.js');
79
- await killOrchestra(id, opts);
80
- });
81
-
82
- program
83
- .command('notes <id>')
84
- .description('Open the orchestra\'s notes/ directory in $EDITOR')
85
- .action(async (id: string) => {
86
- const { openNotes } = await import('./commands/notes.js');
87
- await openNotes(id);
88
- });
89
-
90
- program
91
- .command('mcp-server', { hidden: true })
92
- .description('(internal) Run the NFO MCP server attached to an orchestra')
93
- .requiredOption('--orchestra-id <id>', 'Orchestra id')
94
- .option('--caller-musician-id <id>', 'When the server is hosting a Musician')
95
- .action(async (opts: { orchestraId: string; callerMusicianId?: string }) => {
96
- const { runMcpServerCli } = await import('./commands/mcp-server.js');
97
- await runMcpServerCli({
98
- orchestraId: opts.orchestraId,
99
- callerMusicianId: opts.callerMusicianId,
100
- });
101
- });
102
-
103
- program
104
- .command('tui', { hidden: true })
105
- .description('(internal) Run the NFO Ink TUI for an orchestra')
106
- .requiredOption('--orchestra-id <id>', 'Orchestra id')
107
- .action(async (opts: { orchestraId: string }) => {
108
- const { runTui } = await import('./commands/tui.js');
109
- await runTui({ orchestraId: opts.orchestraId });
110
- });
111
-
112
- program.parseAsync(process.argv);
113
-
114
- async function promptPermissionLevel(): Promise<PermissionLevel> {
115
- const rl = createInterface({ input: process.stdin, output: process.stdout });
116
- try {
117
- const ans = (await rl.question(
118
- `Permission level for this orchestra:
119
- 1) auto — RISKY: bypasses all permission checks
120
- 2) autonomous — auto-accept edits, prompt on risky tools
121
- 3) supervised — claude's default prompt-on-risky behavior
122
- 4) strict — read-only / plan mode
123
- Choose [1-4] (default 3): `,
124
- )).trim();
125
-
126
- const map: Record<string, PermissionLevel> = {
127
- '1': 'auto', '2': 'autonomous', '3': 'supervised', '4': 'strict', '': 'supervised',
128
- };
129
- const level = map[ans];
130
- if (!level || !isPermissionLevel(level)) {
131
- throw new Error(`Invalid choice: ${ans}`);
132
- }
133
-
134
- if (level === 'auto') {
135
- console.log('\n' + AUTO_WARNING + '\n');
136
- const confirm = (await rl.question('> ')).trim();
137
- if (confirm !== AUTO_CONFIRM_PHRASE) {
138
- throw new Error('Auto mode not confirmed. Aborting.');
139
- }
140
- }
141
-
142
- return level;
143
- } finally {
144
- rl.close();
145
- }
146
- }
147
-
148
- async function promptOrchestraPicker(summaries: OrchestraSummary[]): Promise<string> {
149
- const rl = createInterface({ input: process.stdin, output: process.stdout });
150
- try {
151
- console.log('Multiple orchestras found:');
152
- summaries.forEach((s, i) => {
153
- console.log(` ${i + 1}) ${s.running ? '●' : '○'} ${s.id} (${s.project_path})`);
154
- });
155
- const choice = (await rl.question('Pick one [1-N]: ')).trim();
156
- const idx = Number(choice) - 1;
157
- if (Number.isNaN(idx) || idx < 0 || idx >= summaries.length) {
158
- throw new Error('Invalid choice');
159
- }
160
- return summaries[idx].id;
161
- } finally {
162
- rl.close();
163
- }
164
- }
@@ -1,24 +0,0 @@
1
- import type { LaunchResult } from './launch.js';
2
- import { sessionExists, sessionName, attachSession, ensureNfoSessionUi, selectWindow } from '../tmux.js';
3
- import { restoreOrchestra } from './restore.js';
4
- import { readState } from '../state.js';
5
- import { DASHBOARD_WINDOW_NAME } from '../dashboard.js';
6
- import { ensureDashboardWindow, migrateLegacySidebarPane } from './dashboard-window.js';
7
-
8
- export async function attachOrRestore(orchestraId: string, dryRun?: boolean): Promise<LaunchResult> {
9
- const state = await readState(orchestraId);
10
- if (!state) throw new Error(`Unknown orchestra: ${orchestraId}`);
11
-
12
- const name = sessionName(orchestraId);
13
- if (await sessionExists(name)) {
14
- await ensureNfoSessionUi(name);
15
- await ensureDashboardWindow(name, state.project_path, orchestraId);
16
- await migrateLegacySidebarPane(name);
17
- if (!dryRun) {
18
- await selectWindow(name, DASHBOARD_WINDOW_NAME);
19
- await attachSession(name);
20
- }
21
- return { action: 'attached', orchestraId };
22
- }
23
- return restoreOrchestra(orchestraId, dryRun);
24
- }
@@ -1,33 +0,0 @@
1
- import { execa } from 'execa';
2
- import { DASHBOARD_WINDOW_NAME } from '../dashboard.js';
3
- import { createDetachedWindow, respawnPane, setPaneOption } from '../tmux.js';
4
- import { shellQuote } from '../shell-quote.js';
5
-
6
- function tuiCommand(orchestraId: string): string {
7
- const nfoBin = process.argv[1];
8
- return `${shellQuote(nfoBin)} tui --orchestra-id ${shellQuote(orchestraId)}`;
9
- }
10
-
11
- export async function ensureDashboardWindow(
12
- session: string,
13
- cwd: string,
14
- orchestraId: string,
15
- ): Promise<void> {
16
- await removeDashboardWindow(session);
17
- const paneId = await createDetachedWindow(session, DASHBOARD_WINDOW_NAME, cwd);
18
- await setPaneOption(paneId, 'remain-on-exit', 'on');
19
- await respawnPane(paneId, tuiCommand(orchestraId));
20
- }
21
-
22
- export async function migrateLegacySidebarPane(session: string): Promise<void> {
23
- await execa('tmux', ['kill-pane', '-t', `${session}:0.1`], { reject: false });
24
- }
25
-
26
- async function removeDashboardWindow(session: string): Promise<void> {
27
- const { stdout } = await execa('tmux', ['list-windows', '-t', session, '-F', '#{window_name}']);
28
- const names = stdout.split('\n').map((line) => { return line.trim(); }).filter(Boolean);
29
- if (!names.includes(DASHBOARD_WINDOW_NAME)) {
30
- return;
31
- }
32
- await execa('tmux', ['kill-window', '-t', `${session}:${DASHBOARD_WINDOW_NAME}`], { reject: false });
33
- }
@@ -1,50 +0,0 @@
1
- import { createInterface } from 'node:readline/promises';
2
- import { rename, mkdir } from 'node:fs/promises';
3
- import { existsSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { readState } from '../state.js';
6
- import {
7
- sessionName,
8
- sessionExists,
9
- killSession,
10
- } from '../tmux.js';
11
- import { archiveDir, stateFile } from '../config.js';
12
-
13
- export interface KillOptions {
14
- yes?: boolean; // skip confirmation prompt
15
- }
16
-
17
- export async function killOrchestra(orchestraId: string, opts: KillOptions = {}): Promise<void> {
18
- const state = await readState(orchestraId);
19
- if (!state) throw new Error(`Unknown orchestra: ${orchestraId}`);
20
-
21
- if (!opts.yes) {
22
- const rl = createInterface({ input: process.stdin, output: process.stdout });
23
- try {
24
- const ans = (await rl.question(
25
- `Kill orchestra ${orchestraId} (${state.project_path})? [y/N] `,
26
- )).trim().toLowerCase();
27
- if (ans !== 'y' && ans !== 'yes') {
28
- console.log('Aborted.');
29
- return;
30
- }
31
- } finally {
32
- rl.close();
33
- }
34
- }
35
-
36
- // Phase 1: no musicians (and therefore no worktrees to handle).
37
- // Phase 2 will add the worktree-archive prompt.
38
-
39
- const name = sessionName(orchestraId);
40
- if (await sessionExists(name)) {
41
- await killSession(name);
42
- }
43
-
44
- // Archive state.json under archive/state-<timestamp>.json so notes/ stays intact.
45
- await mkdir(archiveDir(orchestraId), { recursive: true });
46
- const archived = join(archiveDir(orchestraId), `state-${Date.now()}.json`);
47
- if (existsSync(stateFile(orchestraId))) {
48
- await rename(stateFile(orchestraId), archived);
49
- }
50
- }
@@ -1,134 +0,0 @@
1
- import { writeFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { resolveRepoRoot } from '../repo.js';
4
- import { projectKeyFromPath } from '../project-key.js';
5
- import { ensureOrchestraDir, readState, writeState } from '../state.js';
6
- import { makeInitialState } from '../state.types.js';
7
- import {
8
- claudeFlagsForLevel,
9
- type PermissionLevel,
10
- } from '../permission.js';
11
- import {
12
- sessionName,
13
- sessionExists,
14
- createDetachedSession,
15
- attachSession,
16
- selectWindow,
17
- ensureNfoSessionUi,
18
- respawnPane,
19
- setPaneOption,
20
- } from '../tmux.js';
21
- import { ORCHESTRATOR_ROLE_PROMPT_V1 } from '../prompts/orchestrator-role.js';
22
- import { orchestraDir } from '../config.js';
23
- import { listOrchestras } from './list.js';
24
- import type { OrchestraSummary } from './list.js';
25
- import { noteRead, noteList } from '../notes.js';
26
- import { DASHBOARD_WINDOW_NAME } from '../dashboard.js';
27
- import { ensureDashboardWindow } from './dashboard-window.js';
28
- import { buildClaudeCommand } from '../claude-command.js';
29
- import { writeOrchestratorMcpConfig } from '../mcp/config.js';
30
-
31
- export interface LaunchOptions {
32
- cwd: string;
33
- interactive?: boolean; // when false, must supply permissionLevel
34
- permissionLevel?: PermissionLevel;
35
- dryRun?: boolean; // when true, do not attach
36
- }
37
-
38
- export interface LaunchResult {
39
- action: 'created' | 'attached' | 'restored';
40
- orchestraId: string;
41
- }
42
-
43
- export type LaunchDecision =
44
- | { kind: 'create'; orchestraId: string; repoRoot: string }
45
- | { kind: 'attach_existing'; orchestraId: string }
46
- | { kind: 'pick'; summaries: OrchestraSummary[] }
47
- | { kind: 'error'; message: string };
48
-
49
- export async function decideAction(cwd: string): Promise<LaunchDecision> {
50
- const repoRoot = await resolveRepoRoot(cwd);
51
-
52
- if (repoRoot) {
53
- const orchestraId = projectKeyFromPath(repoRoot);
54
- const existing = await readState(orchestraId);
55
- if (existing) {
56
- return { kind: 'attach_existing', orchestraId };
57
- }
58
- return { kind: 'create', orchestraId, repoRoot };
59
- }
60
-
61
- // Out of repo. Inspect known orchestras.
62
- const summaries = await listOrchestras();
63
- if (summaries.length === 0) {
64
- return { kind: 'error', message: 'Open NFO in a git repository to create your first orchestra.' };
65
- }
66
- const running = summaries.filter(s => s.running);
67
- if (running.length === 1) {
68
- return { kind: 'attach_existing', orchestraId: running[0].id };
69
- }
70
- return { kind: 'pick', summaries };
71
- }
72
-
73
- export interface CreateOrchestraOptions {
74
- repoRoot: string;
75
- orchestraId: string;
76
- permissionLevel: PermissionLevel;
77
- dryRun?: boolean;
78
- notifyOnPermission?: boolean;
79
- }
80
-
81
- export async function createOrchestra(opts: CreateOrchestraOptions): Promise<LaunchResult> {
82
- await ensureOrchestraDir(opts.orchestraId);
83
- const state = makeInitialState({
84
- orchestraId: opts.orchestraId,
85
- projectPath: opts.repoRoot,
86
- permissionLevel: opts.permissionLevel,
87
- notifyOnPermission: opts.notifyOnPermission,
88
- });
89
- await writeState(opts.orchestraId, state);
90
-
91
- const mcpConfigPath = await writeOrchestratorMcpConfig(opts.orchestraId);
92
-
93
- const promptFile = join(orchestraDir(opts.orchestraId), 'orchestrator-prompt.md');
94
- const notes = await loadOrchestratorNotes(opts.orchestraId);
95
- await writeFile(promptFile, ORCHESTRATOR_ROLE_PROMPT_V1 + notes, 'utf8');
96
-
97
- const name = sessionName(opts.orchestraId);
98
- await createDetachedSession(name, opts.repoRoot);
99
- await ensureNfoSessionUi(name);
100
- await setPaneOption(`${name}:0`, 'remain-on-exit', 'on');
101
-
102
- await ensureDashboardWindow(name, opts.repoRoot, opts.orchestraId);
103
-
104
- const claudeFlags = claudeFlagsForLevel(opts.permissionLevel);
105
- const claudeCmd = buildClaudeCommand({
106
- flags: claudeFlags,
107
- mcpConfigPath,
108
- promptFile,
109
- });
110
- await respawnPane(`${name}:0`, claudeCmd);
111
-
112
- if (!opts.dryRun) {
113
- await selectWindow(name, DASHBOARD_WINDOW_NAME);
114
- await attachSession(name);
115
- }
116
- return { action: 'created', orchestraId: opts.orchestraId };
117
- }
118
-
119
- export async function loadOrchestratorNotes(orchestraId: string): Promise<string> {
120
- const files = await noteList(orchestraId);
121
- const ordered = ['overview.md', 'decisions.md'].filter((f) => { return files.includes(f); });
122
- if (ordered.length === 0) {
123
- return '';
124
- }
125
- const parts: string[] = ['\n\n## Curated project notes (loaded from notes/)\n'];
126
- for (const f of ordered) {
127
- const content = await noteRead(orchestraId, f);
128
- if (content.trim().length === 0) {
129
- continue;
130
- }
131
- parts.push(`\n### ${f}\n\n${content}\n`);
132
- }
133
- return parts.join('');
134
- }
@@ -1,43 +0,0 @@
1
- import { readdir } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { getProjectsDir } from '../config.js';
4
- import { readState } from '../state.js';
5
- import { sessionExists, sessionName } from '../tmux.js';
6
-
7
- export interface OrchestraSummary {
8
- id: string;
9
- project_path: string;
10
- permission_level: string;
11
- created_at: string;
12
- running: boolean;
13
- musician_count: number;
14
- }
15
-
16
- export async function listOrchestras(): Promise<OrchestraSummary[]> {
17
- const projectsDir = getProjectsDir();
18
- if (!existsSync(projectsDir)) return [];
19
- const dirs = await readdir(projectsDir, { withFileTypes: true });
20
- const summaries: OrchestraSummary[] = [];
21
- for (const d of dirs) {
22
- if (!d.isDirectory()) continue;
23
- const state = await readState(d.name);
24
- if (!state) continue;
25
- summaries.push({
26
- id: state.orchestra_id,
27
- project_path: state.project_path,
28
- permission_level: state.permission_level,
29
- created_at: state.created_at,
30
- running: await sessionExists(sessionName(state.orchestra_id)),
31
- musician_count: state.musicians.length,
32
- });
33
- }
34
- return summaries;
35
- }
36
-
37
- export function formatOrchestraList(summaries: OrchestraSummary[]): string {
38
- if (summaries.length === 0) return 'No orchestras found.';
39
- const rows = summaries.map(s =>
40
- `${s.running ? '●' : '○'} ${s.id}\n ${s.project_path}\n level=${s.permission_level} musicians=${s.musician_count}`,
41
- );
42
- return rows.join('\n\n');
43
- }
@@ -1,18 +0,0 @@
1
- import { runServer } from '../mcp/server.js';
2
- import { readState } from '../state.js';
3
-
4
- export interface McpServerCliOptions {
5
- orchestraId: string;
6
- callerMusicianId?: string;
7
- }
8
-
9
- export async function runMcpServerCli(opts: McpServerCliOptions): Promise<void> {
10
- const state = await readState(opts.orchestraId);
11
- if (!state) {
12
- throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
13
- }
14
- await runServer({
15
- orchestraId: opts.orchestraId,
16
- callerMusicianId: opts.callerMusicianId,
17
- });
18
- }
@@ -1,18 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { execa } from 'execa';
3
- import { notesDir } from '../config.js';
4
- import { readState } from '../state.js';
5
-
6
- export async function openNotes(orchestraId: string): Promise<void> {
7
- const state = await readState(orchestraId);
8
- if (!state) throw new Error(`Unknown orchestra: ${orchestraId}`);
9
-
10
- const dir = notesDir(orchestraId);
11
- if (!existsSync(dir)) {
12
- throw new Error(`Notes directory missing for ${orchestraId}: ${dir}`);
13
- }
14
-
15
- const editor = process.env.EDITOR ?? 'vi';
16
- // Open the dir, not a specific file — the user picks which note to edit.
17
- await execa(editor, [dir], { stdio: 'inherit' });
18
- }
@@ -1,153 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { execa } from "execa";
5
- import { readState, writeState } from "../state.js";
6
- import { setMusicianTmuxWindowId } from "../state-updaters.js";
7
- import { orchestraDir } from "../config.js";
8
- import {
9
- sessionName,
10
- sessionExists,
11
- createDetachedSession,
12
- attachSession,
13
- ensureNfoSessionUi,
14
- selectWindow,
15
- respawnPane,
16
- setPaneOption,
17
- } from "../tmux.js";
18
- import { claudeFlagsForLevel } from "../permission.js";
19
- import { ORCHESTRATOR_ROLE_PROMPT_V1 } from "../prompts/orchestrator-role.js";
20
- import { MUSICIAN_ROLE_PROMPT_V1 } from "../prompts/musician-role.js";
21
- import { loadOrchestratorNotes } from "./launch.js";
22
- import type { LaunchResult } from "./launch.js";
23
- import { DASHBOARD_WINDOW_NAME } from "../dashboard.js";
24
- import {
25
- ensureDashboardWindow,
26
- migrateLegacySidebarPane,
27
- } from "./dashboard-window.js";
28
- import { buildClaudeCommand } from "../claude-command.js";
29
- import {
30
- orchestratorMcpConfigPath,
31
- writeMusicianMcpConfig,
32
- writeOrchestratorMcpConfig,
33
- } from "../mcp/config.js";
34
-
35
- function sanitiseName(name: string): string {
36
- const cleaned = name
37
- .toLowerCase()
38
- .replace(/[^a-z0-9-]+/g, "-")
39
- .replace(/-+/g, "-")
40
- .replace(/^-|-$/g, "")
41
- .slice(0, 32);
42
- if (cleaned.length === 0) {
43
- return "musician";
44
- }
45
- return cleaned;
46
- }
47
-
48
- export async function restoreOrchestra(
49
- orchestraId: string,
50
- dryRun?: boolean,
51
- notifyOnPermission?: boolean,
52
- ): Promise<LaunchResult> {
53
- const state = await readState(orchestraId);
54
- if (!state) {
55
- throw new Error(`Unknown orchestra: ${orchestraId}`);
56
- }
57
-
58
- if (notifyOnPermission !== undefined) {
59
- state.notify_on_permission = notifyOnPermission;
60
- await writeState(orchestraId, state);
61
- }
62
-
63
- const name = sessionName(orchestraId);
64
- if (await sessionExists(name)) {
65
- await ensureNfoSessionUi(name);
66
- await ensureDashboardWindow(name, state.project_path, orchestraId);
67
- await migrateLegacySidebarPane(name);
68
- if (!dryRun) {
69
- await selectWindow(name, DASHBOARD_WINDOW_NAME);
70
- await attachSession(name);
71
- }
72
- return { action: "attached", orchestraId };
73
- }
74
-
75
- await createDetachedSession(name, state.project_path);
76
- await ensureNfoSessionUi(name);
77
- await ensureDashboardWindow(name, state.project_path, orchestraId);
78
- await setPaneOption(`${name}:0`, "remain-on-exit", "on");
79
-
80
- const mcpConfigPath = existsSync(orchestratorMcpConfigPath(orchestraId))
81
- ? orchestratorMcpConfigPath(orchestraId)
82
- : await writeOrchestratorMcpConfig(orchestraId);
83
- const flags = claudeFlagsForLevel(state.permission_level);
84
-
85
- // Rebuild the Orchestrator's prompt file with current notes content.
86
- const promptFile = join(orchestraDir(orchestraId), "orchestrator-prompt.md");
87
- if (existsSync(promptFile)) {
88
- const notes = await loadOrchestratorNotes(orchestraId);
89
- await writeFile(promptFile, ORCHESTRATOR_ROLE_PROMPT_V1 + notes, "utf8");
90
- }
91
-
92
- await respawnPane(
93
- `${name}:0`,
94
- buildClaudeCommand({
95
- flags,
96
- resumeSessionId: state.orchestrator_session_id,
97
- mcpConfigPath,
98
- promptFile: existsSync(promptFile) ? promptFile : undefined,
99
- }),
100
- );
101
-
102
- // Restore musicians (Phase 2). Stopped musicians are not restored.
103
- for (const musician of state.musicians) {
104
- if (musician.status === "stopped") {
105
- continue;
106
- }
107
- const workingDir = musician.worktree_path ?? state.project_path;
108
- const winLabel = `mus-${musician.id}-${sanitiseName(musician.name)}`;
109
- const created = await execa("tmux", [
110
- "new-window",
111
- "-t",
112
- name,
113
- "-n",
114
- winLabel,
115
- "-c",
116
- workingDir,
117
- "-d",
118
- "-P",
119
- "-F",
120
- "#{window_id}",
121
- ]);
122
- const newWindowId = created.stdout.trim();
123
- // The recreated window has a new id; persist it so message/query target it.
124
- await setMusicianTmuxWindowId(orchestraId, musician.id, newWindowId);
125
-
126
- const musicianPromptFile = join(
127
- orchestraDir(orchestraId),
128
- `musician-${musician.id}-prompt.md`,
129
- );
130
- await writeFile(musicianPromptFile, MUSICIAN_ROLE_PROMPT_V1, "utf8");
131
- const musicianMcpConfigPath = await writeMusicianMcpConfig(
132
- orchestraId,
133
- musician.id,
134
- );
135
- await setPaneOption(`${name}:${newWindowId}`, "remain-on-exit", "on");
136
- await respawnPane(
137
- `${name}:${newWindowId}`,
138
- buildClaudeCommand({
139
- flags,
140
- resumeSessionId: musician.claude_session_id,
141
- mcpConfigPath: musicianMcpConfigPath,
142
- promptFile: musicianPromptFile,
143
- model: musician.model ?? "sonnet",
144
- }),
145
- );
146
- }
147
-
148
- if (!dryRun) {
149
- await selectWindow(name, DASHBOARD_WINDOW_NAME);
150
- await attachSession(name);
151
- }
152
- return { action: "restored", orchestraId };
153
- }
@@ -1,16 +0,0 @@
1
- import { render } from 'ink';
2
- import { App } from '../tui/App.js';
3
- import { readState } from '../state.js';
4
-
5
- export interface RunTuiOptions {
6
- orchestraId: string;
7
- }
8
-
9
- export async function runTui(opts: RunTuiOptions): Promise<void> {
10
- const state = await readState(opts.orchestraId);
11
- if (!state) {
12
- throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
13
- }
14
- const instance = render(<App orchestraId={opts.orchestraId} />, { exitOnCtrlC: false });
15
- await instance.waitUntilExit();
16
- }