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.
- package/dist/claude-command.js +6 -1
- package/dist/claude-command.js.map +1 -1
- package/dist/claude-trust.js +46 -0
- package/dist/claude-trust.js.map +1 -0
- package/dist/cli.js +5 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/attach.js +8 -8
- package/dist/commands/attach.js.map +1 -1
- package/dist/commands/launch.js +3 -6
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/restore.js +6 -10
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/tui.js +17 -1
- package/dist/commands/tui.js.map +1 -1
- package/dist/mcp/handlers.js +5 -0
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/tool-defs.js +10 -0
- package/dist/mcp/tool-defs.js.map +1 -1
- package/dist/musicians/dismiss.js +1 -1
- package/dist/musicians/dismiss.js.map +1 -1
- package/dist/musicians/reconcile.js +27 -0
- package/dist/musicians/reconcile.js.map +1 -0
- package/dist/musicians/roles.js +15 -0
- package/dist/musicians/roles.js.map +1 -0
- package/dist/musicians/spawn.js +53 -18
- package/dist/musicians/spawn.js.map +1 -1
- package/dist/permission.js +6 -0
- package/dist/permission.js.map +1 -1
- package/dist/prompts/musician-role.js +2 -1
- package/dist/prompts/musician-role.js.map +1 -1
- package/dist/prompts/orchestrator-role.js +18 -6
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/prompts/tool-discipline.js +7 -3
- package/dist/prompts/tool-discipline.js.map +1 -1
- package/dist/tmux.js +23 -0
- package/dist/tmux.js.map +1 -1
- package/dist/tui/components/App.js +8 -12
- package/dist/tui/components/App.js.map +1 -1
- package/dist/tui/components/Help.js +1 -1
- package/dist/tui/components/Help.js.map +1 -1
- package/dist/tui/keymap.js +1 -1
- package/dist/tui/keymap.js.map +1 -1
- package/package.json +18 -8
- package/assets/agent-screen.png +0 -0
- package/assets/main-screen.png +0 -0
- package/assets/orche-clawd.png +0 -0
- package/dist/tui/App.js +0 -428
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/AppView.js +0 -13
- package/dist/tui/AppView.js.map +0 -1
- package/dist/tui/Auditorium.js +0 -17
- package/dist/tui/Auditorium.js.map +0 -1
- package/dist/tui/ConcertHall.js +0 -11
- package/dist/tui/ConcertHall.js.map +0 -1
- package/dist/tui/Help.js +0 -49
- package/dist/tui/Help.js.map +0 -1
- package/dist/tui/OrchestratorPane.js +0 -34
- package/dist/tui/OrchestratorPane.js.map +0 -1
- package/dist/tui/SidebarHeader.js +0 -6
- package/dist/tui/SidebarHeader.js.map +0 -1
- package/dist/tui/StatusBar.js +0 -6
- package/dist/tui/StatusBar.js.map +0 -1
- package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
- package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
- package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
- package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
- package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
- package/docs/specs/2026-05-29-nfo-design.md +0 -468
- package/plan-explorer-musician-hardening.md +0 -56
- package/src/claude-command.ts +0 -35
- package/src/claude-detect.ts +0 -42
- package/src/cli.ts +0 -197
- package/src/commands/attach.ts +0 -24
- package/src/commands/dashboard-window.ts +0 -33
- package/src/commands/kill.ts +0 -50
- package/src/commands/launch.ts +0 -134
- package/src/commands/list.ts +0 -43
- package/src/commands/mcp-server.ts +0 -18
- package/src/commands/notes.ts +0 -18
- package/src/commands/restore.ts +0 -153
- package/src/commands/tui.tsx +0 -22
- package/src/config.ts +0 -44
- package/src/dashboard.ts +0 -1
- package/src/mcp/config.ts +0 -39
- package/src/mcp/handlers.ts +0 -141
- package/src/mcp/server.ts +0 -50
- package/src/mcp/tool-defs.ts +0 -151
- package/src/musicians/dismiss.ts +0 -60
- package/src/musicians/ids.ts +0 -21
- package/src/musicians/lookup.ts +0 -13
- package/src/musicians/message-log.ts +0 -152
- package/src/musicians/message.ts +0 -99
- package/src/musicians/query.ts +0 -19
- package/src/musicians/spawn.ts +0 -139
- package/src/notes.ts +0 -39
- package/src/notify.ts +0 -62
- package/src/orchestrator/report-back.ts +0 -33
- package/src/permission.ts +0 -30
- package/src/project-key.ts +0 -12
- package/src/prompts/musician-role.ts +0 -22
- package/src/prompts/orchestrator-role.ts +0 -84
- package/src/prompts/tool-discipline.ts +0 -41
- package/src/repo.ts +0 -14
- package/src/shell-quote.ts +0 -7
- package/src/state-updaters.ts +0 -132
- package/src/state.ts +0 -49
- package/src/state.types.ts +0 -67
- package/src/tmux.ts +0 -226
- package/src/tui/activity-line.ts +0 -16
- package/src/tui/components/App.tsx +0 -534
- package/src/tui/components/AppView.tsx +0 -98
- package/src/tui/components/Auditorium.tsx +0 -56
- package/src/tui/components/ConcertHall.tsx +0 -31
- package/src/tui/components/Help.tsx +0 -63
- package/src/tui/components/OrchestratorPane.tsx +0 -98
- package/src/tui/components/SidebarHeader.tsx +0 -34
- package/src/tui/components/StatusBar.tsx +0 -42
- package/src/tui/detect-permission.ts +0 -93
- package/src/tui/embedded-session-lifecycle.ts +0 -44
- package/src/tui/embedded-terminal.ts +0 -325
- package/src/tui/format-time.ts +0 -25
- package/src/tui/keymap.ts +0 -104
- package/src/tui/poll-activity.ts +0 -25
- package/src/tui/poll-idle.ts +0 -149
- package/src/tui/poll-permission.ts +0 -50
- package/src/tui/status-icon.ts +0 -35
- package/src/tui/terminal-input.ts +0 -136
- package/src/tui/watch-state.ts +0 -43
- package/src/worktree.ts +0 -41
- package/tests/claude-command.test.ts +0 -30
- package/tests/claude-detect.test.ts +0 -14
- package/tests/commands/attach.test.ts +0 -60
- package/tests/commands/kill.test.ts +0 -66
- package/tests/commands/launch.test.ts +0 -75
- package/tests/commands/list.test.ts +0 -47
- package/tests/commands/notes.test.ts +0 -53
- package/tests/commands/restore.test.ts +0 -126
- package/tests/helpers/tmp-config.ts +0 -16
- package/tests/helpers/tmp-repo.ts +0 -29
- package/tests/integration/orchestrator-spawn.test.ts +0 -108
- package/tests/mcp/handlers.test.ts +0 -163
- package/tests/mcp/tool-defs.test.ts +0 -35
- package/tests/musicians/dismiss.test.ts +0 -102
- package/tests/musicians/message.test.ts +0 -159
- package/tests/musicians/query.test.ts +0 -65
- package/tests/musicians/spawn.test.ts +0 -125
- package/tests/notes.test.ts +0 -56
- package/tests/notify.test.ts +0 -80
- package/tests/orchestrator/report-back.test.ts +0 -18
- package/tests/permission.test.ts +0 -39
- package/tests/project-key.test.ts +0 -33
- package/tests/prompts/tool-discipline.test.ts +0 -25
- package/tests/repo.test.ts +0 -38
- package/tests/state-updaters.test.ts +0 -126
- package/tests/state.test.ts +0 -85
- package/tests/tmux.test.ts +0 -126
- package/tests/tui/AppView.test.tsx +0 -92
- package/tests/tui/Auditorium.test.tsx +0 -67
- package/tests/tui/ConcertHall.test.tsx +0 -22
- package/tests/tui/Help.test.tsx +0 -38
- package/tests/tui/OrchestratorPane.test.ts +0 -30
- package/tests/tui/SidebarHeader.test.tsx +0 -20
- package/tests/tui/StatusBar.test.tsx +0 -51
- package/tests/tui/activity-line.test.ts +0 -21
- package/tests/tui/detect-permission.test.ts +0 -92
- package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
- package/tests/tui/embedded-terminal.test.ts +0 -80
- package/tests/tui/format-time.test.ts +0 -25
- package/tests/tui/keymap.test.ts +0 -93
- package/tests/tui/poll-activity.test.ts +0 -81
- package/tests/tui/poll-idle.test.ts +0 -159
- package/tests/tui/poll-permission.test.ts +0 -222
- package/tests/tui/status-icon.test.ts +0 -27
- package/tests/tui/terminal-input.test.ts +0 -113
- package/tests/tui/watch-state.test.ts +0 -54
- package/tests/worktree.test.ts +0 -73
- package/tsconfig.json +0 -19
- 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
|
-
}
|
package/src/musicians/message.ts
DELETED
|
@@ -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
|
-
}
|
package/src/musicians/query.ts
DELETED
|
@@ -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
|
-
}
|
package/src/musicians/spawn.ts
DELETED
|
@@ -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.`;
|
package/src/project-key.ts
DELETED
|
@@ -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
|
-
`;
|