nfo-cli 0.0.4-improve-prompting → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) 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 +0 -0
  6. package/dist/mcp/handlers.js +5 -0
  7. package/dist/mcp/handlers.js.map +1 -1
  8. package/dist/mcp/tool-defs.js +10 -0
  9. package/dist/mcp/tool-defs.js.map +1 -1
  10. package/dist/musicians/dismiss.js +1 -1
  11. package/dist/musicians/dismiss.js.map +1 -1
  12. package/dist/musicians/roles.js +15 -0
  13. package/dist/musicians/roles.js.map +1 -0
  14. package/dist/musicians/spawn.js +53 -18
  15. package/dist/musicians/spawn.js.map +1 -1
  16. package/dist/permission.js +6 -0
  17. package/dist/permission.js.map +1 -1
  18. package/dist/prompts/musician-role.js +2 -1
  19. package/dist/prompts/musician-role.js.map +1 -1
  20. package/dist/prompts/orchestrator-role.js +18 -6
  21. package/dist/prompts/orchestrator-role.js.map +1 -1
  22. package/dist/prompts/tool-discipline.js +7 -3
  23. package/dist/prompts/tool-discipline.js.map +1 -1
  24. package/package.json +8 -1
  25. package/assets/agent-screen.png +0 -0
  26. package/assets/main-screen.png +0 -0
  27. package/assets/orche-clawd.png +0 -0
  28. package/dist/tui/App.js +0 -428
  29. package/dist/tui/App.js.map +0 -1
  30. package/dist/tui/AppView.js +0 -13
  31. package/dist/tui/AppView.js.map +0 -1
  32. package/dist/tui/Auditorium.js +0 -17
  33. package/dist/tui/Auditorium.js.map +0 -1
  34. package/dist/tui/ConcertHall.js +0 -11
  35. package/dist/tui/ConcertHall.js.map +0 -1
  36. package/dist/tui/Help.js +0 -49
  37. package/dist/tui/Help.js.map +0 -1
  38. package/dist/tui/OrchestratorPane.js +0 -34
  39. package/dist/tui/OrchestratorPane.js.map +0 -1
  40. package/dist/tui/SidebarHeader.js +0 -6
  41. package/dist/tui/SidebarHeader.js.map +0 -1
  42. package/dist/tui/StatusBar.js +0 -6
  43. package/dist/tui/StatusBar.js.map +0 -1
  44. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  45. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  46. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  47. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  48. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  49. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  50. package/plan-explorer-musician-hardening.md +0 -56
  51. package/src/claude-command.ts +0 -35
  52. package/src/claude-detect.ts +0 -42
  53. package/src/cli.ts +0 -197
  54. package/src/commands/attach.ts +0 -24
  55. package/src/commands/dashboard-window.ts +0 -33
  56. package/src/commands/kill.ts +0 -50
  57. package/src/commands/launch.ts +0 -134
  58. package/src/commands/list.ts +0 -43
  59. package/src/commands/mcp-server.ts +0 -18
  60. package/src/commands/notes.ts +0 -18
  61. package/src/commands/restore.ts +0 -153
  62. package/src/commands/tui.tsx +0 -22
  63. package/src/config.ts +0 -44
  64. package/src/dashboard.ts +0 -1
  65. package/src/mcp/config.ts +0 -39
  66. package/src/mcp/handlers.ts +0 -141
  67. package/src/mcp/server.ts +0 -50
  68. package/src/mcp/tool-defs.ts +0 -151
  69. package/src/musicians/dismiss.ts +0 -60
  70. package/src/musicians/ids.ts +0 -21
  71. package/src/musicians/lookup.ts +0 -13
  72. package/src/musicians/message-log.ts +0 -152
  73. package/src/musicians/message.ts +0 -99
  74. package/src/musicians/query.ts +0 -19
  75. package/src/musicians/spawn.ts +0 -139
  76. package/src/notes.ts +0 -39
  77. package/src/notify.ts +0 -62
  78. package/src/orchestrator/report-back.ts +0 -33
  79. package/src/permission.ts +0 -30
  80. package/src/project-key.ts +0 -12
  81. package/src/prompts/musician-role.ts +0 -22
  82. package/src/prompts/orchestrator-role.ts +0 -84
  83. package/src/prompts/tool-discipline.ts +0 -41
  84. package/src/repo.ts +0 -14
  85. package/src/shell-quote.ts +0 -7
  86. package/src/state-updaters.ts +0 -132
  87. package/src/state.ts +0 -49
  88. package/src/state.types.ts +0 -67
  89. package/src/tmux.ts +0 -226
  90. package/src/tui/activity-line.ts +0 -16
  91. package/src/tui/components/App.tsx +0 -534
  92. package/src/tui/components/AppView.tsx +0 -98
  93. package/src/tui/components/Auditorium.tsx +0 -56
  94. package/src/tui/components/ConcertHall.tsx +0 -31
  95. package/src/tui/components/Help.tsx +0 -63
  96. package/src/tui/components/OrchestratorPane.tsx +0 -98
  97. package/src/tui/components/SidebarHeader.tsx +0 -34
  98. package/src/tui/components/StatusBar.tsx +0 -42
  99. package/src/tui/detect-permission.ts +0 -93
  100. package/src/tui/embedded-session-lifecycle.ts +0 -44
  101. package/src/tui/embedded-terminal.ts +0 -325
  102. package/src/tui/format-time.ts +0 -25
  103. package/src/tui/keymap.ts +0 -104
  104. package/src/tui/poll-activity.ts +0 -25
  105. package/src/tui/poll-idle.ts +0 -149
  106. package/src/tui/poll-permission.ts +0 -50
  107. package/src/tui/status-icon.ts +0 -35
  108. package/src/tui/terminal-input.ts +0 -136
  109. package/src/tui/watch-state.ts +0 -43
  110. package/src/worktree.ts +0 -41
  111. package/tests/claude-command.test.ts +0 -30
  112. package/tests/claude-detect.test.ts +0 -14
  113. package/tests/commands/attach.test.ts +0 -60
  114. package/tests/commands/kill.test.ts +0 -66
  115. package/tests/commands/launch.test.ts +0 -75
  116. package/tests/commands/list.test.ts +0 -47
  117. package/tests/commands/notes.test.ts +0 -53
  118. package/tests/commands/restore.test.ts +0 -126
  119. package/tests/helpers/tmp-config.ts +0 -16
  120. package/tests/helpers/tmp-repo.ts +0 -29
  121. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  122. package/tests/mcp/handlers.test.ts +0 -163
  123. package/tests/mcp/tool-defs.test.ts +0 -35
  124. package/tests/musicians/dismiss.test.ts +0 -102
  125. package/tests/musicians/message.test.ts +0 -159
  126. package/tests/musicians/query.test.ts +0 -65
  127. package/tests/musicians/spawn.test.ts +0 -125
  128. package/tests/notes.test.ts +0 -56
  129. package/tests/notify.test.ts +0 -80
  130. package/tests/orchestrator/report-back.test.ts +0 -18
  131. package/tests/permission.test.ts +0 -39
  132. package/tests/project-key.test.ts +0 -33
  133. package/tests/prompts/tool-discipline.test.ts +0 -25
  134. package/tests/repo.test.ts +0 -38
  135. package/tests/state-updaters.test.ts +0 -126
  136. package/tests/state.test.ts +0 -85
  137. package/tests/tmux.test.ts +0 -126
  138. package/tests/tui/AppView.test.tsx +0 -92
  139. package/tests/tui/Auditorium.test.tsx +0 -67
  140. package/tests/tui/ConcertHall.test.tsx +0 -22
  141. package/tests/tui/Help.test.tsx +0 -38
  142. package/tests/tui/OrchestratorPane.test.ts +0 -30
  143. package/tests/tui/SidebarHeader.test.tsx +0 -20
  144. package/tests/tui/StatusBar.test.tsx +0 -51
  145. package/tests/tui/activity-line.test.ts +0 -21
  146. package/tests/tui/detect-permission.test.ts +0 -92
  147. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  148. package/tests/tui/embedded-terminal.test.ts +0 -80
  149. package/tests/tui/format-time.test.ts +0 -25
  150. package/tests/tui/keymap.test.ts +0 -93
  151. package/tests/tui/poll-activity.test.ts +0 -81
  152. package/tests/tui/poll-idle.test.ts +0 -159
  153. package/tests/tui/poll-permission.test.ts +0 -222
  154. package/tests/tui/status-icon.test.ts +0 -27
  155. package/tests/tui/terminal-input.test.ts +0 -113
  156. package/tests/tui/watch-state.test.ts +0 -54
  157. package/tests/worktree.test.ts +0 -73
  158. package/tsconfig.json +0 -19
  159. 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
- });