nfo-cli 0.0.3 → 0.0.4-improve-prompting
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/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/permission.js +8 -8
- package/dist/permission.js.map +1 -1
- package/dist/prompts/orchestrator-role.js +29 -7
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/prompts/tool-discipline.js +6 -0
- package/dist/prompts/tool-discipline.js.map +1 -1
- package/dist/tui/App.js +5 -5
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/AppView.js +1 -1
- package/dist/tui/AppView.js.map +1 -1
- package/dist/tui/SidebarHeader.js +1 -1
- package/dist/tui/SidebarHeader.js.map +1 -1
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/components/App.js +428 -0
- 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/components/Auditorium.js +17 -0
- package/dist/tui/components/Auditorium.js.map +1 -0
- package/dist/tui/components/ConcertHall.js +11 -0
- package/dist/tui/components/ConcertHall.js.map +1 -0
- package/dist/tui/components/Help.js +41 -0
- package/dist/tui/components/Help.js.map +1 -0
- package/dist/tui/components/OrchestratorPane.js +34 -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/components/StatusBar.js +6 -0
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/package.json +1 -1
- package/plan-explorer-musician-hardening.md +56 -0
- package/src/cli.ts +119 -86
- package/src/commands/tui.tsx +10 -4
- package/src/permission.ts +8 -8
- package/src/prompts/orchestrator-role.ts +26 -2
- package/src/prompts/tool-discipline.ts +6 -0
- package/src/tui/{App.tsx → components/App.tsx} +22 -20
- package/src/tui/{AppView.tsx → components/AppView.tsx} +5 -3
- package/src/tui/{Auditorium.tsx → components/Auditorium.tsx} +3 -3
- package/src/tui/{ConcertHall.tsx → components/ConcertHall.tsx} +1 -1
- package/src/tui/{Help.tsx → components/Help.tsx} +0 -9
- package/src/tui/{OrchestratorPane.tsx → components/OrchestratorPane.tsx} +1 -1
- package/src/tui/{SidebarHeader.tsx → components/SidebarHeader.tsx} +3 -1
- package/src/tui/{StatusBar.tsx → components/StatusBar.tsx} +1 -3
- package/tests/permission.test.ts +15 -5
- package/tests/tui/AppView.test.tsx +2 -2
- package/tests/tui/Auditorium.test.tsx +1 -1
- package/tests/tui/ConcertHall.test.tsx +1 -1
- package/tests/tui/Help.test.tsx +1 -1
- package/tests/tui/OrchestratorPane.test.ts +1 -1
- package/tests/tui/SidebarHeader.test.tsx +1 -1
- package/tests/tui/StatusBar.test.tsx +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Plan: Explorer Musician Hardening
|
|
2
|
+
|
|
3
|
+
Two-file change. Goal: enforce that the Orchestrator always delegates codebase
|
|
4
|
+
exploration to a cheap Sonnet Musician before writing a coding task spec, rather
|
|
5
|
+
than burning Opus tokens reading files itself.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. `src/prompts/tool-discipline.ts` — enforce the rule
|
|
10
|
+
|
|
11
|
+
Add one bullet to `ORCHESTRATOR_TOOL_DISCIPLINE` (the mandatory block):
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
- For any coding task, always spawn a Sonnet Explorer Musician first
|
|
15
|
+
(worktree=false, model="sonnet"). Only after the Explorer reports back
|
|
16
|
+
may you spawn a Coder Musician. Never read source files yourself to
|
|
17
|
+
build a coding task spec.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Place it after the existing "never write code yourself" bullet (once that is
|
|
21
|
+
added per the earlier plan).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. `src/prompts/orchestrator-role.ts` — guide the quality of each stage
|
|
26
|
+
|
|
27
|
+
Add a new section to the coordination guidance block, after the existing bullets:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
- Coding task workflow (two stages):
|
|
31
|
+
|
|
32
|
+
Stage 1 — Explorer Musician (Sonnet, worktree=false):
|
|
33
|
+
Task the Explorer to find and report back:
|
|
34
|
+
• Relevant file paths and line numbers for the change.
|
|
35
|
+
• The existing pattern or convention to follow.
|
|
36
|
+
• Any callers / dependents that may be affected (blast radius).
|
|
37
|
+
• Anything that would block or constrain the implementation.
|
|
38
|
+
|
|
39
|
+
Stage 2 — Coder Musician (Sonnet, fresh worktree):
|
|
40
|
+
Build the task spec from the Explorer's findings. Include:
|
|
41
|
+
• Exact files and line numbers to touch.
|
|
42
|
+
• The change required and why (one sentence).
|
|
43
|
+
• The pattern to follow (point to an existing example in the codebase).
|
|
44
|
+
• Acceptance criteria (what done looks like).
|
|
45
|
+
• Explicit constraints (don't break X, preserve Y interface).
|
|
46
|
+
A well-scoped spec is your primary output for coding requests.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## What does NOT change
|
|
52
|
+
|
|
53
|
+
- `musician-role.ts` — no change needed; Musicians already call `report_done`.
|
|
54
|
+
- `spawn.ts` / `handlers.ts` — purely a prompting change, no code changes.
|
|
55
|
+
- The Orchestrator still has full judgment on task decomposition and sequencing;
|
|
56
|
+
the Explorer rule applies to the codebase-research step only.
|
package/src/cli.ts
CHANGED
|
@@ -1,99 +1,117 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { decideAction, createOrchestra } from
|
|
4
|
-
import { attachOrRestore } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { decideAction, createOrchestra } from "./commands/launch.js";
|
|
4
|
+
import { attachOrRestore } from "./commands/attach.js";
|
|
5
|
+
import {
|
|
6
|
+
listOrchestras,
|
|
7
|
+
formatOrchestraList,
|
|
8
|
+
type OrchestraSummary,
|
|
9
|
+
} from "./commands/list.js";
|
|
10
|
+
import {
|
|
11
|
+
isPermissionLevel,
|
|
12
|
+
DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE,
|
|
13
|
+
DANGEROUSLY_SKIP_PERMISSIONS_WARNING,
|
|
14
|
+
type PermissionLevel,
|
|
15
|
+
} from "./permission.js";
|
|
16
|
+
import { detectClaude } from "./claude-detect.js";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
9
19
|
|
|
10
20
|
const program = new Command();
|
|
11
21
|
program
|
|
12
|
-
.name(
|
|
13
|
-
.description(
|
|
14
|
-
.version(
|
|
22
|
+
.name("nfo")
|
|
23
|
+
.description("NoFluffOrchestra — TUI multi-agent orchestrator")
|
|
24
|
+
.version(packageJson.version);
|
|
15
25
|
|
|
16
26
|
program
|
|
17
|
-
.argument(
|
|
18
|
-
.option(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case 'create': {
|
|
29
|
-
const level = await promptPermissionLevel();
|
|
30
|
-
await createOrchestra({
|
|
31
|
-
repoRoot: decision.repoRoot,
|
|
32
|
-
orchestraId: decision.orchestraId,
|
|
33
|
-
permissionLevel: level,
|
|
34
|
-
notifyOnPermission: opts.notifyOnPermission,
|
|
35
|
-
});
|
|
27
|
+
.argument("[id]", "Orchestra id to attach (optional)")
|
|
28
|
+
.option(
|
|
29
|
+
"--notify-on-permission",
|
|
30
|
+
"bell + desktop notify when a musician awaits permission",
|
|
31
|
+
)
|
|
32
|
+
.action(
|
|
33
|
+
async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
|
|
34
|
+
await detectClaude();
|
|
35
|
+
try {
|
|
36
|
+
if (id) {
|
|
37
|
+
await attachOrRestore(id);
|
|
36
38
|
return;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const decision = await decideAction(process.cwd());
|
|
41
|
+
switch (decision.kind) {
|
|
42
|
+
case "create": {
|
|
43
|
+
const level = await promptPermissionLevel();
|
|
44
|
+
await createOrchestra({
|
|
45
|
+
repoRoot: decision.repoRoot,
|
|
46
|
+
orchestraId: decision.orchestraId,
|
|
47
|
+
permissionLevel: level,
|
|
48
|
+
notifyOnPermission: opts.notifyOnPermission,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
case "attach_existing":
|
|
53
|
+
await attachOrRestore(decision.orchestraId);
|
|
54
|
+
return;
|
|
55
|
+
case "pick": {
|
|
56
|
+
const picked = await promptOrchestraPicker(decision.summaries);
|
|
57
|
+
await attachOrRestore(picked);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
case "error":
|
|
61
|
+
console.error(decision.message);
|
|
62
|
+
process.exit(1);
|
|
45
63
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
66
|
+
process.exit(1);
|
|
49
67
|
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
68
|
+
},
|
|
69
|
+
);
|
|
55
70
|
|
|
56
71
|
program
|
|
57
|
-
.command(
|
|
58
|
-
.description(
|
|
72
|
+
.command("list")
|
|
73
|
+
.description("List all known orchestras")
|
|
59
74
|
.action(async () => {
|
|
60
75
|
const summaries = await listOrchestras();
|
|
61
76
|
console.log(formatOrchestraList(summaries));
|
|
62
77
|
});
|
|
63
78
|
|
|
64
79
|
program
|
|
65
|
-
.command(
|
|
66
|
-
.description(
|
|
67
|
-
.option(
|
|
80
|
+
.command("restore <id>")
|
|
81
|
+
.description("Force-restore a stopped orchestra")
|
|
82
|
+
.option(
|
|
83
|
+
"--notify-on-permission",
|
|
84
|
+
"bell + desktop notify when a musician awaits permission",
|
|
85
|
+
)
|
|
68
86
|
.action(async (id: string, opts: { notifyOnPermission?: boolean }) => {
|
|
69
|
-
const { restoreOrchestra } = await import(
|
|
87
|
+
const { restoreOrchestra } = await import("./commands/restore.js");
|
|
70
88
|
await restoreOrchestra(id, undefined, opts.notifyOnPermission);
|
|
71
89
|
});
|
|
72
90
|
|
|
73
91
|
program
|
|
74
|
-
.command(
|
|
75
|
-
.description(
|
|
76
|
-
.option(
|
|
92
|
+
.command("kill <id>")
|
|
93
|
+
.description("Tear down an orchestra (state archived, notes preserved)")
|
|
94
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
77
95
|
.action(async (id: string, opts: { yes?: boolean }) => {
|
|
78
|
-
const { killOrchestra } = await import(
|
|
96
|
+
const { killOrchestra } = await import("./commands/kill.js");
|
|
79
97
|
await killOrchestra(id, opts);
|
|
80
98
|
});
|
|
81
99
|
|
|
82
100
|
program
|
|
83
|
-
.command(
|
|
84
|
-
.description(
|
|
101
|
+
.command("notes <id>")
|
|
102
|
+
.description("Open the orchestra's notes/ directory in $EDITOR")
|
|
85
103
|
.action(async (id: string) => {
|
|
86
|
-
const { openNotes } = await import(
|
|
104
|
+
const { openNotes } = await import("./commands/notes.js");
|
|
87
105
|
await openNotes(id);
|
|
88
106
|
});
|
|
89
107
|
|
|
90
108
|
program
|
|
91
|
-
.command(
|
|
92
|
-
.description(
|
|
93
|
-
.requiredOption(
|
|
94
|
-
.option(
|
|
109
|
+
.command("mcp-server", { hidden: true })
|
|
110
|
+
.description("(internal) Run the NFO MCP server attached to an orchestra")
|
|
111
|
+
.requiredOption("--orchestra-id <id>", "Orchestra id")
|
|
112
|
+
.option("--caller-musician-id <id>", "When the server is hosting a Musician")
|
|
95
113
|
.action(async (opts: { orchestraId: string; callerMusicianId?: string }) => {
|
|
96
|
-
const { runMcpServerCli } = await import(
|
|
114
|
+
const { runMcpServerCli } = await import("./commands/mcp-server.js");
|
|
97
115
|
await runMcpServerCli({
|
|
98
116
|
orchestraId: opts.orchestraId,
|
|
99
117
|
callerMusicianId: opts.callerMusicianId,
|
|
@@ -101,12 +119,15 @@ program
|
|
|
101
119
|
});
|
|
102
120
|
|
|
103
121
|
program
|
|
104
|
-
.command(
|
|
105
|
-
.description(
|
|
106
|
-
.requiredOption(
|
|
122
|
+
.command("tui", { hidden: true })
|
|
123
|
+
.description("(internal) Run the NFO Ink TUI for an orchestra")
|
|
124
|
+
.requiredOption("--orchestra-id <id>", "Orchestra id")
|
|
107
125
|
.action(async (opts: { orchestraId: string }) => {
|
|
108
|
-
const { runTui } = await import(
|
|
109
|
-
await runTui({
|
|
126
|
+
const { runTui } = await import("./commands/tui.js");
|
|
127
|
+
await runTui({
|
|
128
|
+
orchestraId: opts.orchestraId,
|
|
129
|
+
version: packageJson.version,
|
|
130
|
+
});
|
|
110
131
|
});
|
|
111
132
|
|
|
112
133
|
program.parseAsync(process.argv);
|
|
@@ -114,28 +135,36 @@ program.parseAsync(process.argv);
|
|
|
114
135
|
async function promptPermissionLevel(): Promise<PermissionLevel> {
|
|
115
136
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
116
137
|
try {
|
|
117
|
-
const ans = (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
const ans = (
|
|
139
|
+
await rl.question(
|
|
140
|
+
`Permission level for this orchestra:
|
|
141
|
+
1) Dangerously skip permissions — RISKY: bypasses all permission checks
|
|
142
|
+
2) auto — Claude auto mode (prompts only on risky actions)
|
|
143
|
+
3) edits — auto-accept edits, prompt on shell/tools
|
|
144
|
+
4) supervised — claude's default prompt-on-risky behavior
|
|
145
|
+
5) strict — read-only / plan mode
|
|
146
|
+
Choose [1-5] (default 4): `,
|
|
147
|
+
)
|
|
148
|
+
).trim();
|
|
125
149
|
|
|
126
150
|
const map: Record<string, PermissionLevel> = {
|
|
127
|
-
|
|
151
|
+
"1": "dangerouslySkipPermissions",
|
|
152
|
+
"2": "auto",
|
|
153
|
+
"3": "acceptEdits",
|
|
154
|
+
"4": "supervised",
|
|
155
|
+
"5": "strict",
|
|
156
|
+
"": "supervised",
|
|
128
157
|
};
|
|
129
158
|
const level = map[ans];
|
|
130
159
|
if (!level || !isPermissionLevel(level)) {
|
|
131
160
|
throw new Error(`Invalid choice: ${ans}`);
|
|
132
161
|
}
|
|
133
162
|
|
|
134
|
-
if (level ===
|
|
135
|
-
console.log(
|
|
136
|
-
const confirm = (await rl.question(
|
|
137
|
-
if (confirm !==
|
|
138
|
-
throw new Error(
|
|
163
|
+
if (level === "dangerouslySkipPermissions") {
|
|
164
|
+
console.log("\n" + DANGEROUSLY_SKIP_PERMISSIONS_WARNING + "\n");
|
|
165
|
+
const confirm = (await rl.question("> ")).trim();
|
|
166
|
+
if (confirm !== DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE) {
|
|
167
|
+
throw new Error("Auto mode not confirmed. Aborting.");
|
|
139
168
|
}
|
|
140
169
|
}
|
|
141
170
|
|
|
@@ -145,17 +174,21 @@ Choose [1-4] (default 3): `,
|
|
|
145
174
|
}
|
|
146
175
|
}
|
|
147
176
|
|
|
148
|
-
async function promptOrchestraPicker(
|
|
177
|
+
async function promptOrchestraPicker(
|
|
178
|
+
summaries: OrchestraSummary[],
|
|
179
|
+
): Promise<string> {
|
|
149
180
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
150
181
|
try {
|
|
151
|
-
console.log(
|
|
182
|
+
console.log("Multiple orchestras found:");
|
|
152
183
|
summaries.forEach((s, i) => {
|
|
153
|
-
console.log(
|
|
184
|
+
console.log(
|
|
185
|
+
` ${i + 1}) ${s.running ? "●" : "○"} ${s.id} (${s.project_path})`,
|
|
186
|
+
);
|
|
154
187
|
});
|
|
155
|
-
const choice = (await rl.question(
|
|
188
|
+
const choice = (await rl.question("Pick one [1-N]: ")).trim();
|
|
156
189
|
const idx = Number(choice) - 1;
|
|
157
190
|
if (Number.isNaN(idx) || idx < 0 || idx >= summaries.length) {
|
|
158
|
-
throw new Error(
|
|
191
|
+
throw new Error("Invalid choice");
|
|
159
192
|
}
|
|
160
193
|
return summaries[idx].id;
|
|
161
194
|
} finally {
|
package/src/commands/tui.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { render } from
|
|
2
|
-
import { App } from
|
|
3
|
-
import { readState } from
|
|
1
|
+
import { render } from "ink";
|
|
2
|
+
import { App } from "../tui/components/App.js";
|
|
3
|
+
import { readState } from "../state.js";
|
|
4
4
|
|
|
5
5
|
export interface RunTuiOptions {
|
|
6
6
|
orchestraId: string;
|
|
7
|
+
version: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export async function runTui(opts: RunTuiOptions): Promise<void> {
|
|
@@ -11,6 +12,11 @@ export async function runTui(opts: RunTuiOptions): Promise<void> {
|
|
|
11
12
|
if (!state) {
|
|
12
13
|
throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
|
|
13
14
|
}
|
|
14
|
-
const instance = render(
|
|
15
|
+
const instance = render(
|
|
16
|
+
<App orchestraId={opts.orchestraId} version={opts.version} />,
|
|
17
|
+
{
|
|
18
|
+
exitOnCtrlC: false,
|
|
19
|
+
},
|
|
20
|
+
);
|
|
15
21
|
await instance.waitUntilExit();
|
|
16
22
|
}
|
package/src/permission.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PERMISSION_LEVELS = ['auto', '
|
|
1
|
+
export const PERMISSION_LEVELS = ['dangerouslySkipPermissions', 'auto', 'acceptEdits', 'supervised', 'strict'] as const;
|
|
2
2
|
export type PermissionLevel = (typeof PERMISSION_LEVELS)[number];
|
|
3
3
|
|
|
4
4
|
export function isPermissionLevel(s: string): s is PermissionLevel {
|
|
@@ -7,11 +7,11 @@ export function isPermissionLevel(s: string): s is PermissionLevel {
|
|
|
7
7
|
|
|
8
8
|
export function claudeFlagsForLevel(level: PermissionLevel): string[] {
|
|
9
9
|
switch (level) {
|
|
10
|
-
case '
|
|
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.
|
|
10
|
+
case 'dangerouslySkipPermissions':
|
|
13
11
|
return ['--dangerously-skip-permissions'];
|
|
14
|
-
case '
|
|
12
|
+
case 'auto':
|
|
13
|
+
return ['--permission-mode', 'auto'];
|
|
14
|
+
case 'acceptEdits':
|
|
15
15
|
return ['--permission-mode', 'acceptEdits'];
|
|
16
16
|
case 'supervised':
|
|
17
17
|
return ['--permission-mode', 'default'];
|
|
@@ -20,11 +20,11 @@ export function claudeFlagsForLevel(level: PermissionLevel): string[] {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const
|
|
23
|
+
export const DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE = 'I understand';
|
|
24
24
|
|
|
25
|
-
export const
|
|
25
|
+
export const DANGEROUSLY_SKIP_PERMISSIONS_WARNING = `⚠ "Dangerously skip permissions" mode disables all permission checks.
|
|
26
26
|
Musicians can execute arbitrary shell commands, modify files anywhere on
|
|
27
27
|
this system, and access the network without asking. Worktrees limit but
|
|
28
28
|
do not contain risky operations. Use this only in trusted sandboxes or
|
|
29
29
|
when you accept these risks.
|
|
30
|
-
Type "${
|
|
30
|
+
Type "${DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE}" to continue.`;
|
|
@@ -9,8 +9,8 @@ Available NFO tools (in addition to your normal Claude Code tools):
|
|
|
9
9
|
|
|
10
10
|
spawn_musician({ name, task, worktree?, branch_from?, model? })
|
|
11
11
|
Create a Musician with the given task. By default the Musician runs in a
|
|
12
|
-
fresh git worktree off HEAD.
|
|
13
|
-
work (e.g., docs-only) that doesn't need an isolated branch. Returns the
|
|
12
|
+
fresh git worktree off HEAD.
|
|
13
|
+
Pass worktree=false for trivially isolated and research work (e.g., docs-only) that doesn't need an isolated branch. Returns the
|
|
14
14
|
musician_id. Provide a model to be used by the Musician, otherwise it defaults to sonnet.
|
|
15
15
|
For trivial tasks Haiku is a good choice; for complex coding work, Sonnet is better.
|
|
16
16
|
|
|
@@ -45,6 +45,12 @@ Coordination guidance:
|
|
|
45
45
|
- For agent coordination, PREFER the NFO MCP tools over Claude Code's built-in
|
|
46
46
|
Task tool. The user tracks Musician work through NFO; Task spawns are invisible
|
|
47
47
|
to NFO.
|
|
48
|
+
- Deploy research musicians that investigate the codebase for the given task. They are only allowed
|
|
49
|
+
to research and report back findings, without modifying the codebase.
|
|
50
|
+
- Before spawning a coding Musician, prepare a complete taks spec:
|
|
51
|
+
Relevant paths, line numbers, the exact changes and why, acceptance criteria,
|
|
52
|
+
and any constraints (e.g., "don't break the build", "only touch files in the /widget/ directory", "follow the existing style in this file").
|
|
53
|
+
A well-scoped prompt is your primary output on coding requests.
|
|
48
54
|
- Worktrees solve concurrent file-edit safety, not API coupling. If two
|
|
49
55
|
Musicians' outputs need to be wired together, sequence the work, or spawn an
|
|
50
56
|
integration Musician afterward.
|
|
@@ -57,4 +63,22 @@ Coordination guidance:
|
|
|
57
63
|
- Project-level guidance in CLAUDE.md still applies; respect it.
|
|
58
64
|
- You can use Superpowers if present but make sure that works are delegated to
|
|
59
65
|
Musicians in the end if subagent driven development is picked by the user.
|
|
66
|
+
- Coding task workflow (two stages):
|
|
67
|
+
|
|
68
|
+
Stage 1 — Explorer Musician (Sonnet, worktree=false):
|
|
69
|
+
Task the Explorer to find and report back:
|
|
70
|
+
• Relevant file paths and line numbers for the change.
|
|
71
|
+
• The existing pattern or convention to follow.
|
|
72
|
+
• Any callers / dependents that may be affected (blast radius).
|
|
73
|
+
• Anything that would block or constrain the implementation.
|
|
74
|
+
|
|
75
|
+
Stage 2 — Coder Musician (Haiku preferred or Sonnet if really needed, fresh worktree if required):
|
|
76
|
+
Build the task spec from the Explorer's findings. Include:
|
|
77
|
+
• Exact files and line numbers to touch.
|
|
78
|
+
• The change required and why (one sentence).
|
|
79
|
+
• The pattern to follow (point to an existing example in the codebase).
|
|
80
|
+
• Acceptance criteria (what done looks like).
|
|
81
|
+
• Explicit constraints (don't break X, preserve Y interface).
|
|
82
|
+
A well-scoped spec is your primary output for coding requests.
|
|
83
|
+
|
|
60
84
|
`;
|
|
@@ -5,6 +5,12 @@ export const ORCHESTRATOR_TOOL_DISCIPLINE = `Tool discipline (mandatory):
|
|
|
5
5
|
something later. Call the corresponding NFO tool in the same turn.
|
|
6
6
|
- Do not use Claude Code's built-in Task tool for Musician coordination; those
|
|
7
7
|
agents are invisible to NFO.
|
|
8
|
+
- Never write, edit or refactor code yourself. All coding tasks must be delegated
|
|
9
|
+
to a Musician via \`spawn_musician\`. Your task as an Orchestrator is to prepare
|
|
10
|
+
and hand off work, not to execute it.
|
|
11
|
+
- For any coding task, always spawn a Sonnet Explorer Musician first (worktree=false, model="sonnet"), Only
|
|
12
|
+
after the Explorer reports back may you spawn a Coder Musician. Never read source files
|
|
13
|
+
yourself to build a coding task spec.
|
|
8
14
|
- When a Musician reports back, resolve it in the same turn with an NFO tool
|
|
9
15
|
call (usually \`dismiss_musician\` or \`message_musician\`). A prose-only
|
|
10
16
|
acknowledgement is non-compliant.
|
|
@@ -2,30 +2,30 @@ import type { ReactElement } from "react";
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { useInput, useStdout, useWindowSize } from "ink";
|
|
4
4
|
import { AppView } from "./AppView.js";
|
|
5
|
-
import { reduceKey } from "
|
|
6
|
-
import { pollActivity } from "
|
|
5
|
+
import { reduceKey } from "../keymap.js";
|
|
6
|
+
import { pollActivity } from "../poll-activity.js";
|
|
7
7
|
import {
|
|
8
8
|
syncMusicianIdleState,
|
|
9
9
|
type MusicianIdleTracker,
|
|
10
|
-
} from "
|
|
11
|
-
import { pollPermissions } from "
|
|
12
|
-
import { setMusicianStatus } from "
|
|
13
|
-
import { watchOrchestraState, type StopWatching } from "
|
|
14
|
-
import { listOrchestras, type OrchestraSummary } from "
|
|
10
|
+
} from "../poll-idle.js";
|
|
11
|
+
import { pollPermissions } from "../poll-permission.js";
|
|
12
|
+
import { setMusicianStatus } from "../../state-updaters.js";
|
|
13
|
+
import { watchOrchestraState, type StopWatching } from "../watch-state.js";
|
|
14
|
+
import { listOrchestras, type OrchestraSummary } from "../../commands/list.js";
|
|
15
15
|
import {
|
|
16
16
|
EmbeddedTerminal,
|
|
17
17
|
type EmbeddedTerminalSnapshot,
|
|
18
|
-
} from "
|
|
18
|
+
} from "../embedded-terminal.js";
|
|
19
19
|
import {
|
|
20
20
|
claimEmbeddedSessionLease,
|
|
21
21
|
embeddedSessionLeaseIsCurrent,
|
|
22
22
|
runEmbeddedSessionOperation,
|
|
23
|
-
} from "
|
|
23
|
+
} from "../embedded-session-lifecycle.js";
|
|
24
24
|
import {
|
|
25
25
|
toTerminalMouseScroll,
|
|
26
26
|
toTerminalInput,
|
|
27
27
|
toTerminalViewportCommand,
|
|
28
|
-
} from "
|
|
28
|
+
} from "../terminal-input.js";
|
|
29
29
|
import {
|
|
30
30
|
detachCurrentClient,
|
|
31
31
|
embeddedSessionName,
|
|
@@ -33,15 +33,16 @@ import {
|
|
|
33
33
|
killSession,
|
|
34
34
|
selectWindow,
|
|
35
35
|
sessionName,
|
|
36
|
-
} from "
|
|
37
|
-
import { openNotes } from "
|
|
38
|
-
import { dismissMusician } from "
|
|
39
|
-
import { readState } from "
|
|
40
|
-
import { notifyAwaitingPermission } from "
|
|
41
|
-
import type { Musician, OrchestraState } from "
|
|
36
|
+
} from "../../tmux.js";
|
|
37
|
+
import { openNotes } from "../../commands/notes.js";
|
|
38
|
+
import { dismissMusician } from "../../musicians/dismiss.js";
|
|
39
|
+
import { readState } from "../../state.js";
|
|
40
|
+
import { notifyAwaitingPermission } from "../../notify.js";
|
|
41
|
+
import type { Musician, OrchestraState } from "../../state.types.js";
|
|
42
42
|
|
|
43
43
|
export interface AppProps {
|
|
44
44
|
orchestraId: string;
|
|
45
|
+
version: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function textLines(...lines: string[]): EmbeddedTerminalSnapshot["lines"] {
|
|
@@ -358,10 +359,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
358
359
|
const mouseScroll = toTerminalMouseScroll(input);
|
|
359
360
|
if (mouseScroll) {
|
|
360
361
|
const insideTerminalViewport =
|
|
361
|
-
mouseScroll.column >= terminalScreenLeft
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
mouseScroll.column >= terminalScreenLeft &&
|
|
363
|
+
mouseScroll.column <= terminalScreenRight &&
|
|
364
|
+
mouseScroll.row >= terminalScreenTop &&
|
|
365
|
+
mouseScroll.row <= terminalScreenBottom;
|
|
365
366
|
if (insideTerminalViewport) {
|
|
366
367
|
const translatedColumn = mouseScroll.column - terminalScreenLeft + 1;
|
|
367
368
|
const translatedRow = mouseScroll.row - terminalScreenTop + 1;
|
|
@@ -527,6 +528,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
527
528
|
orchestratorConnected={orchestratorSnapshot.connected}
|
|
528
529
|
activeMusicianId={activePaneMusician?.id ?? null}
|
|
529
530
|
orchestratorActive={activePaneMusician === null}
|
|
531
|
+
version={props.version}
|
|
530
532
|
/>
|
|
531
533
|
);
|
|
532
534
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import { Box } from "ink";
|
|
3
|
-
import type { Musician } from "
|
|
4
|
-
import type { OrchestraSummary } from "
|
|
5
|
-
import type { EmbeddedTerminalLine } from "
|
|
3
|
+
import type { Musician } from "../../state.types.js";
|
|
4
|
+
import type { OrchestraSummary } from "../../commands/list.js";
|
|
5
|
+
import type { EmbeddedTerminalLine } from "../embedded-terminal.js";
|
|
6
6
|
import { ConcertHall } from "./ConcertHall.js";
|
|
7
7
|
import { Auditorium } from "./Auditorium.js";
|
|
8
8
|
import { StatusBar } from "./StatusBar.js";
|
|
@@ -28,6 +28,7 @@ export interface AppViewProps {
|
|
|
28
28
|
orchestratorConnected: boolean;
|
|
29
29
|
activeMusicianId?: string | null;
|
|
30
30
|
orchestratorActive?: boolean;
|
|
31
|
+
version: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function AppView(props: AppViewProps): ReactElement {
|
|
@@ -46,6 +47,7 @@ export function AppView(props: AppViewProps): ReactElement {
|
|
|
46
47
|
orchestraId={props.currentId}
|
|
47
48
|
musicianCount={props.musicians.length}
|
|
48
49
|
pendingCount={pendingCount}
|
|
50
|
+
version={props.version}
|
|
49
51
|
/>
|
|
50
52
|
<ConcertHall
|
|
51
53
|
orchestras={props.orchestras}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import type { Musician } from "
|
|
4
|
-
import { statusIcon, statusColor } from "
|
|
5
|
-
import { formatRelativeTime } from "
|
|
3
|
+
import type { Musician } from "../../state.types.js";
|
|
4
|
+
import { statusIcon, statusColor } from "../status-icon.js";
|
|
5
|
+
import { formatRelativeTime } from "../format-time.js";
|
|
6
6
|
|
|
7
7
|
export interface AuditoriumProps {
|
|
8
8
|
musicians: Musician[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import type { OrchestraSummary } from "
|
|
3
|
+
import type { OrchestraSummary } from "../../commands/list.js";
|
|
4
4
|
|
|
5
5
|
export interface ConcertHallProps {
|
|
6
6
|
orchestras: OrchestraSummary[];
|
|
@@ -42,15 +42,6 @@ const ROWS: Row[] = [
|
|
|
42
42
|
label:
|
|
43
43
|
"scroll the left terminal through local scrollback when the pointer is over that pane",
|
|
44
44
|
},
|
|
45
|
-
{
|
|
46
|
-
key: "F6",
|
|
47
|
-
label: "tmux global key: jump to the dashboard window from any NFO window",
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
key: "F7",
|
|
51
|
-
label:
|
|
52
|
-
"tmux global key: jump to the Orchestrator window from any NFO window",
|
|
53
|
-
},
|
|
54
45
|
{ key: "?", label: "toggle this help / close" },
|
|
55
46
|
];
|
|
56
47
|
|