nfo-cli 0.0.4-improve-prompting → 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 (159) 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 +0 -0
  6. package/dist/mcp/handlers.js +5 -0
  7. package/dist/mcp/handlers.js.map +1 -1
  8. package/dist/mcp/tool-defs.js +10 -0
  9. package/dist/mcp/tool-defs.js.map +1 -1
  10. package/dist/musicians/dismiss.js +1 -1
  11. package/dist/musicians/dismiss.js.map +1 -1
  12. package/dist/musicians/roles.js +15 -0
  13. package/dist/musicians/roles.js.map +1 -0
  14. package/dist/musicians/spawn.js +53 -18
  15. package/dist/musicians/spawn.js.map +1 -1
  16. package/dist/permission.js +6 -0
  17. package/dist/permission.js.map +1 -1
  18. package/dist/prompts/musician-role.js +2 -1
  19. package/dist/prompts/musician-role.js.map +1 -1
  20. package/dist/prompts/orchestrator-role.js +18 -6
  21. package/dist/prompts/orchestrator-role.js.map +1 -1
  22. package/dist/prompts/tool-discipline.js +7 -3
  23. package/dist/prompts/tool-discipline.js.map +1 -1
  24. package/package.json +8 -1
  25. package/assets/agent-screen.png +0 -0
  26. package/assets/main-screen.png +0 -0
  27. package/assets/orche-clawd.png +0 -0
  28. package/dist/tui/App.js +0 -428
  29. package/dist/tui/App.js.map +0 -1
  30. package/dist/tui/AppView.js +0 -13
  31. package/dist/tui/AppView.js.map +0 -1
  32. package/dist/tui/Auditorium.js +0 -17
  33. package/dist/tui/Auditorium.js.map +0 -1
  34. package/dist/tui/ConcertHall.js +0 -11
  35. package/dist/tui/ConcertHall.js.map +0 -1
  36. package/dist/tui/Help.js +0 -49
  37. package/dist/tui/Help.js.map +0 -1
  38. package/dist/tui/OrchestratorPane.js +0 -34
  39. package/dist/tui/OrchestratorPane.js.map +0 -1
  40. package/dist/tui/SidebarHeader.js +0 -6
  41. package/dist/tui/SidebarHeader.js.map +0 -1
  42. package/dist/tui/StatusBar.js +0 -6
  43. package/dist/tui/StatusBar.js.map +0 -1
  44. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  45. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  46. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  47. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  48. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  49. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  50. package/plan-explorer-musician-hardening.md +0 -56
  51. package/src/claude-command.ts +0 -35
  52. package/src/claude-detect.ts +0 -42
  53. package/src/cli.ts +0 -197
  54. package/src/commands/attach.ts +0 -24
  55. package/src/commands/dashboard-window.ts +0 -33
  56. package/src/commands/kill.ts +0 -50
  57. package/src/commands/launch.ts +0 -134
  58. package/src/commands/list.ts +0 -43
  59. package/src/commands/mcp-server.ts +0 -18
  60. package/src/commands/notes.ts +0 -18
  61. package/src/commands/restore.ts +0 -153
  62. package/src/commands/tui.tsx +0 -22
  63. package/src/config.ts +0 -44
  64. package/src/dashboard.ts +0 -1
  65. package/src/mcp/config.ts +0 -39
  66. package/src/mcp/handlers.ts +0 -141
  67. package/src/mcp/server.ts +0 -50
  68. package/src/mcp/tool-defs.ts +0 -151
  69. package/src/musicians/dismiss.ts +0 -60
  70. package/src/musicians/ids.ts +0 -21
  71. package/src/musicians/lookup.ts +0 -13
  72. package/src/musicians/message-log.ts +0 -152
  73. package/src/musicians/message.ts +0 -99
  74. package/src/musicians/query.ts +0 -19
  75. package/src/musicians/spawn.ts +0 -139
  76. package/src/notes.ts +0 -39
  77. package/src/notify.ts +0 -62
  78. package/src/orchestrator/report-back.ts +0 -33
  79. package/src/permission.ts +0 -30
  80. package/src/project-key.ts +0 -12
  81. package/src/prompts/musician-role.ts +0 -22
  82. package/src/prompts/orchestrator-role.ts +0 -84
  83. package/src/prompts/tool-discipline.ts +0 -41
  84. package/src/repo.ts +0 -14
  85. package/src/shell-quote.ts +0 -7
  86. package/src/state-updaters.ts +0 -132
  87. package/src/state.ts +0 -49
  88. package/src/state.types.ts +0 -67
  89. package/src/tmux.ts +0 -226
  90. package/src/tui/activity-line.ts +0 -16
  91. package/src/tui/components/App.tsx +0 -534
  92. package/src/tui/components/AppView.tsx +0 -98
  93. package/src/tui/components/Auditorium.tsx +0 -56
  94. package/src/tui/components/ConcertHall.tsx +0 -31
  95. package/src/tui/components/Help.tsx +0 -63
  96. package/src/tui/components/OrchestratorPane.tsx +0 -98
  97. package/src/tui/components/SidebarHeader.tsx +0 -34
  98. package/src/tui/components/StatusBar.tsx +0 -42
  99. package/src/tui/detect-permission.ts +0 -93
  100. package/src/tui/embedded-session-lifecycle.ts +0 -44
  101. package/src/tui/embedded-terminal.ts +0 -325
  102. package/src/tui/format-time.ts +0 -25
  103. package/src/tui/keymap.ts +0 -104
  104. package/src/tui/poll-activity.ts +0 -25
  105. package/src/tui/poll-idle.ts +0 -149
  106. package/src/tui/poll-permission.ts +0 -50
  107. package/src/tui/status-icon.ts +0 -35
  108. package/src/tui/terminal-input.ts +0 -136
  109. package/src/tui/watch-state.ts +0 -43
  110. package/src/worktree.ts +0 -41
  111. package/tests/claude-command.test.ts +0 -30
  112. package/tests/claude-detect.test.ts +0 -14
  113. package/tests/commands/attach.test.ts +0 -60
  114. package/tests/commands/kill.test.ts +0 -66
  115. package/tests/commands/launch.test.ts +0 -75
  116. package/tests/commands/list.test.ts +0 -47
  117. package/tests/commands/notes.test.ts +0 -53
  118. package/tests/commands/restore.test.ts +0 -126
  119. package/tests/helpers/tmp-config.ts +0 -16
  120. package/tests/helpers/tmp-repo.ts +0 -29
  121. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  122. package/tests/mcp/handlers.test.ts +0 -163
  123. package/tests/mcp/tool-defs.test.ts +0 -35
  124. package/tests/musicians/dismiss.test.ts +0 -102
  125. package/tests/musicians/message.test.ts +0 -159
  126. package/tests/musicians/query.test.ts +0 -65
  127. package/tests/musicians/spawn.test.ts +0 -125
  128. package/tests/notes.test.ts +0 -56
  129. package/tests/notify.test.ts +0 -80
  130. package/tests/orchestrator/report-back.test.ts +0 -18
  131. package/tests/permission.test.ts +0 -39
  132. package/tests/project-key.test.ts +0 -33
  133. package/tests/prompts/tool-discipline.test.ts +0 -25
  134. package/tests/repo.test.ts +0 -38
  135. package/tests/state-updaters.test.ts +0 -126
  136. package/tests/state.test.ts +0 -85
  137. package/tests/tmux.test.ts +0 -126
  138. package/tests/tui/AppView.test.tsx +0 -92
  139. package/tests/tui/Auditorium.test.tsx +0 -67
  140. package/tests/tui/ConcertHall.test.tsx +0 -22
  141. package/tests/tui/Help.test.tsx +0 -38
  142. package/tests/tui/OrchestratorPane.test.ts +0 -30
  143. package/tests/tui/SidebarHeader.test.tsx +0 -20
  144. package/tests/tui/StatusBar.test.tsx +0 -51
  145. package/tests/tui/activity-line.test.ts +0 -21
  146. package/tests/tui/detect-permission.test.ts +0 -92
  147. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  148. package/tests/tui/embedded-terminal.test.ts +0 -80
  149. package/tests/tui/format-time.test.ts +0 -25
  150. package/tests/tui/keymap.test.ts +0 -93
  151. package/tests/tui/poll-activity.test.ts +0 -81
  152. package/tests/tui/poll-idle.test.ts +0 -159
  153. package/tests/tui/poll-permission.test.ts +0 -222
  154. package/tests/tui/status-icon.test.ts +0 -27
  155. package/tests/tui/terminal-input.test.ts +0 -113
  156. package/tests/tui/watch-state.test.ts +0 -54
  157. package/tests/worktree.test.ts +0 -73
  158. package/tsconfig.json +0 -19
  159. 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
- }