spec-agent 2.0.2 → 2.0.3
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.
- package/.cursor/rules/spec-agent-assistant.mdc +34 -0
- package/.cursor/skills/spec-agent-execution-orchestrator/SKILL.md +133 -0
- package/.cursor/skills/spec-agent-execution-orchestrator/agent-prompts.md +113 -0
- package/.cursor/skills/spec-agent-execution-orchestrator/reference.md +37 -0
- package/.cursor/skills/spec-agent-onboarding-agent/SKILL.md +77 -0
- package/.cursor/skills/spec-agent-product-dev-agent/SKILL.md +92 -0
- package/CURSOR_AGENT_PACK.md +66 -0
- package/README.md +33 -0
- package/USAGE_FROM_NPM.md +9 -0
- package/dist/commands/orchestrate.d.ts +46 -0
- package/dist/commands/orchestrate.d.ts.map +1 -0
- package/dist/commands/orchestrate.js +229 -0
- package/dist/commands/orchestrate.js.map +1 -0
- package/dist/commands/round.d.ts +15 -0
- package/dist/commands/round.d.ts.map +1 -0
- package/dist/commands/round.js +202 -0
- package/dist/commands/round.js.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/orchestrator-v2-design.md +193 -0
- package/package.json +1 -1
- package/spec-agent-implementation.md +15 -0
- package/src/commands/orchestrate.ts +282 -0
- package/src/commands/round.ts +189 -0
- package/src/index.ts +27 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Spec-Agent Orchestrator v2 设计方案
|
|
2
|
+
|
|
3
|
+
## 1. 目标与范围
|
|
4
|
+
|
|
5
|
+
本方案用于将 `spec-agent` 从“产物生成工具”升级为“可持续推进执行的智能体编排器”。
|
|
6
|
+
|
|
7
|
+
核心目标:
|
|
8
|
+
|
|
9
|
+
- 自动调用 `spec-agent` CLI(pipeline/handoff/execute)
|
|
10
|
+
- 基于产物做决策,不依赖人工记忆当前状态
|
|
11
|
+
- 支持失败重试与中断恢复
|
|
12
|
+
- 输出明确下一步动作,降低人工操作成本
|
|
13
|
+
|
|
14
|
+
非目标(v2 不做):
|
|
15
|
+
|
|
16
|
+
- 自动提交 PR / 自动合并代码
|
|
17
|
+
- 跨仓库复杂编排
|
|
18
|
+
- 无约束的自动改代码
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. 总体架构
|
|
23
|
+
|
|
24
|
+
建议采用 4 个职责清晰的 Agent 角色:
|
|
25
|
+
|
|
26
|
+
- **Onboarding Agent**:检查安装与环境,确保首次可运行
|
|
27
|
+
- **Pipeline Agent**:负责 scan -> analyze -> merge -> plan -> dispatch
|
|
28
|
+
- **Execution Orchestrator Agent**:负责 handoff + execute 循环推进
|
|
29
|
+
- **Failure Triage Agent**:负责错误分流、修复建议与重试策略
|
|
30
|
+
|
|
31
|
+
说明:v2 初版可由一个主 Agent 执行上述职责;v2.1 后再拆分为多 Agent 角色。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 3. 输入与输出契约
|
|
36
|
+
|
|
37
|
+
### 3.1 关键输入产物
|
|
38
|
+
|
|
39
|
+
- `output/manifest.json`
|
|
40
|
+
- `output/spec_summary.json`
|
|
41
|
+
- `output/task_plan.json`
|
|
42
|
+
- `output/dispatch_plan.json`
|
|
43
|
+
- `output/handoff/handoff_bundle.json`
|
|
44
|
+
- `output/execution/run_state.json`
|
|
45
|
+
- `output/execution/execution_report.json`
|
|
46
|
+
- `output/execution/inbox/*.md`
|
|
47
|
+
|
|
48
|
+
### 3.2 Orchestrator 统一上下文文件
|
|
49
|
+
|
|
50
|
+
新增文件:
|
|
51
|
+
|
|
52
|
+
- `output/orchestrator_context.json`
|
|
53
|
+
|
|
54
|
+
用途:
|
|
55
|
+
|
|
56
|
+
- 记录当前阶段、上次动作、最近错误、下一步建议
|
|
57
|
+
- 为 Agent 提供单一事实源(single source of truth)
|
|
58
|
+
|
|
59
|
+
建议结构:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"version": "2.0.0-beta",
|
|
64
|
+
"updatedAt": "2026-04-02T10:00:00Z",
|
|
65
|
+
"workspace": "./output",
|
|
66
|
+
"state": "EXECUTION_RUNNING",
|
|
67
|
+
"lastAction": "spec-agent execute --workspace ./output --max-parallel 4",
|
|
68
|
+
"lastError": null,
|
|
69
|
+
"nextAction": "等待回填:--complete/--fail",
|
|
70
|
+
"metrics": {
|
|
71
|
+
"pending": 120,
|
|
72
|
+
"running": 4,
|
|
73
|
+
"succeeded": 33,
|
|
74
|
+
"failed": 2,
|
|
75
|
+
"blocked": 1
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 4. 状态机设计
|
|
83
|
+
|
|
84
|
+
状态定义:
|
|
85
|
+
|
|
86
|
+
- `INIT`
|
|
87
|
+
- `READY`
|
|
88
|
+
- `PIPELINE_RUNNING`
|
|
89
|
+
- `PIPELINE_READY`
|
|
90
|
+
- `EXECUTION_RUNNING`
|
|
91
|
+
- `WAITING_FEEDBACK`
|
|
92
|
+
- `PARTIAL_FAILED`
|
|
93
|
+
- `DONE`
|
|
94
|
+
|
|
95
|
+
状态转移规则(简化):
|
|
96
|
+
|
|
97
|
+
1. `INIT -> READY`:安装与环境检查通过
|
|
98
|
+
2. `READY -> PIPELINE_RUNNING`:触发 pipeline
|
|
99
|
+
3. `PIPELINE_RUNNING -> PIPELINE_READY`:产物齐全(task_plan + dispatch_plan)
|
|
100
|
+
4. `PIPELINE_READY -> EXECUTION_RUNNING`:生成 handoff 并首次 execute
|
|
101
|
+
5. `EXECUTION_RUNNING -> WAITING_FEEDBACK`:存在 running 任务,等待回填
|
|
102
|
+
6. `WAITING_FEEDBACK -> EXECUTION_RUNNING`:回填完成,继续调度
|
|
103
|
+
7. 任意状态 -> `PARTIAL_FAILED`:关键错误出现且不可自动恢复
|
|
104
|
+
8. `EXECUTION_RUNNING -> DONE`:pending/running/blocked 归零
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 5. 决策规则(执行引擎)
|
|
109
|
+
|
|
110
|
+
按优先级从上到下判断:
|
|
111
|
+
|
|
112
|
+
1. 若 `spec-agent --version` 失败:
|
|
113
|
+
- 提示安装:`npm install -g spec-agent --registry=https://registry.npmjs.org/`
|
|
114
|
+
2. 若 `dispatch_plan.json` 不存在:
|
|
115
|
+
- 执行 `spec-agent pipeline --input ./docs --output ./output`
|
|
116
|
+
3. 若 `handoff_bundle.json` 不存在:
|
|
117
|
+
- 执行 `spec-agent handoff --workspace ./output --target cursor --include-summaries`
|
|
118
|
+
4. 若 `run_state.json` 不存在:
|
|
119
|
+
- 执行 `spec-agent execute --workspace ./output --max-parallel 4`
|
|
120
|
+
5. 若 `run_state` 中有 `running`:
|
|
121
|
+
- 进入等待反馈,不重复调度
|
|
122
|
+
6. 若 `pending > 0` 且 `running = 0`:
|
|
123
|
+
- 再次执行 `execute` 调度
|
|
124
|
+
7. 若 `failed` 增长:
|
|
125
|
+
- 调用故障分流策略,给出重试或人工介入建议
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 6. MVP 边界(先做可实验)
|
|
130
|
+
|
|
131
|
+
v2 初版仅包含:
|
|
132
|
+
|
|
133
|
+
- 安装检查与首轮 pipeline 引导
|
|
134
|
+
- handoff + execute 调度闭环
|
|
135
|
+
- `--complete / --fail` 回填推进
|
|
136
|
+
- `orchestrator_context.json` 最小写入
|
|
137
|
+
|
|
138
|
+
v2 初版不包含:
|
|
139
|
+
|
|
140
|
+
- 自动代码生成与自动代码回填
|
|
141
|
+
- 自动 PR 流程
|
|
142
|
+
- 高级冲突自动修复
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 7. 失败分流策略
|
|
147
|
+
|
|
148
|
+
错误类别与处理:
|
|
149
|
+
|
|
150
|
+
- **安装类**(command not found)
|
|
151
|
+
- 给出安装命令与版本校验命令
|
|
152
|
+
- **认证类**(npm ENEEDAUTH / E403)
|
|
153
|
+
- 给出登录或 2FA/token 指引
|
|
154
|
+
- **配置类**(LLM key/base_url/model 缺失)
|
|
155
|
+
- 给出 `doctor --check-llm` 与变量模板
|
|
156
|
+
- **执行类**(task failed)
|
|
157
|
+
- 记录 error,按 `--retry` 重试,超限标记 failed
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 8. 实施计划
|
|
162
|
+
|
|
163
|
+
### Milestone A(1-2 天)
|
|
164
|
+
|
|
165
|
+
- 落地 `orchestrator_context.json`
|
|
166
|
+
- 实现决策函数(只读产物 -> 推荐命令)
|
|
167
|
+
|
|
168
|
+
### Milestone B(2-3 天)
|
|
169
|
+
|
|
170
|
+
- 将决策函数接入现有 Skill(onboarding + execution)
|
|
171
|
+
- 支持一键“执行一轮”指令
|
|
172
|
+
|
|
173
|
+
### Milestone C(2-3 天)
|
|
174
|
+
|
|
175
|
+
- 加入失败分流模板(安装、认证、LLM、执行)
|
|
176
|
+
- 增加轮次摘要输出(可复盘)
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 9. 验收标准
|
|
181
|
+
|
|
182
|
+
- 新环境可从未安装状态引导至 `pipeline` 完成
|
|
183
|
+
- 可连续推进至少 3 轮 execute,不丢失状态
|
|
184
|
+
- 中断重开后可依据 `run_state` 和 `orchestrator_context` 恢复
|
|
185
|
+
- 每轮输出明确下一步命令,不要求用户自行推断
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 10. 版本节奏对齐
|
|
190
|
+
|
|
191
|
+
- `v1.x`:拆解 + 规划
|
|
192
|
+
- `v2.x`:多 Agent 执行(Beta)
|
|
193
|
+
- `v3.x`:多 Agent 闭环(执行 + 回填 + 验收)
|
package/package.json
CHANGED
|
@@ -472,6 +472,21 @@ spec-agent execute --workspace ./workspace --fail T003 --error "build failed"
|
|
|
472
472
|
|
|
473
473
|
---
|
|
474
474
|
|
|
475
|
+
### 3.14 orchestrate — 产物驱动决策(v2 beta)
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
spec-agent orchestrate --workspace ./workspace --input ./docs --target cursor --max-parallel 4
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**功能**:
|
|
482
|
+
|
|
483
|
+
- 读取 `manifest/spec/task_plan/dispatch/handoff/run_state/report` 等产物
|
|
484
|
+
- 输出统一决策上下文 `orchestrator_context.json`
|
|
485
|
+
- 基于当前产物状态给出下一条建议命令
|
|
486
|
+
- 支持 `--format json` 供上层 Agent/Skill 程序化消费
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
475
490
|
## 四、核心类型定义
|
|
476
491
|
|
|
477
492
|
```typescript
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { Logger } from '../utils/logger';
|
|
4
|
+
import { fileExists, readJson, writeJson } from '../utils/file';
|
|
5
|
+
|
|
6
|
+
interface OrchestrateOptions {
|
|
7
|
+
workspace: string;
|
|
8
|
+
input?: string;
|
|
9
|
+
target: string;
|
|
10
|
+
maxParallel: string;
|
|
11
|
+
format: string;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type OrchestratorState =
|
|
16
|
+
| 'INIT'
|
|
17
|
+
| 'READY'
|
|
18
|
+
| 'PIPELINE_RUNNING'
|
|
19
|
+
| 'PIPELINE_READY'
|
|
20
|
+
| 'EXECUTION_RUNNING'
|
|
21
|
+
| 'WAITING_FEEDBACK'
|
|
22
|
+
| 'PARTIAL_FAILED'
|
|
23
|
+
| 'DONE';
|
|
24
|
+
|
|
25
|
+
interface ExecutionSummary {
|
|
26
|
+
pending: number;
|
|
27
|
+
running: number;
|
|
28
|
+
succeeded: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
blocked: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface OrchestratorContext {
|
|
34
|
+
version: string;
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
workspace: string;
|
|
37
|
+
state: OrchestratorState;
|
|
38
|
+
checks: {
|
|
39
|
+
manifest: boolean;
|
|
40
|
+
specSummary: boolean;
|
|
41
|
+
taskPlan: boolean;
|
|
42
|
+
dispatchPlan: boolean;
|
|
43
|
+
handoffBundle: boolean;
|
|
44
|
+
runState: boolean;
|
|
45
|
+
executionReport: boolean;
|
|
46
|
+
};
|
|
47
|
+
metrics: ExecutionSummary;
|
|
48
|
+
lastAction: string | null;
|
|
49
|
+
lastError: string | null;
|
|
50
|
+
nextAction: string;
|
|
51
|
+
recommendedCommands: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ExecutionReportLike {
|
|
55
|
+
summary?: Partial<ExecutionSummary>;
|
|
56
|
+
lastEvents?: Array<{ action?: string; detail?: string; taskId?: string }>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RunStateLike {
|
|
60
|
+
tasks?: Record<string, { status?: string }>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function orchestrateCommand(options: OrchestrateOptions, command: Command): Promise<void> {
|
|
64
|
+
const logger = new Logger();
|
|
65
|
+
try {
|
|
66
|
+
const workspacePath = path.resolve(options.workspace);
|
|
67
|
+
const inputArg = options.input || './docs';
|
|
68
|
+
const maxParallel = Math.max(1, parseInt(options.maxParallel || '4', 10) || 4);
|
|
69
|
+
const target = (options.target || 'cursor').toLowerCase();
|
|
70
|
+
const contextPath = path.join(workspacePath, 'orchestrator_context.json');
|
|
71
|
+
const context = await buildOrchestratorContext({
|
|
72
|
+
workspaceArg: options.workspace,
|
|
73
|
+
inputArg,
|
|
74
|
+
target,
|
|
75
|
+
maxParallel,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!options.dryRun) {
|
|
79
|
+
await writeJson(contextPath, context);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.format === 'json') {
|
|
83
|
+
logger.json(context);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger.info(`Orchestrator state: ${context.state}`);
|
|
88
|
+
logger.info(`Next action: ${context.nextAction}`);
|
|
89
|
+
logger.info(
|
|
90
|
+
`Metrics: pending=${context.metrics.pending}, running=${context.metrics.running}, succeeded=${context.metrics.succeeded}, failed=${context.metrics.failed}, blocked=${context.metrics.blocked}`
|
|
91
|
+
);
|
|
92
|
+
logger.info(`Context file: ${contextPath}`);
|
|
93
|
+
logger.info('Recommended commands:');
|
|
94
|
+
for (const cmd of context.recommendedCommands) {
|
|
95
|
+
logger.info(` ${cmd}`);
|
|
96
|
+
}
|
|
97
|
+
if (options.dryRun) {
|
|
98
|
+
logger.warn('Dry run mode - context file not written');
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error(`Orchestrate failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function buildOrchestratorContext(input: {
|
|
107
|
+
workspaceArg: string;
|
|
108
|
+
inputArg: string;
|
|
109
|
+
target: string;
|
|
110
|
+
maxParallel: number;
|
|
111
|
+
}): Promise<OrchestratorContext> {
|
|
112
|
+
const workspacePath = path.resolve(input.workspaceArg);
|
|
113
|
+
const manifestPath = path.join(workspacePath, 'manifest.json');
|
|
114
|
+
const specPath = path.join(workspacePath, 'spec_summary.json');
|
|
115
|
+
const taskPlanPath = path.join(workspacePath, 'task_plan.json');
|
|
116
|
+
const dispatchPath = path.join(workspacePath, 'dispatch_plan.json');
|
|
117
|
+
const handoffPath = path.join(workspacePath, 'handoff', 'handoff_bundle.json');
|
|
118
|
+
const runStatePath = path.join(workspacePath, 'execution', 'run_state.json');
|
|
119
|
+
const reportPath = path.join(workspacePath, 'execution', 'execution_report.json');
|
|
120
|
+
const inputPath = path.resolve(input.inputArg);
|
|
121
|
+
|
|
122
|
+
const checks = {
|
|
123
|
+
manifest: await fileExists(manifestPath),
|
|
124
|
+
specSummary: await fileExists(specPath),
|
|
125
|
+
taskPlan: await fileExists(taskPlanPath),
|
|
126
|
+
dispatchPlan: await fileExists(dispatchPath),
|
|
127
|
+
handoffBundle: await fileExists(handoffPath),
|
|
128
|
+
runState: await fileExists(runStatePath),
|
|
129
|
+
executionReport: await fileExists(reportPath),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const metrics: ExecutionSummary = {
|
|
133
|
+
pending: 0,
|
|
134
|
+
running: 0,
|
|
135
|
+
succeeded: 0,
|
|
136
|
+
failed: 0,
|
|
137
|
+
blocked: 0,
|
|
138
|
+
};
|
|
139
|
+
let lastAction: string | null = null;
|
|
140
|
+
let lastError: string | null = null;
|
|
141
|
+
let runningTaskIds: string[] = [];
|
|
142
|
+
if (checks.executionReport) {
|
|
143
|
+
const report = await readJson<ExecutionReportLike>(reportPath);
|
|
144
|
+
metrics.pending = report.summary?.pending || 0;
|
|
145
|
+
metrics.running = report.summary?.running || 0;
|
|
146
|
+
metrics.succeeded = report.summary?.succeeded || 0;
|
|
147
|
+
metrics.failed = report.summary?.failed || 0;
|
|
148
|
+
metrics.blocked = report.summary?.blocked || 0;
|
|
149
|
+
const lastEvent = report.lastEvents && report.lastEvents.length > 0
|
|
150
|
+
? report.lastEvents[report.lastEvents.length - 1]
|
|
151
|
+
: null;
|
|
152
|
+
if (lastEvent?.action) {
|
|
153
|
+
lastAction = `${lastEvent.action}${lastEvent.taskId ? `:${lastEvent.taskId}` : ''}`;
|
|
154
|
+
}
|
|
155
|
+
if (lastEvent?.action === 'failed' || lastEvent?.action === 'blocked') {
|
|
156
|
+
lastError = lastEvent.detail || `${lastEvent.action} event detected`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (checks.runState) {
|
|
161
|
+
const runState = await readJson<RunStateLike>(runStatePath);
|
|
162
|
+
const tasks = runState.tasks || {};
|
|
163
|
+
runningTaskIds = Object.entries(tasks)
|
|
164
|
+
.filter(([, value]) => value.status === 'running')
|
|
165
|
+
.map(([taskId]) => taskId)
|
|
166
|
+
.sort((a, b) => a.localeCompare(b));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { state, nextAction, recommendedCommands } = decideNextAction({
|
|
170
|
+
checks,
|
|
171
|
+
metrics,
|
|
172
|
+
workspaceArg: input.workspaceArg,
|
|
173
|
+
inputArg: input.inputArg,
|
|
174
|
+
inputPathExists: await fileExists(inputPath),
|
|
175
|
+
target: input.target,
|
|
176
|
+
maxParallel: input.maxParallel,
|
|
177
|
+
runningTaskIds,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
version: '2.0.0-beta',
|
|
182
|
+
updatedAt: new Date().toISOString(),
|
|
183
|
+
workspace: workspacePath,
|
|
184
|
+
state,
|
|
185
|
+
checks,
|
|
186
|
+
metrics,
|
|
187
|
+
lastAction,
|
|
188
|
+
lastError,
|
|
189
|
+
nextAction,
|
|
190
|
+
recommendedCommands,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function decideNextAction(input: {
|
|
195
|
+
checks: OrchestratorContext['checks'];
|
|
196
|
+
metrics: ExecutionSummary;
|
|
197
|
+
workspaceArg: string;
|
|
198
|
+
inputArg: string;
|
|
199
|
+
inputPathExists: boolean;
|
|
200
|
+
target: string;
|
|
201
|
+
maxParallel: number;
|
|
202
|
+
runningTaskIds: string[];
|
|
203
|
+
}): { state: OrchestratorState; nextAction: string; recommendedCommands: string[] } {
|
|
204
|
+
const { checks, metrics, workspaceArg, inputArg, inputPathExists, target, maxParallel, runningTaskIds } = input;
|
|
205
|
+
const pipelineCmd = `spec-agent pipeline --input ${inputArg} --output ${workspaceArg}`;
|
|
206
|
+
const handoffCmd = `spec-agent handoff --workspace ${workspaceArg} --target ${target} --include-summaries`;
|
|
207
|
+
const executeCmd = `spec-agent execute --workspace ${workspaceArg} --max-parallel ${maxParallel}`;
|
|
208
|
+
const statusCmd = `spec-agent status --workspace ${workspaceArg}`;
|
|
209
|
+
|
|
210
|
+
if (!checks.dispatchPlan) {
|
|
211
|
+
if (!inputPathExists) {
|
|
212
|
+
return {
|
|
213
|
+
state: 'INIT',
|
|
214
|
+
nextAction: 'Prepare input docs directory before running pipeline',
|
|
215
|
+
recommendedCommands: [
|
|
216
|
+
`mkdir ${inputArg}`,
|
|
217
|
+
'# put your requirement documents under docs/',
|
|
218
|
+
pipelineCmd,
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
state: checks.manifest ? 'PIPELINE_RUNNING' : 'READY',
|
|
224
|
+
nextAction: 'Run pipeline to produce planning artifacts',
|
|
225
|
+
recommendedCommands: [pipelineCmd, statusCmd],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!checks.handoffBundle) {
|
|
230
|
+
return {
|
|
231
|
+
state: 'PIPELINE_READY',
|
|
232
|
+
nextAction: 'Generate handoff bundle for execution loop',
|
|
233
|
+
recommendedCommands: [handoffCmd, executeCmd],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!checks.runState) {
|
|
238
|
+
return {
|
|
239
|
+
state: 'EXECUTION_RUNNING',
|
|
240
|
+
nextAction: 'Initialize execution state and schedule first batch',
|
|
241
|
+
recommendedCommands: [executeCmd],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (metrics.running > 0) {
|
|
246
|
+
const completionIds = runningTaskIds.join(',');
|
|
247
|
+
const failExampleTask = runningTaskIds[0] || 'Txxx';
|
|
248
|
+
return {
|
|
249
|
+
state: 'WAITING_FEEDBACK',
|
|
250
|
+
nextAction: 'Collect worker feedback and apply --complete/--fail',
|
|
251
|
+
recommendedCommands: [
|
|
252
|
+
completionIds
|
|
253
|
+
? `spec-agent execute --workspace ${workspaceArg} --complete ${completionIds}`
|
|
254
|
+
: `spec-agent execute --workspace ${workspaceArg} --complete Txxx`,
|
|
255
|
+
`spec-agent execute --workspace ${workspaceArg} --fail ${failExampleTask} --error "compile failed"`,
|
|
256
|
+
executeCmd,
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (metrics.pending > 0) {
|
|
262
|
+
return {
|
|
263
|
+
state: 'EXECUTION_RUNNING',
|
|
264
|
+
nextAction: 'Schedule next executable batch',
|
|
265
|
+
recommendedCommands: [executeCmd],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (metrics.failed > 0 || metrics.blocked > 0) {
|
|
270
|
+
return {
|
|
271
|
+
state: 'PARTIAL_FAILED',
|
|
272
|
+
nextAction: 'Investigate failed/blocked tasks then retry with execute',
|
|
273
|
+
recommendedCommands: [executeCmd, statusCmd],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
state: 'DONE',
|
|
279
|
+
nextAction: 'Execution loop completed',
|
|
280
|
+
recommendedCommands: [statusCmd],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -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
|
+
}
|