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
package/src/cli.ts DELETED
@@ -1,197 +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 {
6
- listOrchestras,
7
- formatOrchestraList,
8
- type OrchestraSummary,
9
- } from "./commands/list.js";
10
- import {
11
- isPermissionLevel,
12
- DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE,
13
- DANGEROUSLY_SKIP_PERMISSIONS_WARNING,
14
- type PermissionLevel,
15
- } from "./permission.js";
16
- import { detectClaude } from "./claude-detect.js";
17
- import { createInterface } from "node:readline/promises";
18
- import packageJson from "../package.json" with { type: "json" };
19
-
20
- const program = new Command();
21
- program
22
- .name("nfo")
23
- .description("NoFluffOrchestra — TUI multi-agent orchestrator")
24
- .version(packageJson.version);
25
-
26
- program
27
- .argument("[id]", "Orchestra id to attach (optional)")
28
- .option(
29
- "--notify-on-permission",
30
- "bell + desktop notify when a musician awaits permission",
31
- )
32
- .action(
33
- async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
34
- await detectClaude();
35
- try {
36
- if (id) {
37
- await attachOrRestore(id);
38
- return;
39
- }
40
- const decision = await decideAction(process.cwd());
41
- switch (decision.kind) {
42
- case "create": {
43
- const level = await promptPermissionLevel();
44
- await createOrchestra({
45
- repoRoot: decision.repoRoot,
46
- orchestraId: decision.orchestraId,
47
- permissionLevel: level,
48
- notifyOnPermission: opts.notifyOnPermission,
49
- });
50
- return;
51
- }
52
- case "attach_existing":
53
- await attachOrRestore(decision.orchestraId);
54
- return;
55
- case "pick": {
56
- const picked = await promptOrchestraPicker(decision.summaries);
57
- await attachOrRestore(picked);
58
- return;
59
- }
60
- case "error":
61
- console.error(decision.message);
62
- process.exit(1);
63
- }
64
- } catch (err) {
65
- console.error(err instanceof Error ? err.message : String(err));
66
- process.exit(1);
67
- }
68
- },
69
- );
70
-
71
- program
72
- .command("list")
73
- .description("List all known orchestras")
74
- .action(async () => {
75
- const summaries = await listOrchestras();
76
- console.log(formatOrchestraList(summaries));
77
- });
78
-
79
- program
80
- .command("restore <id>")
81
- .description("Force-restore a stopped orchestra")
82
- .option(
83
- "--notify-on-permission",
84
- "bell + desktop notify when a musician awaits permission",
85
- )
86
- .action(async (id: string, opts: { notifyOnPermission?: boolean }) => {
87
- const { restoreOrchestra } = await import("./commands/restore.js");
88
- await restoreOrchestra(id, undefined, opts.notifyOnPermission);
89
- });
90
-
91
- program
92
- .command("kill <id>")
93
- .description("Tear down an orchestra (state archived, notes preserved)")
94
- .option("-y, --yes", "Skip confirmation prompt")
95
- .action(async (id: string, opts: { yes?: boolean }) => {
96
- const { killOrchestra } = await import("./commands/kill.js");
97
- await killOrchestra(id, opts);
98
- });
99
-
100
- program
101
- .command("notes <id>")
102
- .description("Open the orchestra's notes/ directory in $EDITOR")
103
- .action(async (id: string) => {
104
- const { openNotes } = await import("./commands/notes.js");
105
- await openNotes(id);
106
- });
107
-
108
- program
109
- .command("mcp-server", { hidden: true })
110
- .description("(internal) Run the NFO MCP server attached to an orchestra")
111
- .requiredOption("--orchestra-id <id>", "Orchestra id")
112
- .option("--caller-musician-id <id>", "When the server is hosting a Musician")
113
- .action(async (opts: { orchestraId: string; callerMusicianId?: string }) => {
114
- const { runMcpServerCli } = await import("./commands/mcp-server.js");
115
- await runMcpServerCli({
116
- orchestraId: opts.orchestraId,
117
- callerMusicianId: opts.callerMusicianId,
118
- });
119
- });
120
-
121
- program
122
- .command("tui", { hidden: true })
123
- .description("(internal) Run the NFO Ink TUI for an orchestra")
124
- .requiredOption("--orchestra-id <id>", "Orchestra id")
125
- .action(async (opts: { orchestraId: string }) => {
126
- const { runTui } = await import("./commands/tui.js");
127
- await runTui({
128
- orchestraId: opts.orchestraId,
129
- version: packageJson.version,
130
- });
131
- });
132
-
133
- program.parseAsync(process.argv);
134
-
135
- async function promptPermissionLevel(): Promise<PermissionLevel> {
136
- const rl = createInterface({ input: process.stdin, output: process.stdout });
137
- try {
138
- const ans = (
139
- await rl.question(
140
- `Permission level for this orchestra:
141
- 1) Dangerously skip permissions — RISKY: bypasses all permission checks
142
- 2) auto — Claude auto mode (prompts only on risky actions)
143
- 3) edits — auto-accept edits, prompt on shell/tools
144
- 4) supervised — claude's default prompt-on-risky behavior
145
- 5) strict — read-only / plan mode
146
- Choose [1-5] (default 4): `,
147
- )
148
- ).trim();
149
-
150
- const map: Record<string, PermissionLevel> = {
151
- "1": "dangerouslySkipPermissions",
152
- "2": "auto",
153
- "3": "acceptEdits",
154
- "4": "supervised",
155
- "5": "strict",
156
- "": "supervised",
157
- };
158
- const level = map[ans];
159
- if (!level || !isPermissionLevel(level)) {
160
- throw new Error(`Invalid choice: ${ans}`);
161
- }
162
-
163
- if (level === "dangerouslySkipPermissions") {
164
- console.log("\n" + DANGEROUSLY_SKIP_PERMISSIONS_WARNING + "\n");
165
- const confirm = (await rl.question("> ")).trim();
166
- if (confirm !== DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE) {
167
- throw new Error("Auto mode not confirmed. Aborting.");
168
- }
169
- }
170
-
171
- return level;
172
- } finally {
173
- rl.close();
174
- }
175
- }
176
-
177
- async function promptOrchestraPicker(
178
- summaries: OrchestraSummary[],
179
- ): Promise<string> {
180
- const rl = createInterface({ input: process.stdin, output: process.stdout });
181
- try {
182
- console.log("Multiple orchestras found:");
183
- summaries.forEach((s, i) => {
184
- console.log(
185
- ` ${i + 1}) ${s.running ? "●" : "○"} ${s.id} (${s.project_path})`,
186
- );
187
- });
188
- const choice = (await rl.question("Pick one [1-N]: ")).trim();
189
- const idx = Number(choice) - 1;
190
- if (Number.isNaN(idx) || idx < 0 || idx >= summaries.length) {
191
- throw new Error("Invalid choice");
192
- }
193
- return summaries[idx].id;
194
- } finally {
195
- rl.close();
196
- }
197
- }
@@ -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
- }