nfo-cli 0.0.4-improve-prompting → 0.0.6-a89844d-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/claude-command.js +6 -1
  2. package/dist/claude-command.js.map +1 -1
  3. package/dist/claude-trust.js +46 -0
  4. package/dist/claude-trust.js.map +1 -0
  5. package/dist/cli.js +5 -4
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/attach.js +8 -8
  8. package/dist/commands/attach.js.map +1 -1
  9. package/dist/commands/launch.js +3 -6
  10. package/dist/commands/launch.js.map +1 -1
  11. package/dist/commands/restore.js +6 -10
  12. package/dist/commands/restore.js.map +1 -1
  13. package/dist/commands/tui.js +17 -1
  14. package/dist/commands/tui.js.map +1 -1
  15. package/dist/mcp/handlers.js +5 -0
  16. package/dist/mcp/handlers.js.map +1 -1
  17. package/dist/mcp/tool-defs.js +10 -0
  18. package/dist/mcp/tool-defs.js.map +1 -1
  19. package/dist/musicians/dismiss.js +1 -1
  20. package/dist/musicians/dismiss.js.map +1 -1
  21. package/dist/musicians/reconcile.js +27 -0
  22. package/dist/musicians/reconcile.js.map +1 -0
  23. package/dist/musicians/roles.js +15 -0
  24. package/dist/musicians/roles.js.map +1 -0
  25. package/dist/musicians/spawn.js +53 -18
  26. package/dist/musicians/spawn.js.map +1 -1
  27. package/dist/permission.js +6 -0
  28. package/dist/permission.js.map +1 -1
  29. package/dist/prompts/musician-role.js +2 -1
  30. package/dist/prompts/musician-role.js.map +1 -1
  31. package/dist/prompts/orchestrator-role.js +18 -6
  32. package/dist/prompts/orchestrator-role.js.map +1 -1
  33. package/dist/prompts/tool-discipline.js +7 -3
  34. package/dist/prompts/tool-discipline.js.map +1 -1
  35. package/dist/tmux.js +23 -0
  36. package/dist/tmux.js.map +1 -1
  37. package/dist/tui/components/App.js +8 -12
  38. package/dist/tui/components/App.js.map +1 -1
  39. package/dist/tui/components/Help.js +1 -1
  40. package/dist/tui/components/Help.js.map +1 -1
  41. package/dist/tui/keymap.js +1 -1
  42. package/dist/tui/keymap.js.map +1 -1
  43. package/package.json +18 -8
  44. package/assets/agent-screen.png +0 -0
  45. package/assets/main-screen.png +0 -0
  46. package/assets/orche-clawd.png +0 -0
  47. package/dist/tui/App.js +0 -428
  48. package/dist/tui/App.js.map +0 -1
  49. package/dist/tui/AppView.js +0 -13
  50. package/dist/tui/AppView.js.map +0 -1
  51. package/dist/tui/Auditorium.js +0 -17
  52. package/dist/tui/Auditorium.js.map +0 -1
  53. package/dist/tui/ConcertHall.js +0 -11
  54. package/dist/tui/ConcertHall.js.map +0 -1
  55. package/dist/tui/Help.js +0 -49
  56. package/dist/tui/Help.js.map +0 -1
  57. package/dist/tui/OrchestratorPane.js +0 -34
  58. package/dist/tui/OrchestratorPane.js.map +0 -1
  59. package/dist/tui/SidebarHeader.js +0 -6
  60. package/dist/tui/SidebarHeader.js.map +0 -1
  61. package/dist/tui/StatusBar.js +0 -6
  62. package/dist/tui/StatusBar.js.map +0 -1
  63. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  64. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  65. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  66. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  67. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  68. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  69. package/plan-explorer-musician-hardening.md +0 -56
  70. package/src/claude-command.ts +0 -35
  71. package/src/claude-detect.ts +0 -42
  72. package/src/cli.ts +0 -197
  73. package/src/commands/attach.ts +0 -24
  74. package/src/commands/dashboard-window.ts +0 -33
  75. package/src/commands/kill.ts +0 -50
  76. package/src/commands/launch.ts +0 -134
  77. package/src/commands/list.ts +0 -43
  78. package/src/commands/mcp-server.ts +0 -18
  79. package/src/commands/notes.ts +0 -18
  80. package/src/commands/restore.ts +0 -153
  81. package/src/commands/tui.tsx +0 -22
  82. package/src/config.ts +0 -44
  83. package/src/dashboard.ts +0 -1
  84. package/src/mcp/config.ts +0 -39
  85. package/src/mcp/handlers.ts +0 -141
  86. package/src/mcp/server.ts +0 -50
  87. package/src/mcp/tool-defs.ts +0 -151
  88. package/src/musicians/dismiss.ts +0 -60
  89. package/src/musicians/ids.ts +0 -21
  90. package/src/musicians/lookup.ts +0 -13
  91. package/src/musicians/message-log.ts +0 -152
  92. package/src/musicians/message.ts +0 -99
  93. package/src/musicians/query.ts +0 -19
  94. package/src/musicians/spawn.ts +0 -139
  95. package/src/notes.ts +0 -39
  96. package/src/notify.ts +0 -62
  97. package/src/orchestrator/report-back.ts +0 -33
  98. package/src/permission.ts +0 -30
  99. package/src/project-key.ts +0 -12
  100. package/src/prompts/musician-role.ts +0 -22
  101. package/src/prompts/orchestrator-role.ts +0 -84
  102. package/src/prompts/tool-discipline.ts +0 -41
  103. package/src/repo.ts +0 -14
  104. package/src/shell-quote.ts +0 -7
  105. package/src/state-updaters.ts +0 -132
  106. package/src/state.ts +0 -49
  107. package/src/state.types.ts +0 -67
  108. package/src/tmux.ts +0 -226
  109. package/src/tui/activity-line.ts +0 -16
  110. package/src/tui/components/App.tsx +0 -534
  111. package/src/tui/components/AppView.tsx +0 -98
  112. package/src/tui/components/Auditorium.tsx +0 -56
  113. package/src/tui/components/ConcertHall.tsx +0 -31
  114. package/src/tui/components/Help.tsx +0 -63
  115. package/src/tui/components/OrchestratorPane.tsx +0 -98
  116. package/src/tui/components/SidebarHeader.tsx +0 -34
  117. package/src/tui/components/StatusBar.tsx +0 -42
  118. package/src/tui/detect-permission.ts +0 -93
  119. package/src/tui/embedded-session-lifecycle.ts +0 -44
  120. package/src/tui/embedded-terminal.ts +0 -325
  121. package/src/tui/format-time.ts +0 -25
  122. package/src/tui/keymap.ts +0 -104
  123. package/src/tui/poll-activity.ts +0 -25
  124. package/src/tui/poll-idle.ts +0 -149
  125. package/src/tui/poll-permission.ts +0 -50
  126. package/src/tui/status-icon.ts +0 -35
  127. package/src/tui/terminal-input.ts +0 -136
  128. package/src/tui/watch-state.ts +0 -43
  129. package/src/worktree.ts +0 -41
  130. package/tests/claude-command.test.ts +0 -30
  131. package/tests/claude-detect.test.ts +0 -14
  132. package/tests/commands/attach.test.ts +0 -60
  133. package/tests/commands/kill.test.ts +0 -66
  134. package/tests/commands/launch.test.ts +0 -75
  135. package/tests/commands/list.test.ts +0 -47
  136. package/tests/commands/notes.test.ts +0 -53
  137. package/tests/commands/restore.test.ts +0 -126
  138. package/tests/helpers/tmp-config.ts +0 -16
  139. package/tests/helpers/tmp-repo.ts +0 -29
  140. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  141. package/tests/mcp/handlers.test.ts +0 -163
  142. package/tests/mcp/tool-defs.test.ts +0 -35
  143. package/tests/musicians/dismiss.test.ts +0 -102
  144. package/tests/musicians/message.test.ts +0 -159
  145. package/tests/musicians/query.test.ts +0 -65
  146. package/tests/musicians/spawn.test.ts +0 -125
  147. package/tests/notes.test.ts +0 -56
  148. package/tests/notify.test.ts +0 -80
  149. package/tests/orchestrator/report-back.test.ts +0 -18
  150. package/tests/permission.test.ts +0 -39
  151. package/tests/project-key.test.ts +0 -33
  152. package/tests/prompts/tool-discipline.test.ts +0 -25
  153. package/tests/repo.test.ts +0 -38
  154. package/tests/state-updaters.test.ts +0 -126
  155. package/tests/state.test.ts +0 -85
  156. package/tests/tmux.test.ts +0 -126
  157. package/tests/tui/AppView.test.tsx +0 -92
  158. package/tests/tui/Auditorium.test.tsx +0 -67
  159. package/tests/tui/ConcertHall.test.tsx +0 -22
  160. package/tests/tui/Help.test.tsx +0 -38
  161. package/tests/tui/OrchestratorPane.test.ts +0 -30
  162. package/tests/tui/SidebarHeader.test.tsx +0 -20
  163. package/tests/tui/StatusBar.test.tsx +0 -51
  164. package/tests/tui/activity-line.test.ts +0 -21
  165. package/tests/tui/detect-permission.test.ts +0 -92
  166. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  167. package/tests/tui/embedded-terminal.test.ts +0 -80
  168. package/tests/tui/format-time.test.ts +0 -25
  169. package/tests/tui/keymap.test.ts +0 -93
  170. package/tests/tui/poll-activity.test.ts +0 -81
  171. package/tests/tui/poll-idle.test.ts +0 -159
  172. package/tests/tui/poll-permission.test.ts +0 -222
  173. package/tests/tui/status-icon.test.ts +0 -27
  174. package/tests/tui/terminal-input.test.ts +0 -113
  175. package/tests/tui/watch-state.test.ts +0 -54
  176. package/tests/worktree.test.ts +0 -73
  177. package/tsconfig.json +0 -19
  178. package/vitest.config.ts +0 -12
@@ -1,159 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { readFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { messageMusician } from '../../src/musicians/message.js';
5
- import { makeTmpRepo, type TmpRepo } from '../helpers/tmp-repo.js';
6
- import { makeTmpConfig } from '../helpers/tmp-config.js';
7
- import { ensureOrchestraDir, writeState, readState } from '../../src/state.js';
8
- import { makeInitialState } from '../../src/state.types.js';
9
- import { projectKeyFromPath } from '../../src/project-key.js';
10
- import { addMusician } from '../../src/state-updaters.js';
11
- import {
12
- createDetachedSession,
13
- sessionName,
14
- killSession,
15
- capturePane,
16
- } from '../../src/tmux.js';
17
- import { execa } from 'execa';
18
- import { messageLogsDir } from '../../src/config.js';
19
-
20
- describe('messageMusician', () => {
21
- const cleanups: Array<() => Promise<void>> = [];
22
- const sessionsToKill: string[] = [];
23
-
24
- beforeEach(() => { process.env.NFO_HOME = ''; });
25
-
26
- afterEach(async () => {
27
- for (const s of sessionsToKill) {
28
- try { await killSession(s); } catch { /* ignore */ }
29
- }
30
- sessionsToKill.length = 0;
31
- for (const c of cleanups) await c();
32
- cleanups.length = 0;
33
- delete process.env.NFO_HOME;
34
- });
35
-
36
- it('throws when musician is unknown', async () => {
37
- const cfg = await makeTmpConfig();
38
- cleanups.push(cfg.cleanup);
39
- process.env.NFO_HOME = cfg.path;
40
- const repo: TmpRepo = await makeTmpRepo();
41
- cleanups.push(repo.cleanup);
42
- const id = projectKeyFromPath(repo.path);
43
- await ensureOrchestraDir(id);
44
- await writeState(id, makeInitialState({
45
- orchestraId: id, projectPath: repo.path, permissionLevel: 'supervised',
46
- }));
47
-
48
- await expect(
49
- messageMusician({ orchestraId: id, musicianId: 'mus-999', message: 'hi' }),
50
- ).rejects.toThrow(/Unknown musician/);
51
- });
52
-
53
- it("sends keys + Enter immediately when the musician is idle", async () => {
54
- const cfg = await makeTmpConfig();
55
- cleanups.push(cfg.cleanup);
56
- process.env.NFO_HOME = cfg.path;
57
- const repo: TmpRepo = await makeTmpRepo();
58
- cleanups.push(repo.cleanup);
59
-
60
- const orchId = projectKeyFromPath(repo.path);
61
- await ensureOrchestraDir(orchId);
62
- await writeState(orchId, makeInitialState({
63
- orchestraId: orchId, projectPath: repo.path, permissionLevel: 'supervised',
64
- }));
65
-
66
- const sess = sessionName(orchId);
67
- sessionsToKill.push(sess);
68
- await createDetachedSession(sess, repo.path, 220, 50);
69
- const { stdout: winId } = await execa('tmux', [
70
- 'new-window', '-t', sess, '-n', 'mus-001-tester', '-c', repo.path, '-d',
71
- '-P', '-F', '#{window_id}',
72
- ]);
73
-
74
- await addMusician(orchId, {
75
- id: 'mus-001',
76
- name: 'tester',
77
- task_summary: 't',
78
- status: 'idle',
79
- tmux_window_id: winId.trim(),
80
- claude_session_id: null,
81
- worktree_path: null,
82
- branch: null,
83
- spawned_at: '2026-05-29T10:00:00Z',
84
- last_activity: '2026-05-29T10:00:00Z',
85
- });
86
-
87
- const result = await messageMusician({
88
- orchestraId: orchId,
89
- musicianId: 'mus-001',
90
- message: 'echo nfo-message-test',
91
- });
92
- await new Promise(r => setTimeout(r, 250));
93
- const out = await capturePane(`${sess}:${winId.trim()}`, 20);
94
- expect(out).toContain('nfo-message-test');
95
- expect(result.delivery).toBe('immediate');
96
- expect(result.pending_messages).toBe(0);
97
-
98
- const state = await readState(orchId);
99
- expect(state!.musicians[0].last_activity).not.toBe('2026-05-29T10:00:00Z');
100
- expect(state!.musicians[0].status).toBe('working');
101
-
102
- const log = await readFile(join(messageLogsDir(orchId), 'mus-001.jsonl'), 'utf8');
103
- expect(log).toContain('"type":"message_queued"');
104
- expect(log).toContain('"type":"message_delivered"');
105
- });
106
-
107
- it('queues follow-up work when the musician is still working', async () => {
108
- const cfg = await makeTmpConfig();
109
- cleanups.push(cfg.cleanup);
110
- process.env.NFO_HOME = cfg.path;
111
- const repo: TmpRepo = await makeTmpRepo();
112
- cleanups.push(repo.cleanup);
113
-
114
- const orchId = projectKeyFromPath(repo.path);
115
- await ensureOrchestraDir(orchId);
116
- await writeState(orchId, makeInitialState({
117
- orchestraId: orchId, projectPath: repo.path, permissionLevel: 'supervised',
118
- }));
119
-
120
- const sess = sessionName(orchId);
121
- sessionsToKill.push(sess);
122
- await createDetachedSession(sess, repo.path, 220, 50);
123
- const { stdout: winId } = await execa('tmux', [
124
- 'new-window', '-t', sess, '-n', 'mus-001-tester', '-c', repo.path, '-d',
125
- '-P', '-F', '#{window_id}',
126
- ]);
127
-
128
- await addMusician(orchId, {
129
- id: 'mus-001',
130
- name: 'tester',
131
- task_summary: 't',
132
- status: 'working',
133
- tmux_window_id: winId.trim(),
134
- claude_session_id: null,
135
- worktree_path: null,
136
- branch: null,
137
- spawned_at: '2026-05-29T10:00:00Z',
138
- last_activity: '2026-05-29T10:00:00Z',
139
- });
140
-
141
- const result = await messageMusician({
142
- orchestraId: orchId,
143
- musicianId: 'mus-001',
144
- message: 'echo should-not-run-yet',
145
- });
146
- await new Promise(r => setTimeout(r, 250));
147
- const out = await capturePane(`${sess}:${winId.trim()}`, 20);
148
- expect(out).not.toContain('should-not-run-yet');
149
- expect(result.delivery).toBe('queued');
150
- expect(result.pending_messages).toBe(1);
151
-
152
- const state = await readState(orchId);
153
- expect(state!.musicians[0].last_activity).toBe('2026-05-29T10:00:00Z');
154
-
155
- const log = await readFile(join(messageLogsDir(orchId), 'mus-001.jsonl'), 'utf8');
156
- expect(log).toContain('"type":"message_queued"');
157
- expect(log).not.toContain('"type":"message_delivered"');
158
- });
159
- });
@@ -1,65 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { queryMusician } from '../../src/musicians/query.js';
3
- import { makeTmpRepo, type TmpRepo } from '../helpers/tmp-repo.js';
4
- import { makeTmpConfig } from '../helpers/tmp-config.js';
5
- import { ensureOrchestraDir, writeState } from '../../src/state.js';
6
- import { makeInitialState } from '../../src/state.types.js';
7
- import { projectKeyFromPath } from '../../src/project-key.js';
8
- import { addMusician } from '../../src/state-updaters.js';
9
- import { createDetachedSession, sessionName, killSession, sendKeys } from '../../src/tmux.js';
10
- import { execa } from 'execa';
11
-
12
- describe('queryMusician', () => {
13
- const cleanups: Array<() => Promise<void>> = [];
14
- const sessionsToKill: string[] = [];
15
-
16
- beforeEach(() => { process.env.NFO_HOME = ''; });
17
-
18
- afterEach(async () => {
19
- for (const s of sessionsToKill) {
20
- try { await killSession(s); } catch { /* ignore */ }
21
- }
22
- sessionsToKill.length = 0;
23
- for (const c of cleanups) await c();
24
- cleanups.length = 0;
25
- delete process.env.NFO_HOME;
26
- });
27
-
28
- it('returns the visible content of the musician pane', async () => {
29
- const cfg = await makeTmpConfig();
30
- cleanups.push(cfg.cleanup);
31
- process.env.NFO_HOME = cfg.path;
32
- const repo: TmpRepo = await makeTmpRepo();
33
- cleanups.push(repo.cleanup);
34
-
35
- const orchId = projectKeyFromPath(repo.path);
36
- await ensureOrchestraDir(orchId);
37
- await writeState(orchId, makeInitialState({
38
- orchestraId: orchId, projectPath: repo.path, permissionLevel: 'supervised',
39
- }));
40
-
41
- const sess = sessionName(orchId);
42
- sessionsToKill.push(sess);
43
- await createDetachedSession(sess, repo.path, 220, 50);
44
- const { stdout: winId } = await execa('tmux', [
45
- 'new-window', '-t', sess, '-n', 'mus-001-q', '-c', repo.path, '-d',
46
- '-P', '-F', '#{window_id}',
47
- ]);
48
- await addMusician(orchId, baseMus('mus-001', winId.trim()));
49
-
50
- await sendKeys(`${sess}:${winId.trim()}`, 'echo nfo-query-marker', true);
51
- await new Promise(r => setTimeout(r, 250));
52
-
53
- const out = await queryMusician({ orchestraId: orchId, musicianId: 'mus-001' });
54
- expect(out).toContain('nfo-query-marker');
55
- });
56
- });
57
-
58
- function baseMus(id: string, winId: string) {
59
- return {
60
- id, name: 'q', task_summary: 't', status: 'working' as const,
61
- tmux_window_id: winId, claude_session_id: null, worktree_path: null,
62
- branch: null,
63
- spawned_at: '2026-05-29T10:00:00Z', last_activity: '2026-05-29T10:00:00Z',
64
- };
65
- }
@@ -1,125 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { createMusician } from '../../src/musicians/spawn.js';
3
- import { makeTmpRepo, type TmpRepo } from '../helpers/tmp-repo.js';
4
- import { makeTmpConfig } from '../helpers/tmp-config.js';
5
- import { readState, ensureOrchestraDir, writeState } from '../../src/state.js';
6
- import { makeInitialState } from '../../src/state.types.js';
7
- import { projectKeyFromPath } from '../../src/project-key.js';
8
- import {
9
- createDetachedSession,
10
- sessionName,
11
- killSession,
12
- sessionExists,
13
- } from '../../src/tmux.js';
14
- import { existsSync } from 'node:fs';
15
- import { readFile } from 'node:fs/promises';
16
- import { orchestraDir } from '../../src/config.js';
17
- import { MUSICIAN_ROLE_PROMPT_V1 } from '../../src/prompts/musician-role.js';
18
- import { musicianMcpConfigPath } from '../../src/mcp/config.js';
19
-
20
- describe('createMusician', () => {
21
- const cleanups: Array<() => Promise<void>> = [];
22
- const sessionsToKill: string[] = [];
23
-
24
- beforeEach(() => { process.env.NFO_HOME = ''; });
25
-
26
- afterEach(async () => {
27
- for (const s of sessionsToKill) {
28
- try { await killSession(s); } catch { /* ignore */ }
29
- }
30
- sessionsToKill.length = 0;
31
- for (const c of cleanups) await c();
32
- cleanups.length = 0;
33
- delete process.env.NFO_HOME;
34
- });
35
-
36
- it('creates a worktree, a tmux window, and a state.json entry', async () => {
37
- const repo: TmpRepo = await makeTmpRepo();
38
- cleanups.push(repo.cleanup);
39
- const cfg = await makeTmpConfig();
40
- cleanups.push(cfg.cleanup);
41
- process.env.NFO_HOME = cfg.path;
42
-
43
- const orchestraId = projectKeyFromPath(repo.path);
44
- await ensureOrchestraDir(orchestraId);
45
- await writeState(orchestraId, makeInitialState({
46
- orchestraId, projectPath: repo.path, permissionLevel: 'supervised',
47
- }));
48
-
49
- const name = sessionName(orchestraId);
50
- sessionsToKill.push(name);
51
- await createDetachedSession(name, repo.path, 220, 50);
52
-
53
- const result = await createMusician({
54
- orchestraId,
55
- name: 'tester',
56
- task: 'run the test suite',
57
- model: 'haiku',
58
- dryRun: true,
59
- });
60
-
61
- expect(result.musician_id).toMatch(/^mus-\d{3}$/);
62
- expect(result.worktree_path).not.toBeNull();
63
- if (result.worktree_path) {
64
- expect(existsSync(result.worktree_path)).toBe(true);
65
- }
66
-
67
- const state = await readState(orchestraId);
68
- expect(state!.musicians).toHaveLength(1);
69
- expect(state!.musicians[0].name).toBe('tester');
70
- expect(state!.musicians[0].task_summary).toBe('run the test suite');
71
- expect(state!.musicians[0].status).toBe('working');
72
- expect(state!.musicians[0].model).toBe('haiku');
73
-
74
- const promptFile = await readFile(
75
- `${orchestraDir(orchestraId)}/musician-${result.musician_id}-prompt.md`,
76
- 'utf8',
77
- );
78
- expect(promptFile).toBe(MUSICIAN_ROLE_PROMPT_V1);
79
-
80
- const mcpConfig = JSON.parse(await readFile(
81
- musicianMcpConfigPath(orchestraId, result.musician_id),
82
- 'utf8',
83
- ));
84
- expect(mcpConfig.mcpServers.nfo.args).toEqual([
85
- 'mcp-server',
86
- '--orchestra-id',
87
- orchestraId,
88
- '--caller-musician-id',
89
- result.musician_id,
90
- ]);
91
- });
92
-
93
- it('honours worktree=false (no worktree, runs in repo root)', async () => {
94
- const repo: TmpRepo = await makeTmpRepo();
95
- cleanups.push(repo.cleanup);
96
- const cfg = await makeTmpConfig();
97
- cleanups.push(cfg.cleanup);
98
- process.env.NFO_HOME = cfg.path;
99
-
100
- const orchestraId = projectKeyFromPath(repo.path);
101
- await ensureOrchestraDir(orchestraId);
102
- await writeState(orchestraId, makeInitialState({
103
- orchestraId, projectPath: repo.path, permissionLevel: 'supervised',
104
- }));
105
-
106
- const name = sessionName(orchestraId);
107
- sessionsToKill.push(name);
108
- await createDetachedSession(name, repo.path, 220, 50);
109
-
110
- const result = await createMusician({
111
- orchestraId,
112
- name: 'doc-writer',
113
- task: 'update README',
114
- worktree: false,
115
- model: 'sonnet',
116
- dryRun: true,
117
- });
118
-
119
- expect(result.worktree_path).toBeNull();
120
- const state = await readState(orchestraId);
121
- expect(state!.musicians[0].worktree_path).toBeNull();
122
- expect(state!.musicians[0].branch).toBeNull();
123
- expect(state!.musicians[0].model).toBe('sonnet');
124
- });
125
- });
@@ -1,56 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { noteRead, noteWrite, noteList } from '../src/notes.js';
3
- import { ensureOrchestraDir } from '../src/state.js';
4
- import { makeTmpConfig } from './helpers/tmp-config.js';
5
-
6
- describe('notes', () => {
7
- const cleanups: Array<() => Promise<void>> = [];
8
-
9
- beforeEach(() => { process.env.NFO_HOME = ''; });
10
- afterEach(async () => {
11
- for (const c of cleanups) await c();
12
- cleanups.length = 0;
13
- delete process.env.NFO_HOME;
14
- });
15
-
16
- it('noteWrite then noteRead returns the written content', async () => {
17
- const tmp = await makeTmpConfig();
18
- cleanups.push(tmp.cleanup);
19
- process.env.NFO_HOME = tmp.path;
20
- await ensureOrchestraDir('orch-a');
21
-
22
- await noteWrite('orch-a', 'overview.md', '# Project overview\n');
23
- const back = await noteRead('orch-a', 'overview.md');
24
- expect(back).toBe('# Project overview\n');
25
- });
26
-
27
- it('noteRead returns empty string for missing notes', async () => {
28
- const tmp = await makeTmpConfig();
29
- cleanups.push(tmp.cleanup);
30
- process.env.NFO_HOME = tmp.path;
31
- await ensureOrchestraDir('orch-b');
32
-
33
- expect(await noteRead('orch-b', 'nope.md')).toBe('');
34
- });
35
-
36
- it('noteList returns all markdown filenames in notes/', async () => {
37
- const tmp = await makeTmpConfig();
38
- cleanups.push(tmp.cleanup);
39
- process.env.NFO_HOME = tmp.path;
40
- await ensureOrchestraDir('orch-c');
41
- await noteWrite('orch-c', 'overview.md', 'a');
42
- await noteWrite('orch-c', 'decisions.md', 'b');
43
-
44
- const list = await noteList('orch-c');
45
- expect(list.sort()).toEqual(['decisions.md', 'overview.md']);
46
- });
47
-
48
- it('rejects filenames containing path separators', async () => {
49
- const tmp = await makeTmpConfig();
50
- cleanups.push(tmp.cleanup);
51
- process.env.NFO_HOME = tmp.path;
52
- await ensureOrchestraDir('orch-d');
53
- await expect(noteWrite('orch-d', '../escape.md', 'pwn')).rejects.toThrow(/invalid filename/i);
54
- await expect(noteRead('orch-d', '../escape.md')).rejects.toThrow(/invalid filename/i);
55
- });
56
- });
@@ -1,80 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { notifyAwaitingPermission } from '../src/notify.js';
3
-
4
- describe('notifyAwaitingPermission', () => {
5
- it('writes a BEL character to the bell sink', async () => {
6
- const bell = vi.fn();
7
- const spawn = vi.fn().mockResolvedValue(undefined);
8
- await notifyAwaitingPermission({
9
- pendingCount: 1,
10
- platform: 'linux',
11
- bell,
12
- spawn,
13
- });
14
- expect(bell).toHaveBeenCalledTimes(1);
15
- expect(bell).toHaveBeenCalledWith('\x07');
16
- });
17
-
18
- it('on linux, spawns notify-send with NFO title and count message', async () => {
19
- const spawn = vi.fn().mockResolvedValue(undefined);
20
- await notifyAwaitingPermission({
21
- pendingCount: 2,
22
- platform: 'linux',
23
- bell: vi.fn(),
24
- spawn,
25
- });
26
- expect(spawn).toHaveBeenCalledTimes(1);
27
- const [bin, args] = spawn.mock.calls[0];
28
- expect(bin).toBe('notify-send');
29
- expect(args).toEqual(['NFO', '2 musicians awaiting permission']);
30
- });
31
-
32
- it('on darwin, spawns osascript with display notification AppleScript', async () => {
33
- const spawn = vi.fn().mockResolvedValue(undefined);
34
- await notifyAwaitingPermission({
35
- pendingCount: 1,
36
- platform: 'darwin',
37
- bell: vi.fn(),
38
- spawn,
39
- });
40
- expect(spawn).toHaveBeenCalledTimes(1);
41
- const [bin, args] = spawn.mock.calls[0];
42
- expect(bin).toBe('osascript');
43
- expect(args.length).toBe(2);
44
- expect(args[0]).toBe('-e');
45
- expect(args[1]).toContain('display notification');
46
- expect(args[1]).toContain('1 musician awaiting permission');
47
- expect(args[1]).toContain('NFO');
48
- });
49
-
50
- it('on unknown platform, fires bell only (no spawn)', async () => {
51
- const spawn = vi.fn();
52
- await notifyAwaitingPermission({
53
- pendingCount: 1,
54
- platform: 'win32',
55
- bell: vi.fn(),
56
- spawn,
57
- });
58
- expect(spawn).not.toHaveBeenCalled();
59
- });
60
-
61
- it('swallows spawn errors silently', async () => {
62
- const spawn = vi.fn().mockRejectedValue(new Error('notify-send not installed'));
63
- await expect(notifyAwaitingPermission({
64
- pendingCount: 1,
65
- platform: 'linux',
66
- bell: vi.fn(),
67
- spawn,
68
- })).resolves.toBeUndefined();
69
- });
70
-
71
- it('uses singular noun for count=1, plural otherwise', async () => {
72
- const spawn1 = vi.fn().mockResolvedValue(undefined);
73
- await notifyAwaitingPermission({ pendingCount: 1, platform: 'linux', bell: vi.fn(), spawn: spawn1 });
74
- expect(spawn1.mock.calls[0][1]).toEqual(['NFO', '1 musician awaiting permission']);
75
-
76
- const spawnN = vi.fn().mockResolvedValue(undefined);
77
- await notifyAwaitingPermission({ pendingCount: 3, platform: 'linux', bell: vi.fn(), spawn: spawnN });
78
- expect(spawnN.mock.calls[0][1]).toEqual(['NFO', '3 musicians awaiting permission']);
79
- });
80
- });
@@ -1,18 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { formatMusicianDonePrompt } from '../../src/orchestrator/report-back.js';
3
-
4
- describe('formatMusicianDonePrompt', () => {
5
- it('requires a tool call instead of a prose acknowledgement', () => {
6
- const prompt = formatMusicianDonePrompt({
7
- musicianId: 'mus-007',
8
- musicianName: 'fixer',
9
- summary: 'Patched the failing assertion',
10
- nextSteps: 'Ask me to add one more regression test if needed.',
11
- });
12
-
13
- expect(prompt).toContain('Resolve this now with an NFO tool call only:');
14
- expect(prompt).toContain('dismiss_musician');
15
- expect(prompt).toContain('message_musician');
16
- expect(prompt).toContain('A plain-text acknowledgement is invalid here.');
17
- });
18
- });
@@ -1,39 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- PERMISSION_LEVELS,
4
- claudeFlagsForLevel,
5
- isPermissionLevel,
6
- type PermissionLevel,
7
- } from '../src/permission.js';
8
-
9
- describe('permission levels', () => {
10
- it('lists all five levels in order from most to least permissive', () => {
11
- expect(PERMISSION_LEVELS).toEqual([
12
- 'dangerouslySkipPermissions',
13
- 'auto',
14
- 'acceptEdits',
15
- 'supervised',
16
- 'strict',
17
- ]);
18
- });
19
-
20
- it('isPermissionLevel rejects unknown strings', () => {
21
- expect(isPermissionLevel('dangerouslySkipPermissions')).toBe(true);
22
- expect(isPermissionLevel('auto')).toBe(true);
23
- expect(isPermissionLevel('acceptEdits')).toBe(true);
24
- expect(isPermissionLevel('supervised')).toBe(true);
25
- expect(isPermissionLevel('strict')).toBe(true);
26
- expect(isPermissionLevel('YOLO')).toBe(false);
27
- expect(isPermissionLevel('')).toBe(false);
28
- });
29
-
30
- it('claudeFlagsForLevel returns the right flag list per level', () => {
31
- expect(claudeFlagsForLevel('dangerouslySkipPermissions')).toEqual([
32
- '--dangerously-skip-permissions',
33
- ]);
34
- expect(claudeFlagsForLevel('auto')).toEqual(['--permission-mode', 'auto']);
35
- expect(claudeFlagsForLevel('acceptEdits')).toEqual(['--permission-mode', 'acceptEdits']);
36
- expect(claudeFlagsForLevel('supervised')).toEqual(['--permission-mode', 'default']);
37
- expect(claudeFlagsForLevel('strict')).toEqual(['--permission-mode', 'plan']);
38
- });
39
- });
@@ -1,33 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { projectKeyFromPath } from '../src/project-key.js';
3
-
4
- describe('projectKeyFromPath', () => {
5
- it('produces a stable key for a given absolute path', () => {
6
- const key1 = projectKeyFromPath('/home/user/projects/myrepo');
7
- const key2 = projectKeyFromPath('/home/user/projects/myrepo');
8
- expect(key1).toBe(key2);
9
- });
10
-
11
- it('includes the basename as a readable suffix', () => {
12
- const key = projectKeyFromPath('/home/user/projects/myrepo');
13
- expect(key.endsWith('-myrepo')).toBe(true);
14
- });
15
-
16
- it('produces a 10-char sha1 prefix', () => {
17
- const key = projectKeyFromPath('/home/user/projects/myrepo');
18
- const prefix = key.split('-')[0];
19
- expect(prefix).toHaveLength(10);
20
- expect(prefix).toMatch(/^[0-9a-f]{10}$/);
21
- });
22
-
23
- it('produces different keys for different paths', () => {
24
- const a = projectKeyFromPath('/home/user/projects/foo');
25
- const b = projectKeyFromPath('/home/user/projects/bar');
26
- expect(a).not.toBe(b);
27
- });
28
-
29
- it('sanitizes non-alphanumeric basename characters', () => {
30
- const key = projectKeyFromPath('/tmp/my repo with spaces');
31
- expect(key).toMatch(/^[0-9a-f]{10}-[a-z0-9-]+$/);
32
- });
33
- });
@@ -1,25 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { ORCHESTRATOR_ROLE_PROMPT_V1 } from '../../src/prompts/orchestrator-role.js';
3
- import { MUSICIAN_ROLE_PROMPT_V1 } from '../../src/prompts/musician-role.js';
4
- import { buildMusicianInitialPrompt } from '../../src/prompts/tool-discipline.js';
5
-
6
- describe('tool-discipline prompts', () => {
7
- it('requires the orchestrator to resolve coordination via NFO tools', () => {
8
- expect(ORCHESTRATOR_ROLE_PROMPT_V1).toContain('Tool discipline (mandatory):');
9
- expect(ORCHESTRATOR_ROLE_PROMPT_V1).toContain('Call the corresponding NFO tool in the same turn.');
10
- expect(ORCHESTRATOR_ROLE_PROMPT_V1).toContain('A prose-only');
11
- });
12
-
13
- it('requires musicians to use report_done instead of plain text', () => {
14
- expect(MUSICIAN_ROLE_PROMPT_V1).toContain('Plain-text status reports are');
15
- expect(MUSICIAN_ROLE_PROMPT_V1).toContain('next action must be `report_done');
16
- expect(MUSICIAN_ROLE_PROMPT_V1).toContain('Do not end with "done"');
17
- });
18
-
19
- it('injects the tool contract into the initial musician task prompt', () => {
20
- const prompt = buildMusicianInitialPrompt('Run the failing test and fix it.');
21
- expect(prompt).toContain('Run the failing test and fix it.');
22
- expect(prompt).toContain('NFO operating contract (mandatory):');
23
- expect(prompt).toContain('instead of replying with a plain-text completion message');
24
- });
25
- });
@@ -1,38 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { resolveRepoRoot } from '../src/repo.js';
3
- import { makeTmpRepo, makeTmpNonRepo, type TmpRepo } from './helpers/tmp-repo.js';
4
- import { mkdir } from 'node:fs/promises';
5
- import { join } from 'node:path';
6
-
7
- describe('resolveRepoRoot', () => {
8
- const cleanups: Array<() => Promise<void>> = [];
9
- afterEach(async () => {
10
- for (const c of cleanups) await c();
11
- cleanups.length = 0;
12
- });
13
-
14
- async function track(t: TmpRepo) {
15
- cleanups.push(t.cleanup);
16
- return t;
17
- }
18
-
19
- it('returns the repo root when invoked from inside a repo', async () => {
20
- const repo = await track(await makeTmpRepo());
21
- const result = await resolveRepoRoot(repo.path);
22
- expect(result).toBe(repo.path);
23
- });
24
-
25
- it('returns the repo root when invoked from a subdirectory', async () => {
26
- const repo = await track(await makeTmpRepo());
27
- const subdir = join(repo.path, 'src', 'nested');
28
- await mkdir(subdir, { recursive: true });
29
- const result = await resolveRepoRoot(subdir);
30
- expect(result).toBe(repo.path);
31
- });
32
-
33
- it('returns null when invoked outside any repo', async () => {
34
- const nonRepo = await track(await makeTmpNonRepo());
35
- const result = await resolveRepoRoot(nonRepo.path);
36
- expect(result).toBeNull();
37
- });
38
- });