spec-agent 2.0.2 → 2.0.4

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.
@@ -0,0 +1,189 @@
1
+ import * as path from 'path';
2
+ import { Command } from 'commander';
3
+ import { Logger } from '../utils/logger';
4
+ import { writeJson } from '../utils/file';
5
+ import { pipelineCommand } from './pipeline';
6
+ import { handoffCommand } from './handoff';
7
+ import { executeCommand } from './execute';
8
+ import { buildOrchestratorContext } from './orchestrate';
9
+
10
+ interface RoundOptions {
11
+ workspace: string;
12
+ input: string;
13
+ target: string;
14
+ maxParallel: string;
15
+ retry?: string;
16
+ complete?: string;
17
+ fail?: string;
18
+ error?: string;
19
+ dryRun?: boolean;
20
+ }
21
+
22
+ export async function roundCommand(options: RoundOptions, command: Command): Promise<void> {
23
+ const logger = new Logger();
24
+ try {
25
+ const workspaceArg = options.workspace || './output';
26
+ const inputArg = options.input || './docs';
27
+ const target = (options.target || 'cursor').toLowerCase();
28
+ const maxParallel = Math.max(1, parseInt(options.maxParallel || '4', 10) || 4);
29
+ const retry = String(Math.max(0, parseInt(options.retry || '1', 10) || 1));
30
+ const hasFeedback = Boolean((options.complete || '').trim() || (options.fail || '').trim());
31
+ const workspacePath = path.resolve(workspaceArg);
32
+ const contextPath = path.join(workspacePath, 'orchestrator_context.json');
33
+
34
+ const before = await buildOrchestratorContext({
35
+ workspaceArg,
36
+ inputArg,
37
+ target,
38
+ maxParallel,
39
+ });
40
+
41
+ let executedAction = 'none';
42
+ if (!options.dryRun) {
43
+ if (hasFeedback && before.checks.runState) {
44
+ executedAction = 'execute_feedback';
45
+ await executeCommand({
46
+ workspace: workspaceArg,
47
+ maxParallel: String(maxParallel),
48
+ retry,
49
+ complete: options.complete,
50
+ fail: options.fail,
51
+ error: options.error,
52
+ dryRun: false,
53
+ }, {} as Command);
54
+
55
+ const afterFeedback = await buildOrchestratorContext({
56
+ workspaceArg,
57
+ inputArg,
58
+ target,
59
+ maxParallel,
60
+ });
61
+
62
+ if (afterFeedback.metrics.pending > 0 && afterFeedback.metrics.running === 0) {
63
+ executedAction = 'execute_feedback_and_schedule';
64
+ await executeCommand({
65
+ workspace: workspaceArg,
66
+ maxParallel: String(maxParallel),
67
+ retry,
68
+ dryRun: false,
69
+ }, {} as Command);
70
+ }
71
+ } else if (!before.checks.dispatchPlan) {
72
+ executedAction = 'pipeline';
73
+ await pipelineCommand({
74
+ input: inputArg,
75
+ output: workspaceArg,
76
+ agents: 'auto',
77
+ chunkSize: '200kb',
78
+ minChunkSize: '10kb',
79
+ analyzeRetries: '1',
80
+ analyzeBudgetTokens: '0',
81
+ framework: 'vue3',
82
+ dryRun: false,
83
+ }, {} as Command);
84
+ } else if (!before.checks.handoffBundle) {
85
+ executedAction = 'handoff';
86
+ await handoffCommand({
87
+ workspace: workspaceArg,
88
+ output: 'handoff',
89
+ target,
90
+ includeSummaries: true,
91
+ dryRun: false,
92
+ }, {} as Command);
93
+ } else if (!before.checks.runState || (before.metrics.pending > 0 && before.metrics.running === 0)) {
94
+ executedAction = 'execute';
95
+ await executeCommand({
96
+ workspace: workspaceArg,
97
+ maxParallel: String(maxParallel),
98
+ retry,
99
+ dryRun: false,
100
+ }, {} as Command);
101
+ } else {
102
+ executedAction = 'wait_feedback';
103
+ }
104
+ }
105
+
106
+ const after = await buildOrchestratorContext({
107
+ workspaceArg,
108
+ inputArg,
109
+ target,
110
+ maxParallel,
111
+ });
112
+
113
+ if (!options.dryRun) {
114
+ await writeJson(contextPath, after);
115
+ }
116
+
117
+ logger.info(`Round action: ${executedAction}`);
118
+ logger.info(`State: ${after.state}`);
119
+ logger.info(`Next action: ${after.nextAction}`);
120
+ logger.info(
121
+ `Metrics: pending=${after.metrics.pending}, running=${after.metrics.running}, succeeded=${after.metrics.succeeded}, failed=${after.metrics.failed}, blocked=${after.metrics.blocked}`
122
+ );
123
+ logger.info('Recommended commands:');
124
+ for (const cmdLine of after.recommendedCommands) {
125
+ logger.info(` ${cmdLine}`);
126
+ }
127
+ if (options.dryRun) {
128
+ logger.warn('Dry run mode - no command execution performed');
129
+ }
130
+ logger.json({
131
+ status: 'success',
132
+ executedAction,
133
+ state: after.state,
134
+ nextAction: after.nextAction,
135
+ metrics: after.metrics,
136
+ contextPath,
137
+ });
138
+ } catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error);
140
+ logger.error(`Round failed: ${message}`);
141
+ const recovery = inferRecoveryCommands(message, options);
142
+ if (recovery.length > 0) {
143
+ logger.info('Suggested recovery commands:');
144
+ for (const item of recovery) {
145
+ logger.info(` ${item}`);
146
+ }
147
+ }
148
+ process.exit(1);
149
+ }
150
+ }
151
+
152
+ function inferRecoveryCommands(message: string, options: RoundOptions): string[] {
153
+ const workspaceArg = options.workspace || './output';
154
+ const inputArg = options.input || './docs';
155
+ const lower = message.toLowerCase();
156
+
157
+ if (lower.includes('not recognized') || lower.includes('command not found')) {
158
+ return [
159
+ 'npm install -g spec-agent --registry=https://registry.npmjs.org/',
160
+ 'spec-agent --version',
161
+ ];
162
+ }
163
+
164
+ if (lower.includes('eneedauth') || lower.includes('e403') || lower.includes('2fa')) {
165
+ return [
166
+ 'npm login --registry=https://registry.npmjs.org/',
167
+ 'npm profile get "two-factor auth" --registry=https://registry.npmjs.org/',
168
+ ];
169
+ }
170
+
171
+ if (lower.includes('api key') || lower.includes('llm') || lower.includes('configuration')) {
172
+ return [
173
+ `spec-agent doctor --workspace ${workspaceArg} --check-llm`,
174
+ '# set LLM_API_KEY / LLM_BASE_URL / LLM_MODEL then retry',
175
+ ];
176
+ }
177
+
178
+ if (lower.includes('manifest not found') || lower.includes('task plan not found') || lower.includes('dispatch_plan')) {
179
+ return [
180
+ `spec-agent pipeline --input ${inputArg} --output ${workspaceArg}`,
181
+ `spec-agent handoff --workspace ${workspaceArg} --target cursor --include-summaries`,
182
+ ];
183
+ }
184
+
185
+ return [
186
+ `spec-agent orchestrate --workspace ${workspaceArg} --input ${inputArg}`,
187
+ `spec-agent round --workspace ${workspaceArg} --input ${inputArg} --target ${(options.target || 'cursor')}`,
188
+ ];
189
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ import { cleanCommand } from './commands/clean';
14
14
  import { doctorCommand } from './commands/doctor';
15
15
  import { handoffCommand } from './commands/handoff';
16
16
  import { executeCommand } from './commands/execute';
17
+ import { orchestrateCommand } from './commands/orchestrate';
18
+ import { roundCommand } from './commands/round';
17
19
 
18
20
  const program = new Command();
19
21
  const pkgVersion = (() => {
@@ -173,4 +175,29 @@ program
173
175
  .option('--dry-run', 'Preview execution transitions without writing state')
174
176
  .action(executeCommand);
175
177
 
178
+ program
179
+ .command('orchestrate')
180
+ .description('Generate orchestrator context and next-step decisions from artifacts')
181
+ .option('-w, --workspace <dir>', 'Workspace directory', './output')
182
+ .option('-i, --input <path>', 'Input docs path used for pipeline', './docs')
183
+ .option('-t, --target <name>', 'Execution target: cursor, qcoder, codebuddy, generic', 'cursor')
184
+ .option('-p, --max-parallel <count>', 'Max parallel tasks for execute suggestion', '4')
185
+ .option('--format <format>', 'Output format: text, json', 'text')
186
+ .option('--dry-run', 'Preview decisions without writing orchestrator_context.json')
187
+ .action(orchestrateCommand);
188
+
189
+ program
190
+ .command('round')
191
+ .description('Run one orchestrated round (pipeline/handoff/execute decision)')
192
+ .option('-w, --workspace <dir>', 'Workspace directory', './output')
193
+ .option('-i, --input <path>', 'Input docs path used for pipeline', './docs')
194
+ .option('-t, --target <name>', 'Execution target: cursor, qcoder, codebuddy, generic', 'cursor')
195
+ .option('-p, --max-parallel <count>', 'Max parallel tasks for execute', '4')
196
+ .option('-r, --retry <count>', 'Retry times per task after failure', '1')
197
+ .option('--complete <ids>', 'Mark task IDs as completed, comma-separated')
198
+ .option('--fail <ids>', 'Mark task IDs as failed, comma-separated')
199
+ .option('--error <message>', 'Failure reason when using --fail')
200
+ .option('--dry-run', 'Preview one-round decision without running commands')
201
+ .action(roundCommand);
202
+
176
203
  program.parse();
@@ -28,8 +28,8 @@ export interface ImageUnderstandingInput {
28
28
 
29
29
  export function getLLMConfig(): LLMConfig {
30
30
  return {
31
- apiKey: process.env.OPENAI_API_KEY || process.env.LLM_API_KEY || '',
32
- baseUrl: process.env.OPENAI_BASE_URL || process.env.LLM_BASE_URL || 'https://api.openai.com/v1',
31
+ apiKey: process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || '',
32
+ baseUrl: process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
33
33
  model: process.env.LLM_MODEL || 'gpt-4o-mini',
34
34
  maxTokens: parseInt(process.env.LLM_MAX_TOKENS || '4000', 10),
35
35
  temperature: parseFloat(process.env.LLM_TEMPERATURE || '0.3'),
@@ -54,7 +54,7 @@ export function getLLMConfigForPurpose(purpose: 'scan' | 'analyze' | 'vision' |
54
54
  export function validateLLMConfig(config: LLMConfig): void {
55
55
  if (!config.apiKey) {
56
56
  throw new Error(
57
- 'LLM API key not found. Please set one of:\n - OPENAI_API_KEY\n - LLM_API_KEY'
57
+ 'LLM API key not found. Please set one of:\n - LLM_API_KEY (recommended)\n - OPENAI_API_KEY (compatibility alias)'
58
58
  );
59
59
  }
60
60
  }