@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,175 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { FeatureManifest } = require('./protocol');
4
+
5
+ /**
6
+ * Feature Manifest Manager
7
+ *
8
+ * Generates and manages the feature list from session outputs.
9
+ * Enforces passes-only mutation pattern (Anthropic feature list pattern).
10
+ */
11
+ class FeatureManifestManager {
12
+ constructor(runDir) {
13
+ this.runDir = runDir;
14
+ this.filePath = path.join(runDir, 'feature-manifest.json');
15
+ this.manifest = null;
16
+ }
17
+
18
+ /**
19
+ * Generate feature manifest from session outputs
20
+ */
21
+ async generateFromSession(runId, sessionOutputs = {}) {
22
+ const features = [];
23
+
24
+ // Extract features from PRP output
25
+ if (sessionOutputs.prp) {
26
+ const prpFeatures = this.extractFeaturesFromMarkdown(sessionOutputs.prp, 'prp');
27
+ features.push(...prpFeatures);
28
+ }
29
+
30
+ // Extract features from specification output
31
+ if (sessionOutputs.specification) {
32
+ const specFeatures = this.extractFeaturesFromMarkdown(sessionOutputs.specification, 'specification');
33
+ features.push(...specFeatures);
34
+ }
35
+
36
+ // Extract features from stories output
37
+ if (sessionOutputs.stories) {
38
+ const storyFeatures = this.extractFeaturesFromMarkdown(sessionOutputs.stories, 'stories');
39
+ features.push(...storyFeatures);
40
+ }
41
+
42
+ // Extract features from PRD output
43
+ if (sessionOutputs.prd) {
44
+ const prdFeatures = this.extractFeaturesFromMarkdown(sessionOutputs.prd, 'prd');
45
+ features.push(...prdFeatures);
46
+ }
47
+
48
+ // Extract features from tasks output
49
+ if (sessionOutputs.tasks) {
50
+ const taskFeatures = this.extractFeaturesFromMarkdown(sessionOutputs.tasks, 'tasks');
51
+ features.push(...taskFeatures);
52
+ }
53
+
54
+ this.manifest = new FeatureManifest({ runId, features });
55
+ await this.save();
56
+ return this.manifest;
57
+ }
58
+
59
+ /**
60
+ * Generate feature manifest from explicit feature list
61
+ */
62
+ async generateFromList(runId, featureList) {
63
+ const features = featureList.map(f => ({
64
+ title: f.title,
65
+ description: f.description || '',
66
+ sourceQuestions: f.sourceQuestions || [],
67
+ acceptanceCriteria: f.acceptanceCriteria || []
68
+ }));
69
+
70
+ this.manifest = new FeatureManifest({ runId, features });
71
+ await this.save();
72
+ return this.manifest;
73
+ }
74
+
75
+ /**
76
+ * Mark a feature as passing (ONLY allowed mutation)
77
+ */
78
+ async markPasses(featureId) {
79
+ if (!this.manifest) {
80
+ await this.load();
81
+ }
82
+ if (!this.manifest) {
83
+ throw new Error('No feature manifest loaded');
84
+ }
85
+
86
+ const feature = this.manifest.markPasses(featureId);
87
+ await this.save();
88
+ return feature;
89
+ }
90
+
91
+ /**
92
+ * Get progress summary
93
+ */
94
+ getProgress() {
95
+ if (!this.manifest) return { total: 0, passed: 0, remaining: 0, percentage: 0 };
96
+ return this.manifest.getProgress();
97
+ }
98
+
99
+ /**
100
+ * Save manifest to disk
101
+ */
102
+ async save() {
103
+ if (!this.manifest) return;
104
+ await fs.writeJson(this.filePath, this.manifest.toJSON(), { spaces: 2 });
105
+ }
106
+
107
+ /**
108
+ * Load manifest from disk
109
+ */
110
+ async load() {
111
+ if (!await fs.pathExists(this.filePath)) {
112
+ return false;
113
+ }
114
+
115
+ const data = await fs.readJson(this.filePath);
116
+ this.manifest = FeatureManifest.fromJSON(data);
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Get the manifest
122
+ */
123
+ getManifest() {
124
+ return this.manifest;
125
+ }
126
+
127
+ /**
128
+ * Extract features from markdown content (simple heuristic)
129
+ */
130
+ extractFeaturesFromMarkdown(content, source) {
131
+ const features = [];
132
+ const lines = content.split('\n');
133
+
134
+ for (let i = 0; i < lines.length; i++) {
135
+ const line = lines[i].trim();
136
+
137
+ // Match h2/h3 headers that look like features
138
+ const headerMatch = line.match(/^#{2,3}\s+(?:\d+\.\s+)?(.+)/);
139
+ if (headerMatch) {
140
+ const title = headerMatch[1].trim();
141
+
142
+ // Skip generic headers
143
+ const skipHeaders = [
144
+ 'overview', 'summary', 'introduction', 'table of contents',
145
+ 'references', 'appendix', 'glossary', 'changelog',
146
+ 'executive summary', 'background', 'scope', 'assumptions'
147
+ ];
148
+ if (skipHeaders.some(h => title.toLowerCase().includes(h))) {
149
+ continue;
150
+ }
151
+
152
+ // Collect description from following lines
153
+ let description = '';
154
+ for (let j = i + 1; j < lines.length && j < i + 4; j++) {
155
+ const nextLine = lines[j].trim();
156
+ if (nextLine && !nextLine.startsWith('#')) {
157
+ description += (description ? ' ' : '') + nextLine;
158
+ } else if (nextLine.startsWith('#')) {
159
+ break;
160
+ }
161
+ }
162
+
163
+ features.push({
164
+ title,
165
+ description: description.slice(0, 200),
166
+ sourceQuestions: [source]
167
+ });
168
+ }
169
+ }
170
+
171
+ return features;
172
+ }
173
+ }
174
+
175
+ module.exports = FeatureManifestManager;
@@ -0,0 +1,184 @@
1
+ const fs = require('fs-extra');
2
+
3
+ /**
4
+ * Headless Adapter
5
+ *
6
+ * Replaces interactive inquirer prompts with JSON-driven input
7
+ * for CI/automation pipelines. Provides deterministic answer flow
8
+ * without user interaction.
9
+ */
10
+ class HeadlessAdapter {
11
+ constructor(inputSource) {
12
+ this.answers = new Map();
13
+ this.defaultAnswer = null;
14
+ this.answerIndex = 0;
15
+ this.orderedAnswers = [];
16
+ this.log = [];
17
+
18
+ if (inputSource) {
19
+ this.loadSource(inputSource);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Load answers from various source types
25
+ */
26
+ loadSource(source) {
27
+ if (typeof source === 'string') {
28
+ // File path - will be loaded async
29
+ this.pendingFile = source;
30
+ } else if (Array.isArray(source)) {
31
+ // Ordered array of answers
32
+ this.orderedAnswers = source;
33
+ } else if (typeof source === 'object') {
34
+ // Map of questionId -> answer
35
+ for (const [key, value] of Object.entries(source)) {
36
+ this.answers.set(key, value);
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Load answers from a JSON file
43
+ */
44
+ async loadFromFile(filePath) {
45
+ if (!await fs.pathExists(filePath)) {
46
+ throw new Error(`Headless input file not found: ${filePath}`);
47
+ }
48
+
49
+ const data = await fs.readJson(filePath);
50
+
51
+ if (data.answers) {
52
+ // Structured format: { answers: { questionId: answer }, defaults: {...} }
53
+ for (const [key, value] of Object.entries(data.answers)) {
54
+ this.answers.set(key, value);
55
+ }
56
+ }
57
+
58
+ if (data.orderedAnswers) {
59
+ this.orderedAnswers = data.orderedAnswers;
60
+ }
61
+
62
+ if (data.defaultAnswer) {
63
+ this.defaultAnswer = data.defaultAnswer;
64
+ }
65
+
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Initialize (load pending file if any)
71
+ */
72
+ async initialize() {
73
+ if (this.pendingFile) {
74
+ await this.loadFromFile(this.pendingFile);
75
+ this.pendingFile = null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get an answer for a question (replaces inquirer.prompt)
81
+ */
82
+ getAnswer(questionId, questionText) {
83
+ let answer = null;
84
+
85
+ // Try mapped answer first
86
+ if (this.answers.has(questionId)) {
87
+ answer = this.answers.get(questionId);
88
+ }
89
+ // Try ordered answers
90
+ else if (this.answerIndex < this.orderedAnswers.length) {
91
+ answer = this.orderedAnswers[this.answerIndex];
92
+ this.answerIndex++;
93
+ }
94
+ // Use default
95
+ else if (this.defaultAnswer) {
96
+ answer = typeof this.defaultAnswer === 'function'
97
+ ? this.defaultAnswer(questionId, questionText)
98
+ : this.defaultAnswer;
99
+ }
100
+
101
+ this.log.push({
102
+ questionId,
103
+ questionText,
104
+ answer,
105
+ timestamp: new Date().toISOString()
106
+ });
107
+
108
+ return answer;
109
+ }
110
+
111
+ /**
112
+ * Get a choice selection (replaces inquirer list/select)
113
+ */
114
+ getChoice(questionId, choices, defaultChoice) {
115
+ // Try mapped answer
116
+ if (this.answers.has(questionId)) {
117
+ const mapped = this.answers.get(questionId);
118
+ // Find matching choice
119
+ const match = choices.find(c =>
120
+ (typeof c === 'string' && c === mapped) ||
121
+ (typeof c === 'object' && (c.value === mapped || c.name === mapped))
122
+ );
123
+ if (match) {
124
+ return typeof match === 'object' ? match.value : match;
125
+ }
126
+ }
127
+
128
+ // Return default or first choice
129
+ if (defaultChoice !== undefined) return defaultChoice;
130
+ const first = choices[0];
131
+ return typeof first === 'object' ? first.value : first;
132
+ }
133
+
134
+ /**
135
+ * Get confirmation (replaces inquirer confirm)
136
+ */
137
+ getConfirmation(questionId, defaultValue = true) {
138
+ if (this.answers.has(questionId)) {
139
+ const val = this.answers.get(questionId);
140
+ return typeof val === 'boolean' ? val : val === 'yes' || val === 'true';
141
+ }
142
+ return defaultValue;
143
+ }
144
+
145
+ /**
146
+ * Check if there are more answers available
147
+ */
148
+ hasMoreAnswers() {
149
+ return this.answerIndex < this.orderedAnswers.length ||
150
+ this.defaultAnswer !== null;
151
+ }
152
+
153
+ /**
154
+ * Get the interaction log
155
+ */
156
+ getLog() {
157
+ return this.log;
158
+ }
159
+
160
+ /**
161
+ * Get stats
162
+ */
163
+ getStats() {
164
+ return {
165
+ totalQuestions: this.log.length,
166
+ answered: this.log.filter(l => l.answer !== null).length,
167
+ unanswered: this.log.filter(l => l.answer === null).length,
168
+ mappedAnswers: this.answers.size,
169
+ orderedAnswersRemaining: Math.max(0, this.orderedAnswers.length - this.answerIndex)
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Export log to JSON for debugging
175
+ */
176
+ async exportLog(filePath) {
177
+ await fs.writeJson(filePath, {
178
+ stats: this.getStats(),
179
+ log: this.log
180
+ }, { spaces: 2 });
181
+ }
182
+ }
183
+
184
+ module.exports = HeadlessAdapter;
@@ -0,0 +1,183 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { Milestone } = require('./protocol');
4
+
5
+ /**
6
+ * Milestone Tracker
7
+ *
8
+ * Maps ADF question blocks to milestones, tracks progression,
9
+ * and provides checkpoint-based progress reporting.
10
+ */
11
+ class MilestoneTracker {
12
+ constructor(runDir) {
13
+ this.runDir = runDir;
14
+ this.milestonesDir = path.join(runDir, 'milestones');
15
+ this.milestones = [];
16
+ }
17
+
18
+ /**
19
+ * Generate milestones from question blocks
20
+ */
21
+ async generateFromBlocks(blocks) {
22
+ this.milestones = blocks.map((block, index) => {
23
+ return new Milestone({
24
+ index,
25
+ title: block.title || `Block ${index + 1}`,
26
+ description: block.description || '',
27
+ blockNumber: block.number != null ? block.number : index + 1,
28
+ questionsInScope: (block.questions || []).map(q => q.id || q),
29
+ acceptanceCriteria: block.acceptanceCriteria || [
30
+ `All questions in "${block.title || `Block ${index + 1}`}" answered or explicitly skipped`
31
+ ]
32
+ });
33
+ });
34
+
35
+ await this.saveAll();
36
+ return this.milestones;
37
+ }
38
+
39
+ /**
40
+ * Mark a question as completed within its milestone
41
+ */
42
+ async completeQuestion(questionId) {
43
+ for (const milestone of this.milestones) {
44
+ if (milestone.questionsInScope.includes(questionId)) {
45
+ if (!milestone.questionsCompleted.includes(questionId)) {
46
+ milestone.questionsCompleted.push(questionId);
47
+ }
48
+
49
+ // Update percentage
50
+ const total = milestone.questionsInScope.length;
51
+ const done = milestone.questionsCompleted.length;
52
+ milestone.percentage = total > 0 ? Math.round((done / total) * 100) : 0;
53
+
54
+ // Auto-transition status
55
+ if (milestone.status === 'pending') {
56
+ milestone.status = 'in_progress';
57
+ }
58
+ if (done >= total) {
59
+ milestone.status = 'completed';
60
+ milestone.completedAt = new Date().toISOString();
61
+ }
62
+
63
+ await this.saveMilestone(milestone);
64
+ return milestone;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Skip a milestone entirely
72
+ */
73
+ async skipMilestone(index) {
74
+ if (index < 0 || index >= this.milestones.length) {
75
+ throw new Error(`Invalid milestone index: ${index}`);
76
+ }
77
+
78
+ const milestone = this.milestones[index];
79
+ milestone.status = 'skipped';
80
+ await this.saveMilestone(milestone);
81
+ return milestone;
82
+ }
83
+
84
+ /**
85
+ * Get current milestone (first non-completed, non-skipped)
86
+ */
87
+ getCurrentMilestone() {
88
+ return this.milestones.find(m =>
89
+ m.status === 'pending' || m.status === 'in_progress'
90
+ ) || null;
91
+ }
92
+
93
+ /**
94
+ * Get current milestone index
95
+ */
96
+ getCurrentIndex() {
97
+ const current = this.getCurrentMilestone();
98
+ return current ? current.index : this.milestones.length;
99
+ }
100
+
101
+ /**
102
+ * Get overall progress
103
+ */
104
+ getProgress() {
105
+ const total = this.milestones.length;
106
+ const completed = this.milestones.filter(m => m.status === 'completed').length;
107
+ const skipped = this.milestones.filter(m => m.status === 'skipped').length;
108
+ const inProgress = this.milestones.filter(m => m.status === 'in_progress').length;
109
+ const pending = this.milestones.filter(m => m.status === 'pending').length;
110
+
111
+ return {
112
+ total,
113
+ completed,
114
+ skipped,
115
+ inProgress,
116
+ pending,
117
+ percentage: total > 0 ? Math.round(((completed + skipped) / total) * 100) : 0
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Get milestone for handoff package
123
+ */
124
+ getHandoffInfo() {
125
+ const current = this.getCurrentMilestone();
126
+ if (!current) {
127
+ return { index: this.milestones.length, title: 'All complete', percentage: 100 };
128
+ }
129
+ return {
130
+ index: current.index,
131
+ title: current.title,
132
+ percentage: current.percentage
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Save a single milestone
138
+ */
139
+ async saveMilestone(milestone) {
140
+ await fs.ensureDir(this.milestonesDir);
141
+ const filePath = path.join(this.milestonesDir, `ms_${milestone.index + 1}.json`);
142
+ await fs.writeJson(filePath, milestone.toJSON(), { spaces: 2 });
143
+ }
144
+
145
+ /**
146
+ * Save all milestones
147
+ */
148
+ async saveAll() {
149
+ await fs.ensureDir(this.milestonesDir);
150
+ for (const milestone of this.milestones) {
151
+ await this.saveMilestone(milestone);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Load milestones from disk
157
+ */
158
+ async load() {
159
+ if (!await fs.pathExists(this.milestonesDir)) {
160
+ return false;
161
+ }
162
+
163
+ const files = await fs.readdir(this.milestonesDir);
164
+ const milestoneFiles = files.filter(f => f.startsWith('ms_') && f.endsWith('.json'));
165
+
166
+ this.milestones = [];
167
+ for (const file of milestoneFiles.sort()) {
168
+ const data = await fs.readJson(path.join(this.milestonesDir, file));
169
+ this.milestones.push(Milestone.fromJSON(data));
170
+ }
171
+
172
+ return this.milestones.length > 0;
173
+ }
174
+
175
+ /**
176
+ * Get all milestones
177
+ */
178
+ getAll() {
179
+ return this.milestones;
180
+ }
181
+ }
182
+
183
+ module.exports = MilestoneTracker;