@iservu-inc/adf-cli 0.17.1 → 0.18.0

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 (45) hide show
  1. package/.project/chats/current/SESSION-STATUS.md +29 -27
  2. package/.project/docs/ROADMAP.md +74 -64
  3. package/CHANGELOG.md +78 -0
  4. package/CLAUDE.md +1 -1
  5. package/README.md +63 -27
  6. package/bin/adf.js +54 -0
  7. package/lib/analysis/dynamic-pipeline.js +26 -0
  8. package/lib/analysis/knowledge-graph.js +66 -0
  9. package/lib/commands/deploy.js +35 -0
  10. package/lib/commands/harness.js +345 -0
  11. package/lib/commands/init.js +135 -10
  12. package/lib/frameworks/interviewer.js +130 -0
  13. package/lib/frameworks/progress-tracker.js +30 -1
  14. package/lib/frameworks/session-manager.js +76 -0
  15. package/lib/harness/context-window-manager.js +255 -0
  16. package/lib/harness/event-logger.js +115 -0
  17. package/lib/harness/feature-manifest.js +175 -0
  18. package/lib/harness/headless-adapter.js +184 -0
  19. package/lib/harness/milestone-tracker.js +183 -0
  20. package/lib/harness/protocol.js +503 -0
  21. package/lib/harness/provider-bridge.js +226 -0
  22. package/lib/harness/run-manager.js +267 -0
  23. package/lib/templates/scripts/analyze-docs.js +12 -1
  24. package/lib/utils/context-extractor.js +48 -0
  25. package/lib/utils/framework-detector.js +10 -1
  26. package/lib/utils/project-detector.js +5 -1
  27. package/lib/utils/tool-detector.js +167 -0
  28. package/lib/utils/tool-feature-registry.js +82 -13
  29. package/lib/utils/tool-recommender.js +325 -0
  30. package/package.json +1 -1
  31. package/tests/context-extractor.test.js +45 -0
  32. package/tests/framework-detector.test.js +28 -0
  33. package/tests/harness-backward-compat.test.js +251 -0
  34. package/tests/harness-context-window.test.js +310 -0
  35. package/tests/harness-event-logger.test.js +148 -0
  36. package/tests/harness-feature-manifest.test.js +124 -0
  37. package/tests/harness-headless-adapter.test.js +196 -0
  38. package/tests/harness-integration.test.js +207 -0
  39. package/tests/harness-milestone-tracker.test.js +158 -0
  40. package/tests/harness-protocol.test.js +341 -0
  41. package/tests/harness-provider-bridge.test.js +180 -0
  42. package/tests/harness-provider-switch.test.js +204 -0
  43. package/tests/harness-run-manager.test.js +131 -0
  44. package/tests/tool-detector.test.js +152 -0
  45. package/tests/tool-recommender.test.js +218 -0
@@ -0,0 +1,148 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const EventLogger = require('../lib/harness/event-logger');
4
+ const { Event } = require('../lib/harness/protocol');
5
+
6
+ describe('EventLogger', () => {
7
+ const tempDir = path.join(__dirname, 'temp-event-logger-test');
8
+ const eventsFile = path.join(tempDir, '_events.jsonl');
9
+
10
+ beforeEach(async () => {
11
+ await fs.ensureDir(tempDir);
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await fs.remove(tempDir);
16
+ });
17
+
18
+ test('should create logger for run directory', () => {
19
+ const logger = EventLogger.forRun(tempDir);
20
+ expect(logger.filePath).toBe(eventsFile);
21
+ });
22
+
23
+ test('should log and read events', async () => {
24
+ const logger = new EventLogger(eventsFile);
25
+
26
+ await logger.log({ type: 'answer', runId: 'run_1', data: { questionId: 'q1' } });
27
+ await logger.log({ type: 'skip', runId: 'run_1', data: { questionId: 'q2' } });
28
+
29
+ const events = await logger.readAll();
30
+ expect(events.length).toBe(2);
31
+ expect(events[0].type).toBe('answer');
32
+ expect(events[1].type).toBe('skip');
33
+ });
34
+
35
+ test('should accept Event instances', async () => {
36
+ const logger = new EventLogger(eventsFile);
37
+ const event = new Event({ type: 'checkpoint', runId: 'run_1', data: { message: 'test' } });
38
+
39
+ await logger.log(event);
40
+ const events = await logger.readAll();
41
+ expect(events.length).toBe(1);
42
+ expect(events[0].data.message).toBe('test');
43
+ });
44
+
45
+ test('should reject invalid events', async () => {
46
+ const logger = new EventLogger(eventsFile);
47
+ await expect(logger.log({ type: 'invalid_type', runId: 'run_1' }))
48
+ .rejects.toThrow('Invalid event');
49
+ });
50
+
51
+ test('should query by type', async () => {
52
+ const logger = new EventLogger(eventsFile);
53
+
54
+ await logger.log({ type: 'answer', runId: 'run_1', data: { q: 'q1' } });
55
+ await logger.log({ type: 'skip', runId: 'run_1', data: { q: 'q2' } });
56
+ await logger.log({ type: 'answer', runId: 'run_1', data: { q: 'q3' } });
57
+
58
+ const answers = await logger.queryByType('answer');
59
+ expect(answers.length).toBe(2);
60
+
61
+ const skips = await logger.queryByType('skip');
62
+ expect(skips.length).toBe(1);
63
+ });
64
+
65
+ test('should reject invalid query type', async () => {
66
+ const logger = new EventLogger(eventsFile);
67
+ await expect(logger.queryByType('fake')).rejects.toThrow('Invalid event type');
68
+ });
69
+
70
+ test('should query by context window', async () => {
71
+ const logger = new EventLogger(eventsFile);
72
+
73
+ await logger.log({ type: 'answer', runId: 'run_1', contextWindowId: 'cw_1', data: {} });
74
+ await logger.log({ type: 'answer', runId: 'run_1', contextWindowId: 'cw_2', data: {} });
75
+ await logger.log({ type: 'skip', runId: 'run_1', contextWindowId: 'cw_1', data: {} });
76
+
77
+ const cw1Events = await logger.queryByWindow('cw_1');
78
+ expect(cw1Events.length).toBe(2);
79
+
80
+ const cw2Events = await logger.queryByWindow('cw_2');
81
+ expect(cw2Events.length).toBe(1);
82
+ });
83
+
84
+ test('should query by time range', async () => {
85
+ const logger = new EventLogger(eventsFile);
86
+ const now = new Date();
87
+
88
+ await logger.log({
89
+ type: 'answer', runId: 'run_1',
90
+ timestamp: new Date(now.getTime() - 60000).toISOString(),
91
+ data: { q: 'old' }
92
+ });
93
+ await logger.log({
94
+ type: 'answer', runId: 'run_1',
95
+ timestamp: now.toISOString(),
96
+ data: { q: 'new' }
97
+ });
98
+
99
+ const recent = await logger.queryByTimeRange(
100
+ new Date(now.getTime() - 30000).toISOString(),
101
+ new Date(now.getTime() + 1000).toISOString()
102
+ );
103
+ expect(recent.length).toBe(1);
104
+ expect(recent[0].data.q).toBe('new');
105
+ });
106
+
107
+ test('should tail last N events', async () => {
108
+ const logger = new EventLogger(eventsFile);
109
+
110
+ for (let i = 0; i < 5; i++) {
111
+ await logger.log({ type: 'checkpoint', runId: 'run_1', data: { i } });
112
+ }
113
+
114
+ const last2 = await logger.tail(2);
115
+ expect(last2.length).toBe(2);
116
+ expect(last2[0].data.i).toBe(3);
117
+ expect(last2[1].data.i).toBe(4);
118
+ });
119
+
120
+ test('should get stats', async () => {
121
+ const logger = new EventLogger(eventsFile);
122
+
123
+ await logger.log({ type: 'answer', runId: 'run_1', data: {} });
124
+ await logger.log({ type: 'answer', runId: 'run_1', data: {} });
125
+ await logger.log({ type: 'skip', runId: 'run_1', data: {} });
126
+ await logger.log({ type: 'error', runId: 'run_1', data: {} });
127
+
128
+ const stats = await logger.getStats();
129
+ expect(stats.total).toBe(4);
130
+ expect(stats.byType.answer).toBe(2);
131
+ expect(stats.byType.skip).toBe(1);
132
+ expect(stats.byType.error).toBe(1);
133
+ });
134
+
135
+ test('should return empty array when file does not exist', async () => {
136
+ const logger = new EventLogger(path.join(tempDir, 'nonexistent.jsonl'));
137
+ const events = await logger.readAll();
138
+ expect(events).toEqual([]);
139
+ });
140
+
141
+ test('should check existence', async () => {
142
+ const logger = new EventLogger(eventsFile);
143
+ expect(await logger.exists()).toBe(false);
144
+
145
+ await logger.log({ type: 'checkpoint', runId: 'run_1', data: {} });
146
+ expect(await logger.exists()).toBe(true);
147
+ });
148
+ });
@@ -0,0 +1,124 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const FeatureManifestManager = require('../lib/harness/feature-manifest');
4
+
5
+ describe('FeatureManifestManager', () => {
6
+ const tempDir = path.join(__dirname, 'temp-feature-manifest-test');
7
+ let manager;
8
+
9
+ beforeEach(async () => {
10
+ await fs.ensureDir(tempDir);
11
+ manager = new FeatureManifestManager(tempDir);
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await fs.remove(tempDir);
16
+ });
17
+
18
+ test('should generate from explicit feature list', async () => {
19
+ const manifest = await manager.generateFromList('run_1', [
20
+ { title: 'User Auth', description: 'Login/signup flow' },
21
+ { title: 'Dashboard', description: 'Admin dashboard' }
22
+ ]);
23
+
24
+ expect(manifest.features.length).toBe(2);
25
+ expect(manifest.features[0].title).toBe('User Auth');
26
+ expect(manifest.features[0].passes).toBe(false);
27
+ });
28
+
29
+ test('should mark passes (only mutation)', async () => {
30
+ await manager.generateFromList('run_1', [
31
+ { title: 'Feature A' },
32
+ { title: 'Feature B' }
33
+ ]);
34
+
35
+ const featId = manager.getManifest().features[0].id;
36
+ const feature = await manager.markPasses(featId);
37
+ expect(feature.passes).toBe(true);
38
+ expect(feature.status).toBe('passes');
39
+ });
40
+
41
+ test('should throw on unknown feature', async () => {
42
+ await manager.generateFromList('run_1', [{ title: 'X' }]);
43
+ await expect(manager.markPasses('feat_nonexistent')).rejects.toThrow('Feature not found');
44
+ });
45
+
46
+ test('should track progress', async () => {
47
+ await manager.generateFromList('run_1', [
48
+ { title: 'A' }, { title: 'B' }, { title: 'C' }
49
+ ]);
50
+
51
+ let progress = manager.getProgress();
52
+ expect(progress.total).toBe(3);
53
+ expect(progress.passed).toBe(0);
54
+ expect(progress.percentage).toBe(0);
55
+
56
+ const featId = manager.getManifest().features[0].id;
57
+ await manager.markPasses(featId);
58
+
59
+ progress = manager.getProgress();
60
+ expect(progress.passed).toBe(1);
61
+ expect(progress.percentage).toBe(33);
62
+ });
63
+
64
+ test('should save and load', async () => {
65
+ await manager.generateFromList('run_1', [
66
+ { title: 'Feature X', description: 'Does X' }
67
+ ]);
68
+
69
+ const manager2 = new FeatureManifestManager(tempDir);
70
+ const loaded = await manager2.load();
71
+ expect(loaded).toBe(true);
72
+ expect(manager2.getManifest().features[0].title).toBe('Feature X');
73
+ });
74
+
75
+ test('should return false when no manifest to load', async () => {
76
+ const loaded = await manager.load();
77
+ expect(loaded).toBe(false);
78
+ });
79
+
80
+ test('should extract features from markdown', async () => {
81
+ const markdown = `# Project Plan
82
+
83
+ ## Overview
84
+ This is the overview section.
85
+
86
+ ## User Authentication
87
+ Implement login and signup with OAuth2.
88
+
89
+ ## Dashboard Widget
90
+ Interactive charts for analytics.
91
+
92
+ ## Appendix
93
+ Reference material.
94
+ `;
95
+
96
+ const manifest = await manager.generateFromSession('run_1', { prd: markdown });
97
+ const titles = manifest.features.map(f => f.title);
98
+
99
+ expect(titles).toContain('User Authentication');
100
+ expect(titles).toContain('Dashboard Widget');
101
+ // Should skip Overview and Appendix (generic headers)
102
+ expect(titles).not.toContain('Overview');
103
+ expect(titles).not.toContain('Appendix');
104
+ });
105
+
106
+ test('should extract from multiple outputs', async () => {
107
+ const manifest = await manager.generateFromSession('run_1', {
108
+ prp: '## Feature One\nDoes one thing.\n',
109
+ specification: '## Feature Two\nDoes another thing.\n'
110
+ });
111
+
112
+ expect(manifest.features.length).toBe(2);
113
+ });
114
+
115
+ test('should return zero progress when no manifest', () => {
116
+ const progress = manager.getProgress();
117
+ expect(progress.total).toBe(0);
118
+ });
119
+
120
+ test('should throw when marking passes without loaded manifest', async () => {
121
+ // No manifest generated or loaded
122
+ await expect(manager.markPasses('feat_x')).rejects.toThrow('No feature manifest loaded');
123
+ });
124
+ });
@@ -0,0 +1,196 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const HeadlessAdapter = require('../lib/harness/headless-adapter');
4
+
5
+ describe('HeadlessAdapter', () => {
6
+ const tempDir = path.join(__dirname, 'temp-headless-test');
7
+
8
+ beforeEach(async () => {
9
+ await fs.ensureDir(tempDir);
10
+ });
11
+
12
+ afterEach(async () => {
13
+ await fs.remove(tempDir);
14
+ });
15
+
16
+ test('should create with mapped answers', () => {
17
+ const adapter = new HeadlessAdapter({
18
+ 'q1': 'React',
19
+ 'q2': 'TypeScript'
20
+ });
21
+
22
+ expect(adapter.getAnswer('q1')).toBe('React');
23
+ expect(adapter.getAnswer('q2')).toBe('TypeScript');
24
+ });
25
+
26
+ test('should create with ordered answers', () => {
27
+ const adapter = new HeadlessAdapter([
28
+ 'First answer',
29
+ 'Second answer',
30
+ 'Third answer'
31
+ ]);
32
+
33
+ expect(adapter.getAnswer('q1', 'Question 1?')).toBe('First answer');
34
+ expect(adapter.getAnswer('q2', 'Question 2?')).toBe('Second answer');
35
+ expect(adapter.getAnswer('q3', 'Question 3?')).toBe('Third answer');
36
+ });
37
+
38
+ test('should use default answer as fallback', () => {
39
+ const adapter = new HeadlessAdapter({
40
+ 'q1': 'Specific answer'
41
+ });
42
+ adapter.defaultAnswer = 'I will provide this later.';
43
+
44
+ expect(adapter.getAnswer('q1')).toBe('Specific answer');
45
+ expect(adapter.getAnswer('q_unknown')).toBe('I will provide this later.');
46
+ });
47
+
48
+ test('should support function default answer', () => {
49
+ const adapter = new HeadlessAdapter();
50
+ adapter.defaultAnswer = (id, text) => `Auto-answer for ${id}`;
51
+
52
+ expect(adapter.getAnswer('q1', 'What framework?')).toBe('Auto-answer for q1');
53
+ });
54
+
55
+ test('should return null when no answer available', () => {
56
+ const adapter = new HeadlessAdapter();
57
+ expect(adapter.getAnswer('q1')).toBeNull();
58
+ });
59
+
60
+ test('should load from JSON file', async () => {
61
+ const inputFile = path.join(tempDir, 'input.json');
62
+ await fs.writeJson(inputFile, {
63
+ answers: { 'q1': 'React', 'q2': 'PostgreSQL' },
64
+ defaultAnswer: 'TBD'
65
+ });
66
+
67
+ const adapter = new HeadlessAdapter();
68
+ await adapter.loadFromFile(inputFile);
69
+
70
+ expect(adapter.getAnswer('q1')).toBe('React');
71
+ expect(adapter.getAnswer('q_other')).toBe('TBD');
72
+ });
73
+
74
+ test('should load ordered answers from file', async () => {
75
+ const inputFile = path.join(tempDir, 'ordered.json');
76
+ await fs.writeJson(inputFile, {
77
+ orderedAnswers: ['Answer 1', 'Answer 2']
78
+ });
79
+
80
+ const adapter = new HeadlessAdapter();
81
+ await adapter.loadFromFile(inputFile);
82
+
83
+ expect(adapter.getAnswer('q1')).toBe('Answer 1');
84
+ expect(adapter.getAnswer('q2')).toBe('Answer 2');
85
+ });
86
+
87
+ test('should throw on missing file', async () => {
88
+ const adapter = new HeadlessAdapter();
89
+ await expect(adapter.loadFromFile('/nonexistent/file.json'))
90
+ .rejects.toThrow('not found');
91
+ });
92
+
93
+ test('should initialize from string source (file path)', async () => {
94
+ const inputFile = path.join(tempDir, 'init-test.json');
95
+ await fs.writeJson(inputFile, { answers: { 'q1': 'Value' } });
96
+
97
+ const adapter = new HeadlessAdapter(inputFile);
98
+ await adapter.initialize();
99
+
100
+ expect(adapter.getAnswer('q1')).toBe('Value');
101
+ });
102
+
103
+ test('should handle choice selection', () => {
104
+ const adapter = new HeadlessAdapter({ 'workflow': 'balanced' });
105
+
106
+ const choices = [
107
+ { name: 'Rapid', value: 'rapid' },
108
+ { name: 'Balanced', value: 'balanced' },
109
+ { name: 'Comprehensive', value: 'comprehensive' }
110
+ ];
111
+
112
+ expect(adapter.getChoice('workflow', choices)).toBe('balanced');
113
+ });
114
+
115
+ test('should return default choice when no mapping', () => {
116
+ const adapter = new HeadlessAdapter();
117
+
118
+ const choices = [
119
+ { name: 'A', value: 'a' },
120
+ { name: 'B', value: 'b' }
121
+ ];
122
+
123
+ expect(adapter.getChoice('unknown', choices, 'a')).toBe('a');
124
+ });
125
+
126
+ test('should return first choice as last resort', () => {
127
+ const adapter = new HeadlessAdapter();
128
+
129
+ const choices = [
130
+ { name: 'First', value: 'first' },
131
+ { name: 'Second', value: 'second' }
132
+ ];
133
+
134
+ expect(adapter.getChoice('unknown', choices)).toBe('first');
135
+ });
136
+
137
+ test('should handle string choices', () => {
138
+ const adapter = new HeadlessAdapter({ 'q': 'option-b' });
139
+ const choices = ['option-a', 'option-b', 'option-c'];
140
+ expect(adapter.getChoice('q', choices)).toBe('option-b');
141
+ });
142
+
143
+ test('should handle confirmation', () => {
144
+ const adapter = new HeadlessAdapter({ 'confirm': true, 'deny': 'no' });
145
+
146
+ expect(adapter.getConfirmation('confirm')).toBe(true);
147
+ expect(adapter.getConfirmation('deny')).toBe(false);
148
+ expect(adapter.getConfirmation('unknown', true)).toBe(true);
149
+ expect(adapter.getConfirmation('unknown', false)).toBe(false);
150
+ });
151
+
152
+ test('should check if more answers available', () => {
153
+ const adapter = new HeadlessAdapter(['a', 'b']);
154
+ expect(adapter.hasMoreAnswers()).toBe(true);
155
+
156
+ adapter.getAnswer('q1');
157
+ adapter.getAnswer('q2');
158
+ expect(adapter.hasMoreAnswers()).toBe(false);
159
+ });
160
+
161
+ test('should track interaction log', () => {
162
+ const adapter = new HeadlessAdapter({ 'q1': 'React' });
163
+ adapter.getAnswer('q1', 'What framework?');
164
+ adapter.getAnswer('q2', 'What database?');
165
+
166
+ const log = adapter.getLog();
167
+ expect(log.length).toBe(2);
168
+ expect(log[0].questionId).toBe('q1');
169
+ expect(log[0].answer).toBe('React');
170
+ expect(log[1].answer).toBeNull();
171
+ });
172
+
173
+ test('should get stats', () => {
174
+ const adapter = new HeadlessAdapter({ 'q1': 'React' });
175
+ adapter.getAnswer('q1', 'What framework?');
176
+ adapter.getAnswer('q2', 'What database?');
177
+
178
+ const stats = adapter.getStats();
179
+ expect(stats.totalQuestions).toBe(2);
180
+ expect(stats.answered).toBe(1);
181
+ expect(stats.unanswered).toBe(1);
182
+ });
183
+
184
+ test('should export log to file', async () => {
185
+ const adapter = new HeadlessAdapter({ 'q1': 'React' });
186
+ adapter.getAnswer('q1', 'What framework?');
187
+
188
+ const logFile = path.join(tempDir, 'log.json');
189
+ await adapter.exportLog(logFile);
190
+
191
+ expect(await fs.pathExists(logFile)).toBe(true);
192
+ const data = await fs.readJson(logFile);
193
+ expect(data.stats.totalQuestions).toBe(1);
194
+ expect(data.log.length).toBe(1);
195
+ });
196
+ });
@@ -0,0 +1,207 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const RunManager = require('../lib/harness/run-manager');
4
+ const ContextWindowManager = require('../lib/harness/context-window-manager');
5
+ const MilestoneTracker = require('../lib/harness/milestone-tracker');
6
+ const FeatureManifestManager = require('../lib/harness/feature-manifest');
7
+ const EventLogger = require('../lib/harness/event-logger');
8
+ const KnowledgeGraph = require('../lib/analysis/knowledge-graph');
9
+
10
+ describe('Harness Integration', () => {
11
+ const tempDir = path.join(__dirname, 'temp-harness-integration');
12
+
13
+ beforeEach(async () => {
14
+ await fs.ensureDir(tempDir);
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await fs.remove(tempDir);
19
+ });
20
+
21
+ test('full lifecycle: create > answer > handoff > resume > complete', async () => {
22
+ // Phase 1: Create run
23
+ const manager = new RunManager(tempDir);
24
+ const run = await manager.createRun({
25
+ workflow: 'balanced',
26
+ provider: { id: 'anthropic', model: 'claude-opus-4-6', contextWindowSize: 200000 },
27
+ sessionId: 'test-session-1'
28
+ });
29
+
30
+ expect(run.status).toBe('initializing');
31
+ const started = await manager.startRun(run.id);
32
+ expect(started.status).toBe('running');
33
+
34
+ // Phase 2: Open context window
35
+ const runDir = manager.getRunDir(run.id);
36
+ const cwManager = new ContextWindowManager(runDir, run.id);
37
+ const window1 = await cwManager.openWindow({ tokenLimit: 100000 });
38
+
39
+ expect(window1.sequence).toBe(1);
40
+ expect(window1.status).toBe('active');
41
+
42
+ // Phase 3: Generate milestones
43
+ const milestoneTracker = new MilestoneTracker(runDir);
44
+ await milestoneTracker.generateFromBlocks([
45
+ { title: 'Overview', number: 1, questions: [{ id: 'q1' }, { id: 'q2' }] },
46
+ { title: 'Architecture', number: 2, questions: [{ id: 'q3' }, { id: 'q4' }] },
47
+ { title: 'Features', number: 3, questions: [{ id: 'q5' }] }
48
+ ]);
49
+
50
+ // Phase 4: Simulate answering questions
51
+ cwManager.recordQuestionAnswered('q1');
52
+ cwManager.recordQuestionAnswered('q2');
53
+ await milestoneTracker.completeQuestion('q1');
54
+ await milestoneTracker.completeQuestion('q2');
55
+
56
+ const logger = EventLogger.forRun(runDir);
57
+ await logger.log({
58
+ type: 'answer', runId: run.id, contextWindowId: window1.id,
59
+ data: { questionId: 'q1', quality: 85 }
60
+ });
61
+ await logger.log({
62
+ type: 'answer', runId: run.id, contextWindowId: window1.id,
63
+ data: { questionId: 'q2', quality: 90 }
64
+ });
65
+
66
+ // Simulate knowledge extraction
67
+ const kgSessionDir = path.join(tempDir, '.adf', 'sessions', 'test-session');
68
+ await fs.ensureDir(kgSessionDir);
69
+ const kg = new KnowledgeGraph(kgSessionDir);
70
+ kg.add([
71
+ { type: 'tech_stack', content: 'React with TypeScript', confidence: 90, source: 'q1' },
72
+ { type: 'architecture', content: 'Microservices with API gateway', confidence: 85, source: 'q2' }
73
+ ]);
74
+
75
+ // Phase 5: Generate handoff package
76
+ const handoff = await cwManager.generateHandoffPackage({
77
+ progressSummary: 'Completed Overview block (2 of 5 questions answered)',
78
+ completedWork: ['Block 1: Overview'],
79
+ nextSteps: ['Block 2: Architecture'],
80
+ currentMilestone: milestoneTracker.getHandoffInfo(),
81
+ knowledgeGraph: kg.toHandoffFormat(),
82
+ criticalAnswers: [
83
+ { questionId: 'q1', summary: 'React with TypeScript', quality: 85 },
84
+ { questionId: 'q2', summary: 'Microservices', quality: 90 }
85
+ ],
86
+ position: {
87
+ currentBlock: 2,
88
+ totalBlocks: 3,
89
+ answeredIds: ['q1', 'q2'],
90
+ remainingIds: ['q3', 'q4', 'q5']
91
+ },
92
+ activeRules: [],
93
+ sourceProviderId: 'anthropic'
94
+ });
95
+
96
+ expect(handoff.position.answeredIds).toEqual(['q1', 'q2']);
97
+ expect(handoff.knowledgeGraph.highConfidenceItems.length).toBe(2);
98
+
99
+ // Phase 6: Close window, pause run
100
+ await cwManager.closeWindow('completed');
101
+ const paused = await manager.pauseRun(run.id);
102
+ expect(paused.status).toBe('paused');
103
+
104
+ // Phase 7: Resume run in new context window
105
+ const resumed = await manager.resumeRun(run.id);
106
+ expect(resumed.status).toBe('running');
107
+
108
+ const window2 = await cwManager.openWindow({ tokenLimit: 100000 });
109
+ expect(window2.sequence).toBe(2);
110
+ expect(window2.previousWindowId).toBe(window1.id);
111
+
112
+ // Phase 8: Consume handoff
113
+ const consumedHandoff = await cwManager.consumeHandoffPackage('latest');
114
+ expect(consumedHandoff).not.toBeNull();
115
+ expect(consumedHandoff.progressSummary).toContain('Completed Overview');
116
+ expect(consumedHandoff.position.answeredIds).toEqual(['q1', 'q2']);
117
+
118
+ // Restore knowledge from handoff
119
+ const kg2 = new KnowledgeGraph(kgSessionDir);
120
+ kg2.fromHandoffFormat(consumedHandoff.knowledgeGraph);
121
+ expect(kg2.has('tech_stack', 90)).toBe(true);
122
+ expect(kg2.has('architecture', 85)).toBe(true);
123
+
124
+ // Phase 9: Continue answering
125
+ cwManager.recordQuestionAnswered('q3');
126
+ cwManager.recordQuestionAnswered('q4');
127
+ cwManager.recordQuestionAnswered('q5');
128
+ await milestoneTracker.completeQuestion('q3');
129
+ await milestoneTracker.completeQuestion('q4');
130
+ await milestoneTracker.completeQuestion('q5');
131
+
132
+ // Phase 10: Complete run
133
+ const completed = await manager.completeRun(run.id);
134
+ expect(completed.status).toBe('completed');
135
+
136
+ // Verify final state
137
+ const milestoneProgress = milestoneTracker.getProgress();
138
+ expect(milestoneProgress.completed).toBe(3);
139
+ expect(milestoneProgress.percentage).toBe(100);
140
+
141
+ // Verify event log
142
+ const stats = await logger.getStats();
143
+ expect(stats.total).toBeGreaterThan(0);
144
+ expect(stats.byType.answer).toBe(2);
145
+
146
+ // Verify windows
147
+ const allWindows = await cwManager.listWindows();
148
+ expect(allWindows.length).toBe(2);
149
+ });
150
+
151
+ test('feature manifest lifecycle', async () => {
152
+ const manager = new RunManager(tempDir);
153
+ const run = await manager.createRun({ workflow: 'rapid' });
154
+ const runDir = manager.getRunDir(run.id);
155
+
156
+ const manifestManager = new FeatureManifestManager(runDir);
157
+ await manifestManager.generateFromList(run.id, [
158
+ { title: 'User Authentication', description: 'Login/signup' },
159
+ { title: 'Dashboard', description: 'Admin panel' },
160
+ { title: 'API Endpoints', description: 'REST API' }
161
+ ]);
162
+
163
+ let progress = manifestManager.getProgress();
164
+ expect(progress.total).toBe(3);
165
+ expect(progress.passed).toBe(0);
166
+
167
+ // Mark features as passing
168
+ const features = manifestManager.getManifest().features;
169
+ await manifestManager.markPasses(features[0].id);
170
+ await manifestManager.markPasses(features[2].id);
171
+
172
+ progress = manifestManager.getProgress();
173
+ expect(progress.passed).toBe(2);
174
+ expect(progress.percentage).toBe(67);
175
+
176
+ // Verify persistence
177
+ const manifestManager2 = new FeatureManifestManager(runDir);
178
+ await manifestManager2.load();
179
+ expect(manifestManager2.getProgress().passed).toBe(2);
180
+ });
181
+
182
+ test('multiple runs coexist', async () => {
183
+ const manager = new RunManager(tempDir);
184
+
185
+ const run1 = await manager.createRun({ workflow: 'rapid' });
186
+ const run2 = await manager.createRun({ workflow: 'balanced' });
187
+
188
+ // run2 should be current
189
+ const current = await manager.getCurrentRun();
190
+ expect(current.id).toBe(run2.id);
191
+
192
+ // Both should be listable
193
+ const runs = await manager.listRuns();
194
+ expect(runs.length).toBe(2);
195
+
196
+ // Complete run2, run1 still exists
197
+ await manager.startRun(run2.id);
198
+ await manager.completeRun(run2.id);
199
+
200
+ const currentAfter = await manager.getCurrentRunId();
201
+ expect(currentAfter).toBeNull();
202
+
203
+ const run1Loaded = await manager.loadRun(run1.id);
204
+ expect(run1Loaded).not.toBeNull();
205
+ expect(run1Loaded.workflow).toBe('rapid');
206
+ });
207
+ });