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.
- 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 +64 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/restore.js +0 -1
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/tui.js +6 -4
- 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/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 +14 -8
- 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 +42 -8
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/prompts/tool-discipline.js +10 -0
- package/dist/prompts/tool-discipline.js.map +1 -1
- package/dist/tui/{App.js → components/App.js} +20 -20
- package/dist/tui/components/App.js.map +1 -0
- package/dist/tui/components/AppView.js +13 -0
- package/dist/tui/components/AppView.js.map +1 -0
- package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
- package/dist/tui/components/Auditorium.js.map +1 -0
- package/dist/tui/components/ConcertHall.js.map +1 -0
- package/dist/tui/{Help.js → components/Help.js} +0 -8
- package/dist/tui/components/Help.js.map +1 -0
- package/dist/tui/components/OrchestratorPane.js.map +1 -0
- package/dist/tui/components/SidebarHeader.js +6 -0
- package/dist/tui/components/SidebarHeader.js.map +1 -0
- package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/package.json +8 -1
- 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.map +0 -1
- package/dist/tui/AppView.js +0 -13
- package/dist/tui/AppView.js.map +0 -1
- package/dist/tui/Auditorium.js.map +0 -1
- package/dist/tui/ConcertHall.js.map +0 -1
- package/dist/tui/Help.js.map +0 -1
- 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.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/src/claude-command.ts +0 -35
- package/src/claude-detect.ts +0 -42
- package/src/cli.ts +0 -164
- 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 -16
- 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 -60
- package/src/prompts/tool-discipline.ts +0 -35
- 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/App.tsx +0 -532
- package/src/tui/AppView.tsx +0 -96
- package/src/tui/Auditorium.tsx +0 -56
- package/src/tui/ConcertHall.tsx +0 -31
- package/src/tui/Help.tsx +0 -72
- package/src/tui/OrchestratorPane.tsx +0 -98
- package/src/tui/SidebarHeader.tsx +0 -32
- package/src/tui/StatusBar.tsx +0 -44
- package/src/tui/activity-line.ts +0 -16
- 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 -29
- 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
- /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
- /package/dist/tui/{OrchestratorPane.js → components/OrchestratorPane.js} +0 -0
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 = ['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.`;
|
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
|
-
`;
|
|
@@ -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
|
-
}
|