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
@@ -1,99 +0,0 @@
1
- import { sendKeys, sessionName } from '../tmux.js';
2
- import { readState } from '../state.js';
3
- import { findMusicianStrict } from './lookup.js';
4
- import {
5
- setMusicianStatus,
6
- touchMusicianActivity,
7
- } from '../state-updaters.js';
8
- import {
9
- countPendingMusicianMessages,
10
- formatQueuedMusicianMessages,
11
- listPendingMusicianMessages,
12
- markMusicianMessageDelivered,
13
- queueMusicianMessage,
14
- type MusicianMessageDelivery,
15
- } from './message-log.js';
16
-
17
- export interface MessageMusicianOptions {
18
- orchestraId: string;
19
- musicianId: string;
20
- message: string;
21
- }
22
-
23
- export interface MessageMusicianResult {
24
- ok: true;
25
- delivery: 'immediate' | 'queued';
26
- message_id: string;
27
- pending_messages: number;
28
- }
29
-
30
- async function deliverPendingMessages(
31
- orchestraId: string,
32
- musicianId: string,
33
- delivery: MusicianMessageDelivery,
34
- ): Promise<number> {
35
- const state = await readState(orchestraId);
36
- if (!state) {
37
- throw new Error(`Unknown orchestra: ${orchestraId}`);
38
- }
39
- const musician = findMusicianStrict(state, musicianId);
40
- const pending = await listPendingMusicianMessages(orchestraId, musicianId);
41
- if (pending.length === 0) {
42
- return 0;
43
- }
44
-
45
- const target = `${sessionName(orchestraId)}:${musician.tmux_window_id}`;
46
- const message = formatQueuedMusicianMessages(pending);
47
- await sendKeys(target, message, true);
48
-
49
- const deliveredAt = new Date().toISOString();
50
- for (const pendingMessage of pending) {
51
- await markMusicianMessageDelivered(
52
- orchestraId,
53
- musicianId,
54
- pendingMessage.messageId,
55
- delivery,
56
- deliveredAt,
57
- );
58
- }
59
- await touchMusicianActivity(orchestraId, musicianId, deliveredAt);
60
- return pending.length;
61
- }
62
-
63
- export async function messageMusician(opts: MessageMusicianOptions): Promise<MessageMusicianResult> {
64
- const state = await readState(opts.orchestraId);
65
- if (!state) {
66
- throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
67
- }
68
- const musician = findMusicianStrict(state, opts.musicianId);
69
- const queued = await queueMusicianMessage(opts.orchestraId, opts.musicianId, opts.message);
70
-
71
- if (musician.status === 'idle') {
72
- const delivery: MusicianMessageDelivery = (await countPendingMusicianMessages(
73
- opts.orchestraId,
74
- opts.musicianId,
75
- )) === 1 ? 'immediate' : 'queued-drain';
76
- await deliverPendingMessages(opts.orchestraId, opts.musicianId, delivery);
77
- await setMusicianStatus(opts.orchestraId, opts.musicianId, 'working');
78
- return {
79
- ok: true,
80
- delivery: 'immediate',
81
- message_id: queued.messageId,
82
- pending_messages: 0,
83
- };
84
- }
85
-
86
- return {
87
- ok: true,
88
- delivery: 'queued',
89
- message_id: queued.messageId,
90
- pending_messages: await countPendingMusicianMessages(opts.orchestraId, opts.musicianId),
91
- };
92
- }
93
-
94
- export async function drainQueuedMusicianMessages(
95
- orchestraId: string,
96
- musicianId: string,
97
- ): Promise<number> {
98
- return deliverPendingMessages(orchestraId, musicianId, 'queued-drain');
99
- }
@@ -1,19 +0,0 @@
1
- import { capturePane, sessionName } from '../tmux.js';
2
- import { readState } from '../state.js';
3
- import { findMusicianStrict } from './lookup.js';
4
-
5
- export interface QueryMusicianOptions {
6
- orchestraId: string;
7
- musicianId: string;
8
- lines?: number;
9
- }
10
-
11
- export async function queryMusician(opts: QueryMusicianOptions): Promise<string> {
12
- const state = await readState(opts.orchestraId);
13
- if (!state) {
14
- throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
15
- }
16
- const musician = findMusicianStrict(state, opts.musicianId);
17
- const target = `${sessionName(opts.orchestraId)}:${musician.tmux_window_id}`;
18
- return capturePane(target, opts.lines ?? 80);
19
- }
@@ -1,139 +0,0 @@
1
- import { writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { execa } from "execa";
4
- import { addMusician } from "../state-updaters.js";
5
- import { readState } from "../state.js";
6
- import { orchestraDir, worktreesDir } from "../config.js";
7
- import { addWorktree } from "../worktree.js";
8
- import { claudeFlagsForLevel } from "../permission.js";
9
- import { respawnPane, sessionName, setPaneOption } from "../tmux.js";
10
- import { MUSICIAN_ROLE_PROMPT_V1 } from "../prompts/musician-role.js";
11
- import { buildMusicianInitialPrompt } from "../prompts/tool-discipline.js";
12
- import { nextMusicianId } from "./ids.js";
13
- import { buildClaudeCommand } from "../claude-command.js";
14
- import { writeMusicianMcpConfig } from "../mcp/config.js";
15
-
16
- export interface CreateMusicianOptions {
17
- orchestraId: string;
18
- name: string;
19
- task: string;
20
- worktree?: boolean; // default true
21
- branchFrom?: string; // default HEAD
22
- model: "sonnet" | "haiku";
23
- dryRun?: boolean; // skip launching claude; useful for tests
24
- }
25
-
26
- export type CreateMusicianResult = {
27
- musician_id: string;
28
- worktree_path: string | null;
29
- branch: string | null;
30
- tmux_window_id: string;
31
- };
32
-
33
- export async function createMusician(
34
- opts: CreateMusicianOptions,
35
- ): Promise<CreateMusicianResult> {
36
- const state = await readState(opts.orchestraId);
37
- if (!state) {
38
- throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
39
- }
40
-
41
- const musicianId = nextMusicianId(state);
42
- const useWorktree = opts.worktree !== false;
43
-
44
- let workingDir: string;
45
- let worktreePath: string | null = null;
46
- let branch: string | null = null;
47
- if (useWorktree) {
48
- worktreePath = join(worktreesDir(opts.orchestraId), musicianId);
49
- branch = `nfo/${musicianId}`;
50
- await addWorktree({
51
- repoRoot: state.project_path,
52
- path: worktreePath,
53
- branch,
54
- baseRef: opts.branchFrom,
55
- });
56
- workingDir = worktreePath;
57
- } else {
58
- workingDir = state.project_path;
59
- }
60
-
61
- const promptFile = join(
62
- orchestraDir(opts.orchestraId),
63
- `musician-${musicianId}-prompt.md`,
64
- );
65
- await writeFile(promptFile, MUSICIAN_ROLE_PROMPT_V1, "utf8");
66
-
67
- const session = sessionName(opts.orchestraId);
68
- const winLabel = `mus-${musicianId}-${sanitiseName(opts.name)}`;
69
- const { stdout: tmuxWindowId } = await execa("tmux", [
70
- "new-window",
71
- "-t",
72
- session,
73
- "-n",
74
- winLabel,
75
- "-c",
76
- workingDir,
77
- "-d",
78
- "-P",
79
- "-F",
80
- "#{window_id}",
81
- ]);
82
-
83
- if (!opts.dryRun) {
84
- const mcpConfigPath = await writeMusicianMcpConfig(
85
- opts.orchestraId,
86
- musicianId,
87
- );
88
- const flags = claudeFlagsForLevel(state.permission_level);
89
- const cmd = buildClaudeCommand({
90
- flags,
91
- mcpConfigPath,
92
- promptFile,
93
- prompt: buildMusicianInitialPrompt(opts.task),
94
- model: opts.model,
95
- });
96
- await setPaneOption(
97
- `${session}:${tmuxWindowId.trim()}`,
98
- "remain-on-exit",
99
- "on",
100
- );
101
- await respawnPane(`${session}:${tmuxWindowId.trim()}`, cmd);
102
- } else {
103
- await writeMusicianMcpConfig(opts.orchestraId, musicianId);
104
- }
105
-
106
- const now = new Date().toISOString();
107
- await addMusician(opts.orchestraId, {
108
- id: musicianId,
109
- name: opts.name,
110
- task_summary: opts.task.slice(0, 200),
111
- status: "working",
112
- pending_permission: null,
113
- tmux_window_id: tmuxWindowId.trim(),
114
- claude_session_id: null,
115
- worktree_path: worktreePath,
116
- branch,
117
- spawned_at: now,
118
- last_activity: now,
119
- model: opts.model,
120
- });
121
-
122
- return {
123
- musician_id: musicianId,
124
- worktree_path: worktreePath,
125
- branch,
126
- tmux_window_id: tmuxWindowId.trim(),
127
- };
128
- }
129
-
130
- function sanitiseName(name: string): string {
131
- return (
132
- name
133
- .toLowerCase()
134
- .replace(/[^a-z0-9-]+/g, "-")
135
- .replace(/-+/g, "-")
136
- .replace(/^-|-$/g, "")
137
- .slice(0, 32) || "musician"
138
- );
139
- }
package/src/notes.ts DELETED
@@ -1,39 +0,0 @@
1
- import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { notesDir } from './config.js';
5
-
6
- function ensureSafeFilename(filename: string): void {
7
- if (!filename || /[\/\\]/.test(filename) || filename.includes('..')) {
8
- throw new Error(`invalid filename: ${filename}`);
9
- }
10
- }
11
-
12
- export async function noteWrite(
13
- orchestraId: string,
14
- filename: string,
15
- content: string,
16
- ): Promise<void> {
17
- ensureSafeFilename(filename);
18
- const dir = notesDir(orchestraId);
19
- await mkdir(dir, { recursive: true });
20
- await writeFile(join(dir, filename), content, 'utf8');
21
- }
22
-
23
- export async function noteRead(orchestraId: string, filename: string): Promise<string> {
24
- ensureSafeFilename(filename);
25
- const file = join(notesDir(orchestraId), filename);
26
- if (!existsSync(file)) {
27
- return '';
28
- }
29
- return readFile(file, 'utf8');
30
- }
31
-
32
- export async function noteList(orchestraId: string): Promise<string[]> {
33
- const dir = notesDir(orchestraId);
34
- if (!existsSync(dir)) {
35
- return [];
36
- }
37
- const entries = await readdir(dir, { withFileTypes: true });
38
- return entries.filter((e) => { return e.isFile(); }).map((e) => { return e.name; });
39
- }
package/src/notify.ts DELETED
@@ -1,62 +0,0 @@
1
- import { execa } from 'execa';
2
-
3
- export interface NotifyOptions {
4
- pendingCount: number;
5
- platform?: NodeJS.Platform;
6
- bell?: (text: string) => void;
7
- spawn?: (bin: string, args: string[]) => Promise<unknown>;
8
- }
9
-
10
- function defaultBell(text: string): void {
11
- process.stdout.write(text);
12
- }
13
-
14
- async function defaultSpawn(bin: string, args: string[]): Promise<unknown> {
15
- return execa(bin, args);
16
- }
17
-
18
- function pluralise(count: number): string {
19
- if (count === 1) {
20
- return '1 musician awaiting permission';
21
- }
22
- return `${count} musicians awaiting permission`;
23
- }
24
-
25
- /**
26
- * Fire a single notification: ring the terminal bell and (best-effort) spawn
27
- * the platform's desktop notifier. All errors are swallowed — a missing
28
- * notify-send / osascript / etc. must not break the orchestra.
29
- */
30
- export async function notifyAwaitingPermission(opts: NotifyOptions): Promise<void> {
31
- const bell = opts.bell ?? defaultBell;
32
- const spawn = opts.spawn ?? defaultSpawn;
33
- const platform = opts.platform ?? process.platform;
34
- const message = pluralise(opts.pendingCount);
35
-
36
- try {
37
- bell('\x07');
38
- } catch {
39
- // Swallow — a broken stdout sink should never abort.
40
- }
41
-
42
- if (platform === 'linux') {
43
- try {
44
- await spawn('notify-send', ['NFO', message]);
45
- } catch {
46
- // notify-send may not be installed — best-effort only.
47
- }
48
- return;
49
- }
50
-
51
- if (platform === 'darwin') {
52
- const script = `display notification "${message}" with title "NFO"`;
53
- try {
54
- await spawn('osascript', ['-e', script]);
55
- } catch {
56
- // osascript should exist on macOS but swallow defensively.
57
- }
58
- return;
59
- }
60
-
61
- // Unknown platform (win32, freebsd, etc.) — bell-only.
62
- }
@@ -1,33 +0,0 @@
1
- import { sendKeys, sessionName } from '../tmux.js';
2
-
3
- export interface MusicianDoneReport {
4
- musicianId: string;
5
- musicianName: string;
6
- summary: string;
7
- nextSteps?: string | null;
8
- }
9
-
10
- export function formatMusicianDonePrompt(report: MusicianDoneReport): string {
11
- const nextSteps = report.nextSteps?.trim()
12
- ? `\nSuggested next steps from the Musician:\n${report.nextSteps.trim()}\n`
13
- : '';
14
-
15
- return `Musician ${report.musicianId} (${report.musicianName}) reported done and is now idle.
16
-
17
- Summary:
18
- ${report.summary}
19
- ${nextSteps}
20
- Resolve this now with an NFO tool call only:
21
- - If the work is good enough, call dismiss_musician({ musician_id: ${JSON.stringify(report.musicianId)} }).
22
- - If it needs another pass, call message_musician({ musician_id: ${JSON.stringify(report.musicianId)}, message: "..." }).
23
-
24
- Do not leave this Musician idle without either dismissing it or sending the next iteration.
25
- A plain-text acknowledgement is invalid here.`;
26
- }
27
-
28
- export async function notifyOrchestratorOfDoneReport(
29
- orchestraId: string,
30
- report: MusicianDoneReport,
31
- ): Promise<void> {
32
- await sendKeys(`${sessionName(orchestraId)}:0`, formatMusicianDonePrompt(report), true);
33
- }
package/src/permission.ts DELETED
@@ -1,30 +0,0 @@
1
- export const PERMISSION_LEVELS = ['auto', 'autonomous', 'supervised', 'strict'] as const;
2
- export type PermissionLevel = (typeof PERMISSION_LEVELS)[number];
3
-
4
- export function isPermissionLevel(s: string): s is PermissionLevel {
5
- return (PERMISSION_LEVELS as readonly string[]).includes(s);
6
- }
7
-
8
- export function claudeFlagsForLevel(level: PermissionLevel): string[] {
9
- switch (level) {
10
- case 'auto':
11
- // Spec §5.2 + §12.2 open question: exact bypass flag is `--dangerously-skip-permissions`
12
- // in current Claude Code releases. If a future release renames it, update here.
13
- return ['--dangerously-skip-permissions'];
14
- case 'autonomous':
15
- return ['--permission-mode', 'acceptEdits'];
16
- case 'supervised':
17
- return ['--permission-mode', 'default'];
18
- case 'strict':
19
- return ['--permission-mode', 'plan'];
20
- }
21
- }
22
-
23
- export const AUTO_CONFIRM_PHRASE = 'I understand';
24
-
25
- export const AUTO_WARNING = `⚠ AUTO mode disables all permission checks.
26
- Musicians can execute arbitrary shell commands, modify files anywhere on
27
- this system, and access the network without asking. Worktrees limit but
28
- do not contain risky operations. Use this only in trusted sandboxes or
29
- when you accept these risks.
30
- Type "${AUTO_CONFIRM_PHRASE}" to continue.`;
@@ -1,12 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { basename } from 'node:path';
3
-
4
- export function projectKeyFromPath(absolutePath: string): string {
5
- const hash = createHash('sha1').update(absolutePath).digest('hex').slice(0, 10);
6
- const name = basename(absolutePath)
7
- .toLowerCase()
8
- .replace(/[^a-z0-9-]+/g, '-')
9
- .replace(/-+/g, '-')
10
- .replace(/^-|-$/g, '');
11
- return `${hash}-${name || 'project'}`;
12
- }
@@ -1,22 +0,0 @@
1
- import { MUSICIAN_TOOL_DISCIPLINE } from './tool-discipline.js';
2
-
3
- export const MUSICIAN_ROLE_PROMPT_V1 = `You are a Musician in an NFO orchestra.
4
-
5
- You were spawned by the Orchestrator with a specific task. The user typing into
6
- your pane is debugging / observing — usually the user does NOT direct you;
7
- the Orchestrator does. Treat new user messages as either Orchestrator
8
- hand-offs or out-of-band human guidance, and use judgment.
9
-
10
- Your workspace is a dedicated git worktree, so file edits are isolated from
11
- other Musicians. When you finish the task you were spawned with, call the
12
- \`report_done\` MCP tool with a concise summary and optional next steps. After
13
- that, stay alive while the Orchestrator reviews your report. NFO may batch
14
- queued follow-up messages and deliver them right after \`report_done\`; if the
15
- Orchestrator is satisfied, it may dismiss you instead.
16
-
17
- ${MUSICIAN_TOOL_DISCIPLINE}
18
-
19
- You also have the full NFO MCP tool surface (\`spawn_musician\`,
20
- \`message_musician\`, etc.). Avoid spawning sub-Musicians unless the
21
- Orchestrator explicitly asks you to. Keep coordination centralised.
22
- `;
@@ -1,60 +0,0 @@
1
- import { ORCHESTRATOR_TOOL_DISCIPLINE } from "./tool-discipline.js";
2
-
3
- export const ORCHESTRATOR_ROLE_PROMPT_V1 = `You are the Orchestrator of an NFO orchestra.
4
-
5
- NFO (NoFluffOrchestra) is a TUI for multi-agent work on the user's repository.
6
- You coordinate Musicians (other Claude Code agents) via the NFO MCP tools.
7
-
8
- Available NFO tools (in addition to your normal Claude Code tools):
9
-
10
- spawn_musician({ name, task, worktree?, branch_from?, model? })
11
- Create a Musician with the given task. By default the Musician runs in a
12
- fresh git worktree off HEAD. Pass worktree=false for trivially isolated
13
- work (e.g., docs-only) that doesn't need an isolated branch. Returns the
14
- musician_id. Provide a model to be used by the Musician, otherwise it defaults to sonnet.
15
- For trivial tasks Haiku is a good choice; for complex coding work, Sonnet is better.
16
-
17
- message_musician({ musician_id, message })
18
- Send a message to a Musician. If the Musician is idle, NFO delivers it
19
- immediately. If the Musician is still working, NFO queues it and delivers
20
- it automatically on the next idle boundary.
21
-
22
- query_musician({ musician_id, lines? })
23
- Read the most recent visible output from the Musician's pane. Use this
24
- sparingly — capture-pane is heuristic and may include rendering artifacts.
25
-
26
- list_musicians()
27
- Return all currently-active Musicians with their status.
28
-
29
- dismiss_musician({ musician_id, archive_worktree? })
30
- Tear down a Musician. The worktree is archived under
31
- .../archive/<musician_id>/worktree (the branch is preserved). Pass
32
- archive_worktree=false to drop the worktree entirely. By default drop the worktree. Ask
33
- the user before archiving, as these can accumulate and consume disk space.
34
-
35
- note_write({ filename, content }) / note_read({ filename }) / note_list()
36
- Your private project memory under ~/.config/nfo/projects/<key>/notes/.
37
- On every fresh Orchestrator session, the contents of notes/overview.md
38
- and notes/decisions.md are loaded into your context automatically.
39
- Use these to record decisions, open questions, and durable project
40
- understanding the user would want you to remember next session.
41
-
42
- Coordination guidance:
43
-
44
- - ${ORCHESTRATOR_TOOL_DISCIPLINE.trim().replace(/\n/g, "\n ")}
45
- - For agent coordination, PREFER the NFO MCP tools over Claude Code's built-in
46
- Task tool. The user tracks Musician work through NFO; Task spawns are invisible
47
- to NFO.
48
- - Worktrees solve concurrent file-edit safety, not API coupling. If two
49
- Musicians' outputs need to be wired together, sequence the work, or spawn an
50
- integration Musician afterward.
51
- - The orchestra's permission level applies to every Musician you spawn.
52
- - Prefer concise follow-up nudges. NFO persists them in JSONL and batches any
53
- backlog before a Musician truly becomes idle again.
54
- - When a Musician calls \`report_done\` and no queued follow-up is waiting, NFO
55
- pushes that completion back into your Claude session. Review the report
56
- promptly and either dismiss the Musician or send the next iteration.
57
- - Project-level guidance in CLAUDE.md still applies; respect it.
58
- - You can use Superpowers if present but make sure that works are delegated to
59
- Musicians in the end if subagent driven development is picked by the user.
60
- `;
@@ -1,35 +0,0 @@
1
- export const ORCHESTRATOR_TOOL_DISCIPLINE = `Tool discipline (mandatory):
2
-
3
- - Use the NFO MCP tools for all musician coordination and orchestra-local memory.
4
- - Never merely say that you will spawn, message, query, list, dismiss, or note
5
- something later. Call the corresponding NFO tool in the same turn.
6
- - Do not use Claude Code's built-in Task tool for Musician coordination; those
7
- agents are invisible to NFO.
8
- - When a Musician reports back, resolve it in the same turn with an NFO tool
9
- call (usually \`dismiss_musician\` or \`message_musician\`). A prose-only
10
- acknowledgement is non-compliant.
11
- `;
12
-
13
- export const MUSICIAN_TOOL_DISCIPLINE = `Tool discipline (mandatory):
14
-
15
- - Use NFO MCP tools for orchestra coordination. Plain-text status reports are
16
- not a valid handoff.
17
- - When your assigned task is complete and ready for Orchestrator review, your
18
- next action must be \`report_done({ summary, next_steps? })\`.
19
- - Do not end with "done", "finished", or similar prose instead of calling
20
- \`report_done\`.
21
- - After \`report_done\`, wait for the Orchestrator to send the next task or
22
- dismiss you.
23
- `;
24
-
25
- export function buildMusicianInitialPrompt(task: string): string {
26
- const trimmedTask = task.trim();
27
- const body = trimmedTask.length > 0 ? trimmedTask : task;
28
-
29
- return `${body}
30
-
31
- NFO operating contract (mandatory):
32
- - Use the NFO MCP tools for orchestra coordination.
33
- - When you finish this task and are ready to hand it back, call \`report_done({ summary, next_steps? })\` instead of replying with a plain-text completion message.
34
- - After \`report_done\`, wait for the Orchestrator to message you again or dismiss you.`;
35
- }
package/src/repo.ts DELETED
@@ -1,14 +0,0 @@
1
- import { execa } from 'execa';
2
-
3
- export async function resolveRepoRoot(cwd: string): Promise<string | null> {
4
- try {
5
- const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
6
- cwd,
7
- reject: false,
8
- });
9
- const trimmed = stdout.trim();
10
- return trimmed.length > 0 ? trimmed : null;
11
- } catch {
12
- return null;
13
- }
14
- }
@@ -1,7 +0,0 @@
1
- export function shellQuote(value: string): string {
2
- if (value.length === 0) {
3
- return "''";
4
- }
5
-
6
- return `'${value.replace(/'/g, `'\"'\"'`)}'`;
7
- }