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
@@ -1,152 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { appendFile, mkdir, readFile } from 'node:fs/promises';
3
- import { existsSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { messageLogsDir } from '../config.js';
6
-
7
- export type MusicianMessageDelivery = 'immediate' | 'queued-drain';
8
-
9
- interface MusicianMessageQueuedEvent {
10
- type: 'message_queued';
11
- message_id: string;
12
- musician_id: string;
13
- message: string;
14
- created_at: string;
15
- }
16
-
17
- interface MusicianMessageDeliveredEvent {
18
- type: 'message_delivered';
19
- message_id: string;
20
- delivered_at: string;
21
- delivery: MusicianMessageDelivery;
22
- }
23
-
24
- type MusicianMessageEvent = MusicianMessageQueuedEvent | MusicianMessageDeliveredEvent;
25
-
26
- export interface PendingMusicianMessage {
27
- messageId: string;
28
- musicianId: string;
29
- message: string;
30
- createdAt: string;
31
- }
32
-
33
- export interface QueueMusicianMessageResult {
34
- messageId: string;
35
- createdAt: string;
36
- }
37
-
38
- function messageLogFile(orchestraId: string, musicianId: string): string {
39
- return join(messageLogsDir(orchestraId), `${musicianId}.jsonl`);
40
- }
41
-
42
- async function appendEvent(
43
- orchestraId: string,
44
- musicianId: string,
45
- event: MusicianMessageEvent,
46
- ): Promise<void> {
47
- const dir = messageLogsDir(orchestraId);
48
- await mkdir(dir, { recursive: true });
49
- await appendFile(messageLogFile(orchestraId, musicianId), JSON.stringify(event) + '\n', 'utf8');
50
- }
51
-
52
- async function readEvents(orchestraId: string, musicianId: string): Promise<MusicianMessageEvent[]> {
53
- const file = messageLogFile(orchestraId, musicianId);
54
- if (!existsSync(file)) {
55
- return [];
56
- }
57
- const raw = await readFile(file, 'utf8');
58
- return raw
59
- .split('\n')
60
- .map((line) => { return line.trim(); })
61
- .filter((line) => { return line.length > 0; })
62
- .map((line) => { return JSON.parse(line) as MusicianMessageEvent; });
63
- }
64
-
65
- export async function queueMusicianMessage(
66
- orchestraId: string,
67
- musicianId: string,
68
- message: string,
69
- ): Promise<QueueMusicianMessageResult> {
70
- const createdAt = new Date().toISOString();
71
- const messageId = `${Date.now()}-${randomUUID()}`;
72
- await appendEvent(orchestraId, musicianId, {
73
- type: 'message_queued',
74
- message_id: messageId,
75
- musician_id: musicianId,
76
- message,
77
- created_at: createdAt,
78
- });
79
- return { messageId, createdAt };
80
- }
81
-
82
- export async function markMusicianMessageDelivered(
83
- orchestraId: string,
84
- musicianId: string,
85
- messageId: string,
86
- delivery: MusicianMessageDelivery,
87
- deliveredAt = new Date().toISOString(),
88
- ): Promise<void> {
89
- await appendEvent(orchestraId, musicianId, {
90
- type: 'message_delivered',
91
- message_id: messageId,
92
- delivered_at: deliveredAt,
93
- delivery,
94
- });
95
- }
96
-
97
- export async function listPendingMusicianMessages(
98
- orchestraId: string,
99
- musicianId: string,
100
- ): Promise<PendingMusicianMessage[]> {
101
- const events = await readEvents(orchestraId, musicianId);
102
- const pending = new Map<string, PendingMusicianMessage>();
103
-
104
- for (const event of events) {
105
- if (event.type === 'message_queued') {
106
- pending.set(event.message_id, {
107
- messageId: event.message_id,
108
- musicianId: event.musician_id,
109
- message: event.message,
110
- createdAt: event.created_at,
111
- });
112
- continue;
113
- }
114
- pending.delete(event.message_id);
115
- }
116
-
117
- return [...pending.values()].sort((left, right) => {
118
- return left.createdAt.localeCompare(right.createdAt);
119
- });
120
- }
121
-
122
- export async function countPendingMusicianMessages(
123
- orchestraId: string,
124
- musicianId: string,
125
- ): Promise<number> {
126
- const pending = await listPendingMusicianMessages(orchestraId, musicianId);
127
- return pending.length;
128
- }
129
-
130
- export function formatQueuedMusicianMessages(messages: PendingMusicianMessage[]): string {
131
- if (messages.length === 0) {
132
- return '';
133
- }
134
- if (messages.length === 1) {
135
- return messages[0].message;
136
- }
137
-
138
- const lines = [
139
- 'NFO queued follow-up messages while you were busy:',
140
- '',
141
- ];
142
-
143
- messages.forEach((message, index) => {
144
- lines.push(`${index + 1}.`);
145
- lines.push(message.message);
146
- if (index < messages.length - 1) {
147
- lines.push('');
148
- }
149
- });
150
-
151
- return lines.join('\n');
152
- }
@@ -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 = ['dangerouslySkipPermissions', 'auto', 'acceptEdits', '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 'dangerouslySkipPermissions':
11
- return ['--dangerously-skip-permissions'];
12
- case 'auto':
13
- return ['--permission-mode', 'auto'];
14
- case 'acceptEdits':
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 DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE = 'I understand';
24
-
25
- export const DANGEROUSLY_SKIP_PERMISSIONS_WARNING = `⚠ "Dangerously skip permissions" 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 "${DANGEROUSLY_SKIP_PERMISSIONS_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
- `;