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.
Files changed (59) hide show
  1. package/dist/cli.js +64 -54
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/restore.js +0 -1
  4. package/dist/commands/restore.js.map +1 -1
  5. package/dist/commands/tui.js +6 -4
  6. package/dist/commands/tui.js.map +1 -1
  7. package/dist/permission.js +8 -8
  8. package/dist/permission.js.map +1 -1
  9. package/dist/prompts/orchestrator-role.js +29 -7
  10. package/dist/prompts/orchestrator-role.js.map +1 -1
  11. package/dist/prompts/tool-discipline.js +6 -0
  12. package/dist/prompts/tool-discipline.js.map +1 -1
  13. package/dist/tui/App.js +5 -5
  14. package/dist/tui/App.js.map +1 -1
  15. package/dist/tui/AppView.js +1 -1
  16. package/dist/tui/AppView.js.map +1 -1
  17. package/dist/tui/SidebarHeader.js +1 -1
  18. package/dist/tui/SidebarHeader.js.map +1 -1
  19. package/dist/tui/StatusBar.js +1 -1
  20. package/dist/tui/StatusBar.js.map +1 -1
  21. package/dist/tui/components/App.js +428 -0
  22. package/dist/tui/components/App.js.map +1 -0
  23. package/dist/tui/components/AppView.js +13 -0
  24. package/dist/tui/components/AppView.js.map +1 -0
  25. package/dist/tui/components/Auditorium.js +17 -0
  26. package/dist/tui/components/Auditorium.js.map +1 -0
  27. package/dist/tui/components/ConcertHall.js +11 -0
  28. package/dist/tui/components/ConcertHall.js.map +1 -0
  29. package/dist/tui/components/Help.js +41 -0
  30. package/dist/tui/components/Help.js.map +1 -0
  31. package/dist/tui/components/OrchestratorPane.js +34 -0
  32. package/dist/tui/components/OrchestratorPane.js.map +1 -0
  33. package/dist/tui/components/SidebarHeader.js +6 -0
  34. package/dist/tui/components/SidebarHeader.js.map +1 -0
  35. package/dist/tui/components/StatusBar.js +6 -0
  36. package/dist/tui/components/StatusBar.js.map +1 -0
  37. package/package.json +1 -1
  38. package/plan-explorer-musician-hardening.md +56 -0
  39. package/src/cli.ts +119 -86
  40. package/src/commands/tui.tsx +10 -4
  41. package/src/permission.ts +8 -8
  42. package/src/prompts/orchestrator-role.ts +26 -2
  43. package/src/prompts/tool-discipline.ts +6 -0
  44. package/src/tui/{App.tsx → components/App.tsx} +22 -20
  45. package/src/tui/{AppView.tsx → components/AppView.tsx} +5 -3
  46. package/src/tui/{Auditorium.tsx → components/Auditorium.tsx} +3 -3
  47. package/src/tui/{ConcertHall.tsx → components/ConcertHall.tsx} +1 -1
  48. package/src/tui/{Help.tsx → components/Help.tsx} +0 -9
  49. package/src/tui/{OrchestratorPane.tsx → components/OrchestratorPane.tsx} +1 -1
  50. package/src/tui/{SidebarHeader.tsx → components/SidebarHeader.tsx} +3 -1
  51. package/src/tui/{StatusBar.tsx → components/StatusBar.tsx} +1 -3
  52. package/tests/permission.test.ts +15 -5
  53. package/tests/tui/AppView.test.tsx +2 -2
  54. package/tests/tui/Auditorium.test.tsx +1 -1
  55. package/tests/tui/ConcertHall.test.tsx +1 -1
  56. package/tests/tui/Help.test.tsx +1 -1
  57. package/tests/tui/OrchestratorPane.test.ts +1 -1
  58. package/tests/tui/SidebarHeader.test.tsx +1 -1
  59. package/tests/tui/StatusBar.test.tsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nfo-cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.4-improve-prompting",
4
4
  "description": "No Fluff Orchestra — a simple, fluff-free CLI multi-agent orchestrator",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 'commander';
3
- import { decideAction, createOrchestra } from './commands/launch.js';
4
- import { attachOrRestore } from './commands/attach.js';
5
- import { listOrchestras, formatOrchestraList, type OrchestraSummary } from './commands/list.js';
6
- import { isPermissionLevel, AUTO_CONFIRM_PHRASE, AUTO_WARNING, type PermissionLevel } from './permission.js';
7
- import { detectClaude } from './claude-detect.js';
8
- import { createInterface } from 'node:readline/promises';
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('nfo')
13
- .description('NoFluffOrchestra — TUI multi-agent orchestrator')
14
- .version('0.0.0');
22
+ .name("nfo")
23
+ .description("NoFluffOrchestra — TUI multi-agent orchestrator")
24
+ .version(packageJson.version);
15
25
 
16
26
  program
17
- .argument('[id]', 'Orchestra id to attach (optional)')
18
- .option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
19
- .action(async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
20
- await detectClaude();
21
- try {
22
- if (id) {
23
- await attachOrRestore(id);
24
- return;
25
- }
26
- const decision = await decideAction(process.cwd());
27
- switch (decision.kind) {
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
- case 'attach_existing':
39
- await attachOrRestore(decision.orchestraId);
40
- return;
41
- case 'pick': {
42
- const picked = await promptOrchestraPicker(decision.summaries);
43
- await attachOrRestore(picked);
44
- return;
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
- case 'error':
47
- console.error(decision.message);
48
- process.exit(1);
64
+ } catch (err) {
65
+ console.error(err instanceof Error ? err.message : String(err));
66
+ process.exit(1);
49
67
  }
50
- } catch (err) {
51
- console.error(err instanceof Error ? err.message : String(err));
52
- process.exit(1);
53
- }
54
- });
68
+ },
69
+ );
55
70
 
56
71
  program
57
- .command('list')
58
- .description('List all known orchestras')
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('restore <id>')
66
- .description('Force-restore a stopped orchestra')
67
- .option('--notify-on-permission', 'bell + desktop notify when a musician awaits permission')
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('./commands/restore.js');
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('kill <id>')
75
- .description('Tear down an orchestra (state archived, notes preserved)')
76
- .option('-y, --yes', 'Skip confirmation prompt')
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('./commands/kill.js');
96
+ const { killOrchestra } = await import("./commands/kill.js");
79
97
  await killOrchestra(id, opts);
80
98
  });
81
99
 
82
100
  program
83
- .command('notes <id>')
84
- .description('Open the orchestra\'s notes/ directory in $EDITOR')
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('./commands/notes.js');
104
+ const { openNotes } = await import("./commands/notes.js");
87
105
  await openNotes(id);
88
106
  });
89
107
 
90
108
  program
91
- .command('mcp-server', { hidden: true })
92
- .description('(internal) Run the NFO MCP server attached to an orchestra')
93
- .requiredOption('--orchestra-id <id>', 'Orchestra id')
94
- .option('--caller-musician-id <id>', 'When the server is hosting a Musician')
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('./commands/mcp-server.js');
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('tui', { hidden: true })
105
- .description('(internal) Run the NFO Ink TUI for an orchestra')
106
- .requiredOption('--orchestra-id <id>', 'Orchestra id')
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('./commands/tui.js');
109
- await runTui({ orchestraId: opts.orchestraId });
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 = (await rl.question(
118
- `Permission level for this orchestra:
119
- 1) auto RISKY: bypasses all permission checks
120
- 2) autonomous auto-accept edits, prompt on risky tools
121
- 3) supervised claude's default prompt-on-risky behavior
122
- 4) strict read-only / plan mode
123
- Choose [1-4] (default 3): `,
124
- )).trim();
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
- '1': 'auto', '2': 'autonomous', '3': 'supervised', '4': 'strict', '': 'supervised',
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 === 'auto') {
135
- console.log('\n' + AUTO_WARNING + '\n');
136
- const confirm = (await rl.question('> ')).trim();
137
- if (confirm !== AUTO_CONFIRM_PHRASE) {
138
- throw new Error('Auto mode not confirmed. Aborting.');
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(summaries: OrchestraSummary[]): Promise<string> {
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('Multiple orchestras found:');
182
+ console.log("Multiple orchestras found:");
152
183
  summaries.forEach((s, i) => {
153
- console.log(` ${i + 1}) ${s.running ? '●' : '○'} ${s.id} (${s.project_path})`);
184
+ console.log(
185
+ ` ${i + 1}) ${s.running ? "●" : "○"} ${s.id} (${s.project_path})`,
186
+ );
154
187
  });
155
- const choice = (await rl.question('Pick one [1-N]: ')).trim();
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('Invalid choice');
191
+ throw new Error("Invalid choice");
159
192
  }
160
193
  return summaries[idx].id;
161
194
  } finally {
@@ -1,9 +1,10 @@
1
- import { render } from 'ink';
2
- import { App } from '../tui/App.js';
3
- import { readState } from '../state.js';
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(<App orchestraId={opts.orchestraId} />, { exitOnCtrlC: false });
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', 'autonomous', 'supervised', 'strict'] as const;
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 '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.
10
+ case 'dangerouslySkipPermissions':
13
11
  return ['--dangerously-skip-permissions'];
14
- case 'autonomous':
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 AUTO_CONFIRM_PHRASE = 'I understand';
23
+ export const DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE = 'I understand';
24
24
 
25
- export const AUTO_WARNING = `⚠ AUTO mode disables all permission checks.
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 "${AUTO_CONFIRM_PHRASE}" to continue.`;
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. Pass worktree=false for trivially isolated
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 "./keymap.js";
6
- import { pollActivity } from "./poll-activity.js";
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 "./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";
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 "./embedded-terminal.js";
18
+ } from "../embedded-terminal.js";
19
19
  import {
20
20
  claimEmbeddedSessionLease,
21
21
  embeddedSessionLeaseIsCurrent,
22
22
  runEmbeddedSessionOperation,
23
- } from "./embedded-session-lifecycle.js";
23
+ } from "../embedded-session-lifecycle.js";
24
24
  import {
25
25
  toTerminalMouseScroll,
26
26
  toTerminalInput,
27
27
  toTerminalViewportCommand,
28
- } from "./terminal-input.js";
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 "../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";
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
- && mouseScroll.column <= terminalScreenRight
363
- && mouseScroll.row >= terminalScreenTop
364
- && mouseScroll.row <= terminalScreenBottom;
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 "../state.types.js";
4
- import type { OrchestraSummary } from "../commands/list.js";
5
- import type { EmbeddedTerminalLine } from "./embedded-terminal.js";
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 "../state.types.js";
4
- import { statusIcon, statusColor } from "./status-icon.js";
5
- import { formatRelativeTime } from "./format-time.js";
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 "../commands/list.js";
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
 
@@ -3,7 +3,7 @@ import { Box, Text } from "ink";
3
3
  import type {
4
4
  EmbeddedTerminalLine,
5
5
  EmbeddedTerminalSpan,
6
- } from "./embedded-terminal.js";
6
+ } from "../embedded-terminal.js";
7
7
 
8
8
  export interface OrchestratorPaneProps {
9
9
  title: string;