@mzzsfy/dsh-rs-workflow 0.2.3 → 1.0.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.
- package/README.md +14 -49
- package/cordis.patch.yml +8 -17
- package/lib/board.mjs +355 -0
- package/lib/driver/approve.mjs +126 -0
- package/lib/driver/control.mjs +46 -0
- package/lib/driver/index.mjs +454 -0
- package/lib/driver/prompts.mjs +102 -0
- package/lib/driver/runner.mjs +173 -0
- package/lib/driver/scheduler.mjs +159 -0
- package/lib/index.js +30 -213
- package/lib/orchestrator.mjs +428 -0
- package/lib/planner-gate.mjs +195 -0
- package/lib/release.mjs +183 -0
- package/lib/settings-schema.mjs +73 -0
- package/lib/spec.mjs +161 -0
- package/lib/storage.mjs +35 -0
- package/lib/store.mjs +255 -0
- package/lib/template-tool.mjs +161 -0
- package/lib/template.mjs +341 -0
- package/package.json +8 -6
- package/src/client.js +1269 -0
- package/lib/preset-sync.mjs +0 -227
- package/preset/rs-workflow/agent.cordis.yml +0 -196
- package/preset/rs-workflow/preset.yml +0 -5
- package/preset/rs-workflow/skills/rs-workflow/SKILL.md +0 -110
- package/preset/rs-workflow/skills/rs-workflow/references/engine.js +0 -1446
- package/preset/rs-workflow/skills/rs-workflow/references/templates.md +0 -104
- package/preset/rs-workflow/skills/rs-workflow/slots.json5 +0 -53
|
@@ -1,1446 +0,0 @@
|
|
|
1
|
-
// ── rs-workflow 若水工作流编排脚本 v2(原始 rs-tui 语义强制对齐) ─────────────
|
|
2
|
-
// 由主代理经 workflow 工具调用:
|
|
3
|
-
// script = 本文件全文(原样, 不改写)
|
|
4
|
-
// args = { request, contextNotes?, slots?(16键), lockedTemplate?, defaultTemplate?, limits?, budgets?, prefix? }
|
|
5
|
-
// slots 值支持 string|{provider,model}|{rotation:[...]}|array(候选依次故障转移);
|
|
6
|
-
// budgets 四阈值 clamp [1,10]; defaultTemplate 'auto'/缺省 → multi-plan 兜底;
|
|
7
|
-
// prefix 为断点续跑种子 [{id, description, output?, changedFiles?}], 仅
|
|
8
|
-
// lite / plan-final / step-review 生效, multi-plan 忽略并记日志。
|
|
9
|
-
// 只用 agent / parallel / phase / log 钩子; 无 fs / network / timer / Node API。
|
|
10
|
-
// 结构化输出全部经 schema 约束; agent 返回 null 视为该次调用失败。
|
|
11
|
-
// 语义要点: 模板四级兜底链(锁定→planner 声明→矩阵→defaultTemplate); 审批
|
|
12
|
-
// fail-closed(审批者不可用视为拒绝+reviewerFault); APPROVED 必须附 evidence
|
|
13
|
-
// (重问预算 emptyOutputRetryLimit); 拒绝计数挂被审对象, plan 型阈值
|
|
14
|
-
// planRejectBeforeBlocked, 其余 reviewRejectBeforeEscalate, 达阈值升级重规划
|
|
15
|
-
// (ESCALATION_LIMIT 次后 blocked); pr 通过不清零升级账, 交付类通过清零;
|
|
16
|
-
// 调度预算按图规模每轮现算。
|
|
17
|
-
|
|
18
|
-
const A = args || {}
|
|
19
|
-
const REQ = String(A.request || '').trim()
|
|
20
|
-
if (!REQ) throw new Error('rs-workflow: 缺少 request(contextNotes 可空, request 必填)')
|
|
21
|
-
|
|
22
|
-
const TEMPLATES = ['lite', 'plan-final', 'step-review', 'multi-plan']
|
|
23
|
-
const SLOTS = (A.slots && typeof A.slots === 'object' && !Array.isArray(A.slots)) ? A.slots : {}
|
|
24
|
-
const LOCKED = TEMPLATES.indexOf(A.lockedTemplate) >= 0 ? A.lockedTemplate : ''
|
|
25
|
-
const CONTEXT_NOTES = String(A.contextNotes || '').trim()
|
|
26
|
-
const LIMITS = (A.limits && typeof A.limits === 'object' && !Array.isArray(A.limits)) ? A.limits : {}
|
|
27
|
-
const BUDGETS_SRC = (A.budgets && typeof A.budgets === 'object' && !Array.isArray(A.budgets)) ? A.budgets : {}
|
|
28
|
-
// 无信号兜底模板: 'auto'/非法/缺省 → multi-plan(原始 selectTemplate fallback 口径)
|
|
29
|
-
const DEFAULT_TEMPLATE = TEMPLATES.indexOf(A.defaultTemplate) >= 0 ? A.defaultTemplate : 'multi-plan'
|
|
30
|
-
// 任务上限: <=0/非数值回落缺省(自保口径与 lib schema .min(1) 一致, 非法值此处兜底)
|
|
31
|
-
const MAX_TASKS_DEFAULT = 8
|
|
32
|
-
const MAX_TASKS = LIMITS.maxTasks > 0 ? Math.min(Math.floor(Number(LIMITS.maxTasks) || 0), 64) || MAX_TASKS_DEFAULT : MAX_TASKS_DEFAULT
|
|
33
|
-
|
|
34
|
-
// ── budgets 四阈值: 引擎侧 clamp [1,10](workflow schema 不支持数值边界) ─────
|
|
35
|
-
const BUDGET_DEFAULTS = { reviewRejectBeforeEscalate: 2, planRejectBeforeBlocked: 2, emptyOutputRetryLimit: 3, reportNudgeLimit: 3 }
|
|
36
|
-
const BUDGET_MIN = 1
|
|
37
|
-
const BUDGET_MAX = 10
|
|
38
|
-
function clampBudget(name) {
|
|
39
|
-
const raw = BUDGETS_SRC[name]
|
|
40
|
-
if (raw === undefined || raw === null) return BUDGET_DEFAULTS[name]
|
|
41
|
-
// 空串与纯空白同为无信号输入, 一并回落缺省
|
|
42
|
-
const text = String(raw).trim()
|
|
43
|
-
if (text === '') return BUDGET_DEFAULTS[name]
|
|
44
|
-
const n = Math.floor(Number(text))
|
|
45
|
-
if (!isFinite(n)) return BUDGET_DEFAULTS[name]
|
|
46
|
-
// 数值一律钳入 [1,10](与 lib 侧 zod .min(1) 口径一致), 0<x<1 与负值同样钳到下界
|
|
47
|
-
return Math.min(BUDGET_MAX, Math.max(BUDGET_MIN, n))
|
|
48
|
-
}
|
|
49
|
-
const BUDGET = {
|
|
50
|
-
reviewRejectBeforeEscalate: clampBudget('reviewRejectBeforeEscalate'),
|
|
51
|
-
planRejectBeforeBlocked: clampBudget('planRejectBeforeBlocked'),
|
|
52
|
-
emptyOutputRetryLimit: clampBudget('emptyOutputRetryLimit'),
|
|
53
|
-
reportNudgeLimit: clampBudget('reportNudgeLimit'),
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const ESCALATION_LIMIT = 2
|
|
57
|
-
const LOOP_BUDGET_BASE = 8
|
|
58
|
-
const LOOP_BUDGET_PER_NODE = 3
|
|
59
|
-
// 每次升级最多新增的任务数(重规划上限), 供调度预算按升级次数增额
|
|
60
|
-
const LOOP_BUDGET_PER_ESCALATION = MAX_TASKS * LOOP_BUDGET_PER_NODE
|
|
61
|
-
// 截断常量(对齐原始 HandoffSummary/规划口径)
|
|
62
|
-
const REQUEST_CHARS = 2 * 1000
|
|
63
|
-
const KEY_OUTPUT_CHARS = 500
|
|
64
|
-
const DONE_WINDOW = 10
|
|
65
|
-
const PLAN_EXCERPT_CHARS = 2 * 1000
|
|
66
|
-
const EXEC_SUMMARY_CHARS = 500
|
|
67
|
-
const ELLIPSIS = '...[已压缩]'
|
|
68
|
-
const TRIAGE_ATTEMPTS = 2
|
|
69
|
-
// 单任务连续执行失败原地重试的机器保险丝(正常路径由阈值先触发)
|
|
70
|
-
// 单任务原地重试保险丝(阈值+2): 仅防 fail/reject 合并记账判定被异常绕过后的死循环, 非业务阈值
|
|
71
|
-
const FAIL_RETRY_FUSE = BUDGET.reviewRejectBeforeEscalate + 2
|
|
72
|
-
|
|
73
|
-
const SUBJECT_OVERALL = 'overall'
|
|
74
|
-
const SUBJECT_PLAN = 'plan'
|
|
75
|
-
const PLAN_REVIEW_NODE = 'pr'
|
|
76
|
-
const PREFIX_ID_MARK = 'x'
|
|
77
|
-
const ALL_DONE = '无,全部完成'
|
|
78
|
-
const NONE = '无'
|
|
79
|
-
// 蓝图预留 id 模式: pr(计划审)/fr(终审)/srN(子计划审)/xrN(交叉终审)/r-*(任务审前缀)。
|
|
80
|
-
// planner 声明撞名会与运行期 review 节点同 id 错位, normalizeTasks 统一强制重编号
|
|
81
|
-
const RESERVED_ID_TEST = /^(pr|fr|sr\d+|xr\d+|r-.+)$/
|
|
82
|
-
|
|
83
|
-
// task 节点进入语境 → executor 槽位(原始 TASK_SLOT_BY_REASON 映射)
|
|
84
|
-
const TASK_SLOT_BY_REASON = {
|
|
85
|
-
advance: 'executor-task',
|
|
86
|
-
reject: 'executor-enhance',
|
|
87
|
-
fail: 'executor-retry',
|
|
88
|
-
escalate: 'executor-escalate',
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 各模板执行语气(原始 TASK_TONE)
|
|
92
|
-
const TASK_TONE = {
|
|
93
|
-
lite: '改动自行运行验证,报告需包含验证结果,终审只兜底',
|
|
94
|
-
'plan-final': '严格按计划顺序执行,每完成一个任务报告进度',
|
|
95
|
-
'step-review': '每完成一个任务报告变更与自验结果供审',
|
|
96
|
-
'multi-plan': '子计划内按细化方案执行,子计划完成输出交付清单',
|
|
97
|
-
}
|
|
98
|
-
const TASK_TONE_DEFAULT = '完成后按 schema 返回执行结果'
|
|
99
|
-
|
|
100
|
-
// 审批契约固定文案: fail-closed 与证据门槛的对外理由
|
|
101
|
-
const REVIEWER_UNAVAILABLE_REASON = '审批者不可用(视为拒绝), 可原样重交'
|
|
102
|
-
const REVIEWER_UNAVAILABLE_PLAN_REASON = '审批者不可用(视为拒绝), 将带此原因重新规划'
|
|
103
|
-
const EVIDENCE_REQUIRED_REASON = '审批缺少验证证据(视为拒绝), 补充证据后可原样重交'
|
|
104
|
-
const EVIDENCE_REASK_NOTE = '\n(审批必须附验证证据: evidence 填实际执行的检查命令与结果要点, 否则视为驳回)'
|
|
105
|
-
const REVIEW_RESEND_NOTE = '\n裁决重申: 审查完成后按 schema 返回裁决, verdict 取 APPROVED 或 REJECTED, reasons 写结论理由。'
|
|
106
|
-
const REPORT_NUDGE_NOTE = '\n【回传补救】上一轮回传缺少有效任务报告(summary 为空白), 无法结算。请基于已有进度继续: 核对工作区实况, 剩余工作完成后按 schema 返回完整结果(status=completed|failed, summary=一句话结果, changedFiles=变更文件列表)。'
|
|
107
|
-
|
|
108
|
-
// 范围核查与各审批基准
|
|
109
|
-
const SCOPE_RULE = '范围核查(强制规则): 以 git diff / git status 的实际变更为准; 实际变更命中【非本任务范围的申报文件】清单的, 是其他任务/已完成工作, 不算越界; 除此之外未在申报清单中出现且不在豁免清单中的文件 → verdict=REJECTED 并在 reasons 点名越界文件。'
|
|
110
|
-
const OVERALL_SCOPE_RULE = '终审范围核查(强制规则): 汇总各任务申报文件的并集, 与全量实际变更比对, 并集之外的文件 → verdict=REJECTED 并在 reasons 点名。'
|
|
111
|
-
const GIT_EVIDENCE_RULE = '以只读命令收集 git 证据(git diff / git status 等变更统计与工作区状态), 与待审内容对照, 不信自报 summary。'
|
|
112
|
-
const PLAN_REVIEW_CRITERIA = '审批基准(计划审批): 计划是否可执行、任务粒度是否均匀且可独立验收、依赖是否成立、并行任务文件是否互斥。'
|
|
113
|
-
const TASK_REVIEW_CRITERIA = '审批基准(可判定清单): 1) 只审本任务范围内的交付; 2) 每条问题必须附证据(测试名/命令输出/file:line), 纯叙述不算; 3) 不确定的点写进 summary, 不猜测; 4) critical 级问题必须同时出现在 reasons; 5) 被审对象是否真正完成且正确, 是否引入明显缺陷、回归或破坏无关功能。'
|
|
114
|
-
const SUBPLAN_REVIEW_CRITERIA = '审批基准(可判定清单): 本子计划交付是否完整达成子计划目标; 每条问题必须附证据(测试名/命令输出/file:line), 纯叙述不算; 不确定的点写进 summary; critical 级问题必须出现在 reasons。'
|
|
115
|
-
const OVERALL_REVIEW_CRITERIA = '审批基准(可判定清单): 站在整体交付视角, 全部任务产出是否完整满足原始需求; 每条问题必须附证据(测试名/命令输出/file:line), 纯叙述不算; 不确定的点写进 summary; critical 级问题必须出现在 reasons。'
|
|
116
|
-
const PARALLEL_NOTE = '并行任务提示: 其他并行任务可能同时变更工作区,只处理本任务边界内的文件。\n工作区可能含其他并行任务的中间态改动,发现非本任务预期的未完成变更时,不得回滚或覆盖,实现应适应这些变更继续工作。'
|
|
117
|
-
|
|
118
|
-
// 角色身份行(原始协议型短身份 + 规则列表, schema 契约行留尾部)
|
|
119
|
-
const EXECUTOR_IDENTITY = [
|
|
120
|
-
'[rs executor] 执行编码任务。',
|
|
121
|
-
'',
|
|
122
|
-
'规则:',
|
|
123
|
-
'- 专注当前子任务, 不重新规划, 不改动与任务无关的文件, 不启动新的 workflow 或子代理',
|
|
124
|
-
'- 不得回滚非本任务引入的变更, 发现其他任务的改动时在其基础上工作',
|
|
125
|
-
'- 失败不伪造: 无法完成时如实上报失败并说明原因',
|
|
126
|
-
].join('\n')
|
|
127
|
-
const REVIEWER_IDENTITY = [
|
|
128
|
-
'[rs reviewer] 审查代码或计划。',
|
|
129
|
-
'',
|
|
130
|
-
'规则:',
|
|
131
|
-
'- 工作结果审查基于 git 证据(变更统计/工作区状态)与待审内容对照,不信自报 summary',
|
|
132
|
-
'- 只审批: 不修改任何文件, 不启动新的 workflow 或子代理',
|
|
133
|
-
'- "代码看起来对"不算验证, 必须实际运行验证',
|
|
134
|
-
'- "实现者自报测试通过"须独立复跑核验',
|
|
135
|
-
'- "应该没问题"不算验证, 未验证的点写明不确定',
|
|
136
|
-
'- 无法裁决时 REJECTED 并在 reasons 中说明疑问',
|
|
137
|
-
].join('\n')
|
|
138
|
-
const REVIEWER_FINAL_IDENTITY = [
|
|
139
|
-
'[rs reviewer] 终审全部交付结果。',
|
|
140
|
-
'',
|
|
141
|
-
'规则:',
|
|
142
|
-
'- 站在整体交付视角审查: 全部任务的最终产出是否完整满足原始需求',
|
|
143
|
-
'- 工作结果审查基于 git 证据(变更统计/工作区状态)与全部待审内容对照,不信自报 summary',
|
|
144
|
-
'- 只审批: 不修改任何文件, 不启动新的 workflow 或子代理',
|
|
145
|
-
'- "代码看起来对"不算验证, 必须实际运行验证',
|
|
146
|
-
'- "实现者自报测试通过"须独立复跑核验',
|
|
147
|
-
'- "应该没问题"不算验证, 未验证的点写明不确定',
|
|
148
|
-
'- 无法裁决时 REJECTED 并在 reasons 中说明疑问',
|
|
149
|
-
].join('\n')
|
|
150
|
-
const PLANNER_IDENTITY = [
|
|
151
|
-
'[rs planner] 制定计划。',
|
|
152
|
-
'',
|
|
153
|
-
'规则:',
|
|
154
|
-
'- 输出简洁的执行计划: 关键步骤 + 注意事项',
|
|
155
|
-
'- 只规划, 不执行编码, 不修改文件',
|
|
156
|
-
].join('\n')
|
|
157
|
-
|
|
158
|
-
const EXEC_REPORT_CONTRACT = '完成后按 schema 返回: status=completed|failed; summary=给后续节点的交接摘要(做了什么/验证结果/遗留注意); changedFiles=你变更文件的相对路径清单。'
|
|
159
|
-
const REVIEW_REPORT_CONTRACT = '按 schema 返回: verdict=APPROVED|REJECTED; severity=可选问题分级(critical/important/minor); reasons=驳回时必须给出具体、可执行的修改意见; summary=审批结论摘要(不确定的点写在这里); evidence=实际执行的检查命令与结果要点(APPROVED 必填, 空证据按驳回处理)。无法裁决时 REJECTED 并在 reasons 中说明疑问。'
|
|
160
|
-
const PLAN_FIELD_REQUIREMENT = 'plan 字段: 简明实现计划(目标/方案要点/验证方式), 关键文件用精确路径列出(比论述抗截断)。'
|
|
161
|
-
const REPLAN_HEAD = '此前工作流多次未通过审批或执行失败,需要升级重规划。'
|
|
162
|
-
|
|
163
|
-
// ── 工作位候选链: 细分位 → 同域基础位 → 会话默认模型 ─────────────────────────
|
|
164
|
-
// 值支持 string('provider/model'|'model') | {provider,model} | {rotation:[...]} | array
|
|
165
|
-
function parseBinding(b) {
|
|
166
|
-
if (!b) return null
|
|
167
|
-
if (typeof b === 'string') {
|
|
168
|
-
const i = b.indexOf('/')
|
|
169
|
-
return i > 0 ? { provider: b.slice(0, i), model: b.slice(i + 1) } : { model: b }
|
|
170
|
-
}
|
|
171
|
-
if (typeof b === 'object' && !Array.isArray(b)) {
|
|
172
|
-
const o = {}
|
|
173
|
-
if (typeof b.provider === 'string' && b.provider) o.provider = b.provider
|
|
174
|
-
if (typeof b.model === 'string' && b.model) o.model = b.model
|
|
175
|
-
return Object.keys(o).length ? o : null
|
|
176
|
-
}
|
|
177
|
-
return null
|
|
178
|
-
}
|
|
179
|
-
function bindingsOf(v) {
|
|
180
|
-
if (Array.isArray(v)) {
|
|
181
|
-
const out = []
|
|
182
|
-
for (const item of v) {
|
|
183
|
-
if (item && typeof item === 'object' && !Array.isArray(item) && Array.isArray(item.rotation)) out.push.apply(out, bindingsOf(item.rotation))
|
|
184
|
-
else {
|
|
185
|
-
const o = parseBinding(item)
|
|
186
|
-
if (o) out.push(o)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return out
|
|
190
|
-
}
|
|
191
|
-
if (v && typeof v === 'object' && Array.isArray(v.rotation)) return bindingsOf(v.rotation)
|
|
192
|
-
const o = parseBinding(v)
|
|
193
|
-
return o ? [o] : []
|
|
194
|
-
}
|
|
195
|
-
function slotOpts(slot) {
|
|
196
|
-
const direct = bindingsOf(SLOTS[slot])
|
|
197
|
-
if (direct.length) return direct
|
|
198
|
-
const base = bindingsOf(SLOTS[String(slot).split('-')[0]])
|
|
199
|
-
if (base.length) return base
|
|
200
|
-
return [{}]
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// 候选故障转移: 从游标位起依次试候选, 成功后游标指向下一候选(被拒重做/重问即换模型)
|
|
204
|
-
async function callAgent(holder, candidates, spec) {
|
|
205
|
-
const list = (candidates && candidates.length) ? candidates : [{}]
|
|
206
|
-
const start = (holder.i || 0) % list.length
|
|
207
|
-
for (let k = 0; k < list.length; k++) {
|
|
208
|
-
const idx = (start + k) % list.length
|
|
209
|
-
holder.lastTried = list[idx]
|
|
210
|
-
const r = await agent(spec.prompt, { label: spec.label, schema: spec.schema, ...(list[idx]) })
|
|
211
|
-
if (r) {
|
|
212
|
-
holder.i = (idx + 1) % list.length
|
|
213
|
-
return r
|
|
214
|
-
}
|
|
215
|
-
log(spec.label + ' 候选调用失败, 切换下一候选')
|
|
216
|
-
}
|
|
217
|
-
holder.i = (start + 1) % list.length
|
|
218
|
-
return null
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 重规划统一重试口径: callAgent 内部只做候选故障转移, 这里再给整轮一次重试,
|
|
222
|
-
// 瞬时失败/空产出两次皆空才交由调用方 blocked(与分诊 TRIAGE_ATTEMPTS 口径对齐)
|
|
223
|
-
const REPLAN_ATTEMPTS = 2
|
|
224
|
-
async function callReplanAgent(holder, candidates, spec) {
|
|
225
|
-
for (let attempt = 1; attempt <= REPLAN_ATTEMPTS; attempt++) {
|
|
226
|
-
const r = await callAgent(holder, candidates, spec)
|
|
227
|
-
if (r) return r
|
|
228
|
-
if (attempt < REPLAN_ATTEMPTS) log(spec.label + ' 无产出, 重试 ' + attempt + '/' + (REPLAN_ATTEMPTS - 1))
|
|
229
|
-
}
|
|
230
|
-
return null
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 失败语境的真实最后尝试候选身份([上次失败模型] 段取值)
|
|
234
|
-
function failedModelOf(holder) {
|
|
235
|
-
const b = holder.lastTried
|
|
236
|
-
const id = b ? [b.provider, b.model].filter(Boolean).join('/') : ''
|
|
237
|
-
return id || '(会话默认模型)'
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ── 确定性分诊矩阵: 缺失信号按 low/small 降级; 无信号由调用方走 defaultTemplate ──
|
|
241
|
-
function normalizeLevel(v, allowed) {
|
|
242
|
-
return allowed.indexOf(v) >= 0 ? v : ''
|
|
243
|
-
}
|
|
244
|
-
const LEVELS_LMH = ['low', 'medium', 'high']
|
|
245
|
-
const LEVELS_SML = ['small', 'medium', 'large']
|
|
246
|
-
function chooseTemplate(s) {
|
|
247
|
-
const complexity = normalizeLevel(s.complexity, LEVELS_LMH)
|
|
248
|
-
const risk = normalizeLevel(s.risk, LEVELS_LMH)
|
|
249
|
-
const scope = normalizeLevel(s.scope, LEVELS_SML)
|
|
250
|
-
if (risk === 'high' || scope === 'large') return 'multi-plan'
|
|
251
|
-
if (complexity === 'high') return 'step-review'
|
|
252
|
-
if (complexity === 'medium' || risk === 'medium') return 'plan-final'
|
|
253
|
-
return 'lite'
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// 超限截断 + 省略标记
|
|
257
|
-
function compress(text, limit) {
|
|
258
|
-
const s = String(text || '')
|
|
259
|
-
return s.length <= limit ? s : s.slice(0, limit - ELLIPSIS.length) + ELLIPSIS
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ── schema(仅用 type/properties/required/items/enum) ────────────────────────
|
|
263
|
-
const TASK_ITEM = {
|
|
264
|
-
type: 'object',
|
|
265
|
-
properties: {
|
|
266
|
-
id: { type: 'string' },
|
|
267
|
-
description: { type: 'string' },
|
|
268
|
-
acceptance: { type: 'string' },
|
|
269
|
-
files: { type: 'array', items: { type: 'string' } },
|
|
270
|
-
after: { type: 'array', items: { type: 'string' } },
|
|
271
|
-
},
|
|
272
|
-
required: ['id', 'description'],
|
|
273
|
-
}
|
|
274
|
-
const TASKS_FIELD = { type: 'array', items: TASK_ITEM }
|
|
275
|
-
const SUBPLAN_ITEM = {
|
|
276
|
-
type: 'object',
|
|
277
|
-
properties: {
|
|
278
|
-
id: { type: 'string' },
|
|
279
|
-
title: { type: 'string' },
|
|
280
|
-
description: { type: 'string' },
|
|
281
|
-
after: { type: 'array', items: { type: 'string' } },
|
|
282
|
-
},
|
|
283
|
-
required: ['title', 'description'],
|
|
284
|
-
}
|
|
285
|
-
const PLAN_SCHEMA = {
|
|
286
|
-
type: 'object',
|
|
287
|
-
properties: {
|
|
288
|
-
templateId: { type: 'string' },
|
|
289
|
-
complexity: { type: 'string', enum: LEVELS_LMH },
|
|
290
|
-
risk: { type: 'string', enum: LEVELS_LMH },
|
|
291
|
-
scope: { type: 'string', enum: LEVELS_SML },
|
|
292
|
-
reasoning: { type: 'string' },
|
|
293
|
-
plan: { type: 'string' },
|
|
294
|
-
tasks: TASKS_FIELD,
|
|
295
|
-
subplans: { type: 'array', items: SUBPLAN_ITEM },
|
|
296
|
-
},
|
|
297
|
-
required: ['plan', 'tasks'],
|
|
298
|
-
}
|
|
299
|
-
const EXEC_SCHEMA = {
|
|
300
|
-
type: 'object',
|
|
301
|
-
properties: {
|
|
302
|
-
status: { type: 'string', enum: ['completed', 'failed'] },
|
|
303
|
-
summary: { type: 'string' },
|
|
304
|
-
changedFiles: { type: 'array', items: { type: 'string' } },
|
|
305
|
-
},
|
|
306
|
-
required: ['status', 'summary'],
|
|
307
|
-
}
|
|
308
|
-
const REVIEW_SCHEMA = {
|
|
309
|
-
type: 'object',
|
|
310
|
-
properties: {
|
|
311
|
-
verdict: { type: 'string', enum: ['APPROVED', 'REJECTED'] },
|
|
312
|
-
severity: { type: 'string', enum: ['critical', 'important', 'minor'] },
|
|
313
|
-
reasons: { type: 'array', items: { type: 'string' } },
|
|
314
|
-
summary: { type: 'string' },
|
|
315
|
-
evidence: { type: 'string' },
|
|
316
|
-
},
|
|
317
|
-
required: ['verdict', 'summary', 'evidence'],
|
|
318
|
-
}
|
|
319
|
-
const REPLAN_SCHEMA = {
|
|
320
|
-
type: 'object',
|
|
321
|
-
properties: {
|
|
322
|
-
analysis: { type: 'string' },
|
|
323
|
-
tasks: TASKS_FIELD,
|
|
324
|
-
},
|
|
325
|
-
required: ['tasks'],
|
|
326
|
-
}
|
|
327
|
-
const PLAN_REPLAN_SCHEMA = {
|
|
328
|
-
type: 'object',
|
|
329
|
-
properties: {
|
|
330
|
-
analysis: { type: 'string' },
|
|
331
|
-
plan: { type: 'string' },
|
|
332
|
-
tasks: TASKS_FIELD,
|
|
333
|
-
subplans: { type: 'array', items: SUBPLAN_ITEM },
|
|
334
|
-
},
|
|
335
|
-
required: ['plan', 'tasks'],
|
|
336
|
-
}
|
|
337
|
-
const SUBPLAN_GEN_SCHEMA = {
|
|
338
|
-
type: 'object',
|
|
339
|
-
properties: {
|
|
340
|
-
plan: { type: 'string' },
|
|
341
|
-
tasks: TASKS_FIELD,
|
|
342
|
-
},
|
|
343
|
-
required: ['plan', 'tasks'],
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// 拆解规则(cap = 当前可用任务预算)
|
|
347
|
-
function decompRules(cap) {
|
|
348
|
-
return [
|
|
349
|
-
'拆解规则: 每个任务是可独立验收的工作单元(自带必要的验证); 粒度均匀, 单任务应能在一个代理会话内完成; 有合并冲突风险的任务必须用 after 声明依赖串行 —— 无依赖关系的任务才会被并行执行, 并行候选必须文件互不相交。',
|
|
350
|
-
'依赖声明: after 列出前置任务 id; 省略 after = 链式接续上一个任务。本规则同样适用于子计划内的任务: 文件互斥的任务可显式 after: [] 并行, 有冲突必须声明依赖。',
|
|
351
|
-
'每任务必须给 acceptance(可独立验证的完成判据)与 files(预期触达文件清单); 禁止 TBD、"适当处理"式占位描述。',
|
|
352
|
-
'任务数匹配变更量级, 禁止机械均分; 任务数不超过 ' + cap + ' 个, 超出说明粒度不对, 合并相近任务。',
|
|
353
|
-
].join('\n')
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ── 任务表规范化: 补 id / 链式缺省 / 清未知引用 / 破环 / 预算截断 ────────────
|
|
357
|
-
function normalizeTasks(raw, idPrefix, seedSeen, cap) {
|
|
358
|
-
const seen = {}
|
|
359
|
-
if (seedSeen) for (const k in seedSeen) seen[k] = true
|
|
360
|
-
const picked = []
|
|
361
|
-
// 撞名/预留重编号的映射: planner 在 after 里引用声明 id, 需同步改到新 id
|
|
362
|
-
const idRemap = {}
|
|
363
|
-
const list = Array.isArray(raw) ? raw : []
|
|
364
|
-
for (const t of list) {
|
|
365
|
-
if (!t || typeof t.description !== 'string' || !t.description.trim()) continue
|
|
366
|
-
let id = (typeof t.id === 'string' && t.id.trim()) ? t.id.trim() : ''
|
|
367
|
-
// 预留 id 一并重编号, 防任务节点与蓝图固定 review 节点同 id 错位
|
|
368
|
-
if (!id || seen[id] || RESERVED_ID_TEST.test(id)) {
|
|
369
|
-
const declared = id
|
|
370
|
-
let k = 0
|
|
371
|
-
do { k++; id = idPrefix + k } while (seen[id])
|
|
372
|
-
// 首写优先: 重复声明同一 id 时, after 引用指到首个重编号任务而非被后者覆盖
|
|
373
|
-
if (declared && idRemap[declared] === undefined) idRemap[declared] = id
|
|
374
|
-
}
|
|
375
|
-
seen[id] = true
|
|
376
|
-
picked.push({
|
|
377
|
-
id: id,
|
|
378
|
-
description: t.description.trim(),
|
|
379
|
-
acceptance: typeof t.acceptance === 'string' ? t.acceptance.trim() : '',
|
|
380
|
-
files: Array.isArray(t.files) ? t.files.filter(function (x) { return typeof x === 'string' }) : [],
|
|
381
|
-
after: Array.isArray(t.after) ? t.after.filter(function (x) { return typeof x === 'string' }) : null,
|
|
382
|
-
})
|
|
383
|
-
}
|
|
384
|
-
if (picked.length > cap) {
|
|
385
|
-
log('任务数超预算 ' + cap + ', 截断保留前 ' + cap + ' 个')
|
|
386
|
-
picked.length = cap
|
|
387
|
-
}
|
|
388
|
-
const out = picked
|
|
389
|
-
const ids = {}
|
|
390
|
-
for (const t of out) ids[t.id] = true
|
|
391
|
-
out.forEach(function (t, i) {
|
|
392
|
-
if (!t.after) t.after = i > 0 ? [out[i - 1].id] : []
|
|
393
|
-
// 先同步重编号映射, 再按白名单过滤(引用不存在 id 静默丢弃)
|
|
394
|
-
t.after = t.after.map(function (x) { return idRemap[x] || x }).filter(function (x) { return ids[x] && x !== t.id })
|
|
395
|
-
})
|
|
396
|
-
const resolved = {}
|
|
397
|
-
let changed = true
|
|
398
|
-
while (changed) {
|
|
399
|
-
changed = false
|
|
400
|
-
for (const t of out) {
|
|
401
|
-
if (resolved[t.id]) continue
|
|
402
|
-
let ok = true
|
|
403
|
-
for (const d of t.after) if (!resolved[d]) { ok = false; break }
|
|
404
|
-
if (ok) { resolved[t.id] = true; changed = true }
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
for (const t of out) {
|
|
408
|
-
if (!resolved[t.id]) { t.after = []; log('依赖成环, 已解除 ' + t.id + ' 的前置约束') }
|
|
409
|
-
}
|
|
410
|
-
return out
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// prefix 续跑种子校验: 只保留可识别的已完成任务条目, id 去重
|
|
414
|
-
function validPrefix(raw) {
|
|
415
|
-
if (!Array.isArray(raw)) return []
|
|
416
|
-
const seen = {}
|
|
417
|
-
const out = []
|
|
418
|
-
for (const p of raw) {
|
|
419
|
-
if (!p || typeof p.id !== 'string' || !p.id.trim() || typeof p.description !== 'string' || !p.description.trim()) continue
|
|
420
|
-
const id = p.id.trim()
|
|
421
|
-
if (seen[id]) continue
|
|
422
|
-
seen[id] = true
|
|
423
|
-
out.push({
|
|
424
|
-
id: id,
|
|
425
|
-
description: p.description.trim(),
|
|
426
|
-
output: String(p.output || ''),
|
|
427
|
-
changedFiles: Array.isArray(p.changedFiles) ? p.changedFiles.filter(function (x) { return typeof x === 'string' }) : [],
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
return out
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// ── 引擎状态 ────────────────────────────────────────────────────────────────
|
|
434
|
-
const nodes = []
|
|
435
|
-
let escalations = 0
|
|
436
|
-
// 全程累计升级次数(不随审批通过清零), 供调度预算增额
|
|
437
|
-
let lifetimeEscalations = 0
|
|
438
|
-
let blocked = null
|
|
439
|
-
let escalating = false
|
|
440
|
-
let triage = null
|
|
441
|
-
let templateId = ''
|
|
442
|
-
let OUTLINE = []
|
|
443
|
-
let PLAN_TEXT = ''
|
|
444
|
-
let templateSource = ''
|
|
445
|
-
let replanSeq = 0
|
|
446
|
-
let PREFIX_ITEMS = []
|
|
447
|
-
let PREFIX_IDS = []
|
|
448
|
-
|
|
449
|
-
function addNode(n) { nodes.push(n); return n }
|
|
450
|
-
function taskNode(id, description, after, planSource, extra) {
|
|
451
|
-
const n = addNode({
|
|
452
|
-
id: id, type: 'task', description: description, deps: (after || []).slice(), status: 'pending',
|
|
453
|
-
output: '', changedFiles: [], plannedFiles: [], acceptance: '',
|
|
454
|
-
failCount: 0, rejectCount: 0, nudgeCount: 0,
|
|
455
|
-
enterReason: 'advance', rejectPrefix: '', failedModel: '', siblings: [],
|
|
456
|
-
planSource: planSource || 'PLAN', seed: false, outlineDeps: [],
|
|
457
|
-
reviewNote: '', dead: false, cursor: { i: 0 },
|
|
458
|
-
})
|
|
459
|
-
if (extra) Object.assign(n, extra)
|
|
460
|
-
return n
|
|
461
|
-
}
|
|
462
|
-
function reviewNode(id, description, deps, subject, slot, kind) {
|
|
463
|
-
return addNode({
|
|
464
|
-
id: id, type: 'review', description: description, deps: (deps || []).slice(), status: 'pending',
|
|
465
|
-
output: '', kind: kind, subject: subject || '', slot: slot || '',
|
|
466
|
-
rejectCount: 0, fixNote: '', reviewerFault: false,
|
|
467
|
-
verdict: '', reviewEvidence: '', warn: false, dead: false, cursor: { i: 0 },
|
|
468
|
-
})
|
|
469
|
-
}
|
|
470
|
-
function planNode(id, description, deps, outlineIndex) {
|
|
471
|
-
return addNode({
|
|
472
|
-
id: id, type: 'plan', description: description, deps: (deps || []).slice(), status: 'pending',
|
|
473
|
-
output: '', outlineIndex: outlineIndex, outlineDeps: [], rejectCount: 0,
|
|
474
|
-
dead: false, cursor: { i: 0 },
|
|
475
|
-
})
|
|
476
|
-
}
|
|
477
|
-
function subjOf(id) {
|
|
478
|
-
for (const n of nodes) if (n.id === id && !n.dead) return n
|
|
479
|
-
return null
|
|
480
|
-
}
|
|
481
|
-
function liveIds() {
|
|
482
|
-
const m = {}
|
|
483
|
-
for (const n of nodes) if (!n.dead) m[n.id] = true
|
|
484
|
-
return m
|
|
485
|
-
}
|
|
486
|
-
function liveTasks() {
|
|
487
|
-
return nodes.filter(function (n) { return !n.dead && n.type === 'task' })
|
|
488
|
-
}
|
|
489
|
-
// 全局任务预算: prefix 种子不计入
|
|
490
|
-
function remainingTaskBudget() {
|
|
491
|
-
let used = 0
|
|
492
|
-
for (const n of nodes) if (!n.dead && n.type === 'task' && !n.seed) used++
|
|
493
|
-
return MAX_TASKS - used
|
|
494
|
-
}
|
|
495
|
-
function doneTasks() {
|
|
496
|
-
return liveTasks().filter(function (n) { return n.status === 'done' })
|
|
497
|
-
}
|
|
498
|
-
function lastDoneTask() {
|
|
499
|
-
const done = doneTasks()
|
|
500
|
-
return done.length ? done[done.length - 1] : null
|
|
501
|
-
}
|
|
502
|
-
// changedFiles 收口上限: 条数与单条长度双重封顶, 防异常 executor 输出经 union
|
|
503
|
-
// 注入后续全部 reviewer/终审提示词(与 compress 截断纪律同口径)
|
|
504
|
-
const CHANGED_FILES_MAX = 200
|
|
505
|
-
const CHANGED_FILE_CHARS = 300
|
|
506
|
-
function unionChangedFiles() {
|
|
507
|
-
const out = []
|
|
508
|
-
for (const n of nodes) {
|
|
509
|
-
if (n.dead || n.type === 'review') continue
|
|
510
|
-
for (const f of (n.changedFiles || [])) if (out.indexOf(f) < 0) out.push(f)
|
|
511
|
-
}
|
|
512
|
-
const capped = out.slice(0, CHANGED_FILES_MAX).map(function (f) { return String(f).slice(0, CHANGED_FILE_CHARS) })
|
|
513
|
-
if (capped.length < out.length || capped.some(function (f, i) { return f.length !== String(out[i]).length })) {
|
|
514
|
-
log('[rs-workflow] changedFiles 超上限, 已截断: ' + out.length + ' 条 → ' + capped.length + ' 条')
|
|
515
|
-
}
|
|
516
|
-
return capped
|
|
517
|
-
}
|
|
518
|
-
function planTextFor(node) {
|
|
519
|
-
if (node.planSource === 'PLAN') return PLAN_TEXT
|
|
520
|
-
const p = subjOf(node.planSource)
|
|
521
|
-
return (p && String(p.output || '').trim()) || PLAN_TEXT
|
|
522
|
-
}
|
|
523
|
-
// 拒绝计数挂载点: subplan 挂子计划节点, task/带 task 主语终审挂任务, 其余挂审批节点自身
|
|
524
|
-
function reviewCountHolder(node) {
|
|
525
|
-
if (node.kind === 'plan') return node
|
|
526
|
-
if (node.kind === 'subplan' || node.kind === 'task') return subjOf(node.subject)
|
|
527
|
-
if (node.kind === 'final' && node.subject !== SUBJECT_OVERALL) return subjOf(node.subject)
|
|
528
|
-
return node
|
|
529
|
-
}
|
|
530
|
-
// plan 型被审对象(pr 计划/sr 子计划)阈值 = planRejectBeforeBlocked, 其余 = reviewRejectBeforeEscalate
|
|
531
|
-
function planTypeReview(node) {
|
|
532
|
-
return node.kind === 'plan' || node.kind === 'subplan'
|
|
533
|
-
}
|
|
534
|
-
// 从节点沿依赖前向可达(含 reject/fail 回边语义的依赖传播), 供尾段替换
|
|
535
|
-
function reachableFrom(startId) {
|
|
536
|
-
const bad = {}
|
|
537
|
-
bad[startId] = true
|
|
538
|
-
let changed = true
|
|
539
|
-
while (changed) {
|
|
540
|
-
changed = false
|
|
541
|
-
for (const n of nodes) {
|
|
542
|
-
if (bad[n.id]) continue
|
|
543
|
-
let hit = false
|
|
544
|
-
for (const d of n.deps) if (bad[d]) { hit = true; break }
|
|
545
|
-
if (hit) { bad[n.id] = true; changed = true }
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return bad
|
|
549
|
-
}
|
|
550
|
-
// 活跃节点前向首个 pending 后继(BFS 沿依赖, 可跨 done), 供交接摘要[下一步]
|
|
551
|
-
function nextPendingFrom(nodeId) {
|
|
552
|
-
const seen = {}
|
|
553
|
-
const queue = []
|
|
554
|
-
for (const n of nodes) if (!n.dead && n.deps.indexOf(nodeId) >= 0) queue.push(n.id)
|
|
555
|
-
while (queue.length) {
|
|
556
|
-
const id = queue.shift()
|
|
557
|
-
if (seen[id]) continue
|
|
558
|
-
seen[id] = true
|
|
559
|
-
const n = subjOf(id)
|
|
560
|
-
if (!n) continue
|
|
561
|
-
if (n.status === 'pending') return n.description
|
|
562
|
-
for (const m of nodes) if (!m.dead && m.deps.indexOf(id) >= 0) queue.push(m.id)
|
|
563
|
-
}
|
|
564
|
-
return ''
|
|
565
|
-
}
|
|
566
|
-
// 已完成子任务列表行: 最近 DONE_WINDOW 条带 keyOutput, 更早仅描述
|
|
567
|
-
function completedLines() {
|
|
568
|
-
const done = doneTasks()
|
|
569
|
-
const lines = []
|
|
570
|
-
done.forEach(function (n, i) {
|
|
571
|
-
const ko = i >= done.length - DONE_WINDOW ? compress(n.output, KEY_OUTPUT_CHARS) : ''
|
|
572
|
-
lines.push(ko ? (i + 1) + '. ' + n.description + ': ' + ko : (i + 1) + '. ' + n.description)
|
|
573
|
-
})
|
|
574
|
-
return lines.join('\n')
|
|
575
|
-
}
|
|
576
|
-
function doneSummaryText() { return completedLines() }
|
|
577
|
-
|
|
578
|
-
// ── 交接摘要(原始 HandoffSummary 序列化格式) ────────────────────────────────
|
|
579
|
-
function handoff(currentDesc, opts) {
|
|
580
|
-
const o = opts || {}
|
|
581
|
-
const sections = ['[原始需求] ' + compress(REQ, REQUEST_CHARS)]
|
|
582
|
-
// 进度只统计真实任务: prefix 种子已完成, 计入分母会稀释进度感知
|
|
583
|
-
// (种子进度已由 prefixHandoff 的断点续跑段表达)
|
|
584
|
-
const tasks = liveTasks().filter(function (n) { return !n.seed })
|
|
585
|
-
const total = tasks.length
|
|
586
|
-
if (total > 0) {
|
|
587
|
-
let idx = -1
|
|
588
|
-
for (let i = 0; i < total; i++) if (tasks[i].status !== 'done') { idx = i; break }
|
|
589
|
-
// 口径标注: [已完成子任务] 含续跑种子, 不标注会让两段数字看起来自相矛盾
|
|
590
|
-
const note = PREFIX_IDS.length > 0 ? '(不含 ' + PREFIX_IDS.length + ' 条续跑已完成记录)' : ''
|
|
591
|
-
sections.push('[进度] ' + (idx < 0 ? total : idx + 1) + '/' + total + note)
|
|
592
|
-
}
|
|
593
|
-
sections.push('[已完成子任务]\n' + (completedLines() || NONE))
|
|
594
|
-
sections.push('[当前子任务]\n' + currentDesc)
|
|
595
|
-
const parallel = o.parallel || []
|
|
596
|
-
if (parallel.length) sections.push('[并行执行中]\n' + parallel.map(function (d) { return '- ' + d }).join('\n'))
|
|
597
|
-
sections.push('[下一步]\n' + (o.next || ALL_DONE))
|
|
598
|
-
const files = unionChangedFiles()
|
|
599
|
-
sections.push('[关键文件变更]\n' + (files.length ? files.map(function (f) { return '- ' + f }).join('\n') : NONE))
|
|
600
|
-
if (o.failedModel) sections.push('[上次失败模型] ' + o.failedModel)
|
|
601
|
-
if (o.forced) sections.push('[强制续跑] 请继续推进工作,不要停止。')
|
|
602
|
-
return sections.join('\n\n')
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// prefix 续跑清单注入(规划/重规划上下文)
|
|
607
|
-
function prefixHandoff() {
|
|
608
|
-
if (!PREFIX_ITEMS.length) return ''
|
|
609
|
-
return '【已完成工作(断点续跑, 禁止重复规划)】\n' + PREFIX_ITEMS.map(function (p) {
|
|
610
|
-
return '- [' + PREFIX_ID_MARK + p.id + '] ' + p.description + ' → ' + String(p.output || '完成').slice(0, KEY_OUTPUT_CHARS)
|
|
611
|
-
}).join('\n') + '\n只规划剩余任务; 与已完成工作重复的任务不要产出。'
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// 拒绝重入前缀三分支(reviewerFault 折算/正常拒绝/裸拒绝)
|
|
615
|
-
function rejectionPrefix(reasons, reviewerFault) {
|
|
616
|
-
const text = reasons.filter(Boolean).join('; ')
|
|
617
|
-
if (reviewerFault) return '审批环节未产出有效结论(原因: ' + text + '),交付未被实质否决;若无修改可原样重新提交,等待复审。'
|
|
618
|
-
const bare = text.trim() ? '' : '\n审阅者未给出具体理由,请对照待审内容自查证据缺口与明显缺陷后再提交,勿原样重交。'
|
|
619
|
-
return '审批不通过,原因: ' + text + '\n请修改后重新提交。' + bare
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// ── 提示词构建 ──────────────────────────────────────────────────────────────
|
|
623
|
-
// 首次分诊规划提示词(原始四步教学, 第四步改 schema 提交, XML 示例改同构 JSON)
|
|
624
|
-
function plannerPrompt() {
|
|
625
|
-
const parts = [
|
|
626
|
-
PLANNER_IDENTITY,
|
|
627
|
-
'',
|
|
628
|
-
'请对以下需求进行工作流规划,分四步完成:',
|
|
629
|
-
'',
|
|
630
|
-
'第一步 评估三信号:',
|
|
631
|
-
'- complexity(复杂度): low / medium / high',
|
|
632
|
-
'- risk(风险): low / medium / high',
|
|
633
|
-
'- scope(变更范围): small / medium / large',
|
|
634
|
-
'无法评估的信号留空不填, 引擎按低档处理。',
|
|
635
|
-
'',
|
|
636
|
-
'第二步 选择工作流模板:',
|
|
637
|
-
'- lite 单发终审: 简单明确,一眼能看完的活',
|
|
638
|
-
'- plan-final 计划终审: 中等,需先想清步骤,执行可信',
|
|
639
|
-
'- step-review 逐步审批: 多步且每步质量敏感',
|
|
640
|
-
'- multi-plan 多计划交叉: 多功能大型,多计划多验证',
|
|
641
|
-
'信号与模板的对应关系:高风险或大范围用 multi-plan;高复杂度用 step-review;中复杂度或中风险用 plan-final;低复杂低风险小范围用 lite。',
|
|
642
|
-
'',
|
|
643
|
-
'第三步 按所选模板的档位形态写计划:',
|
|
644
|
-
'- lite: 恰 1 个任务,原文即任务描述,不拆步骤',
|
|
645
|
-
'- plan-final / step-review: 1..N 个任务',
|
|
646
|
-
'- multi-plan: K 个子计划(subplans, title+目标描述)表达子计划大纲,不直接列任务',
|
|
647
|
-
'',
|
|
648
|
-
'任务依赖声明(id/after,均省略则链式):',
|
|
649
|
-
'- 显式声明: {"id":"t1", ...};省略 id 时自动编号',
|
|
650
|
-
'- 依赖前置: after:["t1"] 或多值 after:["t1","t2"](汇合)',
|
|
651
|
-
'- 并行: 彼此独立的任务声明相同 after(如两个任务都 after:["t1"] 即并行)',
|
|
652
|
-
'- 无依赖根任务: after:[];省略 after = 依赖前一任务(保守链式,忘标不意外并行)',
|
|
653
|
-
'- after 只能引用前面已声明的任务 id,禁止前向引用',
|
|
654
|
-
'- 并行任务按模块/文件边界拆分,避免改同一文件相互冲突',
|
|
655
|
-
'- 并行任务 description 末尾声明主要涉及文件,如「实现 X(主要涉及 src/a.ts)」',
|
|
656
|
-
'',
|
|
657
|
-
'第四步 严格按 schema 返回提交计划。',
|
|
658
|
-
'',
|
|
659
|
-
'示例(step-review 形态,t2/t3 并行依赖 t1):',
|
|
660
|
-
'{"templateId":"step-review","complexity":"high","risk":"medium","scope":"medium","plan":"...","tasks":[{"id":"t1","description":"实现用户登录 API","acceptance":"接口返回预期结果","files":["src/a.ts"],"after":[]},{"id":"t2","description":"编写登录集成测试","after":["t1"]},{"id":"t3","description":"编写登录前端页面","after":["t1"]}]}',
|
|
661
|
-
'',
|
|
662
|
-
'示例(multi-plan 形态,子计划也可声明依赖,s2/s3 并行依赖 s1):',
|
|
663
|
-
'{"templateId":"multi-plan","complexity":"high","scope":"large","plan":"...","subplans":[{"id":"s1","title":"认证模块","description":"实现登录注册与令牌管理","after":[]},{"id":"s2","title":"权限模块","description":"实现角色与资源级权限校验","after":["s1"]},{"id":"s3","title":"审计模块","description":"实现操作审计日志","after":["s1"]}]}',
|
|
664
|
-
'',
|
|
665
|
-
'需求: ' + compress(REQ, REQUEST_CHARS),
|
|
666
|
-
CONTEXT_NOTES ? '【仓库上下文(主代理勘察所得)】\n' + CONTEXT_NOTES : '',
|
|
667
|
-
prefixHandoff(),
|
|
668
|
-
LOCKED ? '【模板锁定】用户已指定模板: ' + LOCKED + '。templateId 按该值填写, 并按该模板形态拆解。' : '',
|
|
669
|
-
decompRules(MAX_TASKS),
|
|
670
|
-
PLAN_FIELD_REQUIREMENT,
|
|
671
|
-
]
|
|
672
|
-
return parts.filter(function (x) { return x !== undefined && x !== null }).join('\n')
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// executor 任务指令(buildTaskInstruction + §4.7 并行提示 + v1 纪律行)
|
|
676
|
-
function buildExecutorPrompt(node, nudge) {
|
|
677
|
-
const parallelDescs = node.siblings || []
|
|
678
|
-
const isFailContext = node.enterReason === 'fail'
|
|
679
|
-
const forced = node.enterReason === 'fail' || node.enterReason === 'escalate'
|
|
680
|
-
const sections = [EXECUTOR_IDENTITY]
|
|
681
|
-
if (node.rejectPrefix) sections.push(node.rejectPrefix)
|
|
682
|
-
sections.push(handoff(node.description, {
|
|
683
|
-
parallel: parallelDescs,
|
|
684
|
-
next: nextPendingFrom(node.id),
|
|
685
|
-
failedModel: isFailContext ? node.failedModel : '',
|
|
686
|
-
forced: forced,
|
|
687
|
-
}))
|
|
688
|
-
const planText = planTextFor(node)
|
|
689
|
-
if (templateId !== 'lite' && planText.trim()) sections.push('[执行计划]\n' + compress(planText, PLAN_EXCERPT_CHARS))
|
|
690
|
-
if (CONTEXT_NOTES) sections.push('[仓库上下文(主代理勘察所得)]\n' + CONTEXT_NOTES)
|
|
691
|
-
const depLines = []
|
|
692
|
-
for (const d of node.deps) {
|
|
693
|
-
const dn = subjOf(d)
|
|
694
|
-
if (dn && dn.type !== 'review' && dn.status === 'done') depLines.push('- ' + dn.description + ': ' + compress(dn.output, KEY_OUTPUT_CHARS))
|
|
695
|
-
}
|
|
696
|
-
if (depLines.length) sections.push('[前序任务产出]\n' + depLines.join('\n'))
|
|
697
|
-
if (node.acceptance) sections.push('[验收判据]\n' + node.acceptance)
|
|
698
|
-
sections.push('[要求] ' + (TASK_TONE[templateId] || TASK_TONE_DEFAULT))
|
|
699
|
-
if (parallelDescs.length) sections.push(PARALLEL_NOTE)
|
|
700
|
-
sections.push(EXEC_REPORT_CONTRACT)
|
|
701
|
-
if (nudge) sections.push(nudge)
|
|
702
|
-
return sections.filter(Boolean).join('\n\n')
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// reviewer 指令(buildReviewInstruction 结构 + git 证据 + v1 范围核查)
|
|
706
|
-
function buildReviewPrompt(node) {
|
|
707
|
-
const isPlan = node.kind === 'plan'
|
|
708
|
-
const finalView = node.kind === 'final' || node.kind === 'cross'
|
|
709
|
-
const sections = [finalView ? REVIEWER_FINAL_IDENTITY : REVIEWER_IDENTITY]
|
|
710
|
-
sections.push(handoff(node.description, { next: nextPendingFrom(node.id) }))
|
|
711
|
-
sections.push(node.description)
|
|
712
|
-
if (isPlan) {
|
|
713
|
-
sections.push('[待审执行计划]\n实施计划全文:\n' + compress(PLAN_TEXT, PLAN_EXCERPT_CHARS))
|
|
714
|
-
sections.push('[git 证据]\n计划审批只需审阅计划文本本身, 不需要运行命令。')
|
|
715
|
-
sections.push(PLAN_REVIEW_CRITERIA)
|
|
716
|
-
} else if (node.kind === 'task') {
|
|
717
|
-
const s = subjOf(node.subject)
|
|
718
|
-
const sOut = s ? String(s.output || '') : ''
|
|
719
|
-
sections.push('[待审工作结果]\n' + (s ? s.description : node.description) + (sOut ? '\n' + compress(sOut, EXEC_SUMMARY_CHARS) : ''))
|
|
720
|
-
if (s && s.acceptance) sections.push('[验收判据]\n' + s.acceptance)
|
|
721
|
-
sections.push('[自报变更文件]\n' + ((s && s.changedFiles.length) ? s.changedFiles.join(', ') : '(未申报, 以 git diff 为准)'))
|
|
722
|
-
if (s && s.plannedFiles.length) sections.push('[规划期申报文件(基线)]\n' + s.plannedFiles.join(', '))
|
|
723
|
-
sections.push('[git 证据]\n' + GIT_EVIDENCE_RULE)
|
|
724
|
-
const exempt = unionChangedFiles().filter(function (f) { return !s || (s.changedFiles.indexOf(f) < 0 && s.plannedFiles.indexOf(f) < 0) })
|
|
725
|
-
sections.push('[非本任务范围的申报文件(豁免清单)]\n' + (exempt.length ? exempt.join(', ') : '(无)'))
|
|
726
|
-
sections.push(TASK_REVIEW_CRITERIA)
|
|
727
|
-
sections.push(SCOPE_RULE)
|
|
728
|
-
} else if (node.kind === 'subplan') {
|
|
729
|
-
const p = subjOf(node.subject)
|
|
730
|
-
const spTasks = p ? nodes.filter(function (n) { return !n.dead && n.type === 'task' && n.planSource === p.id }) : []
|
|
731
|
-
const detail = ['本子计划交付:']
|
|
732
|
-
if (p && String(p.output || '').trim()) detail.push('[细化方案]\n' + compress(p.output, PLAN_EXCERPT_CHARS))
|
|
733
|
-
const outs = spTasks.map(function (t) { return '- ' + t.description + ': ' + compress(t.output, KEY_OUTPUT_CHARS) })
|
|
734
|
-
if (outs.length) detail.push('[任务产出]\n' + outs.join('\n'))
|
|
735
|
-
sections.push('[待审工作结果]\n' + detail.join('\n'))
|
|
736
|
-
const files = []
|
|
737
|
-
for (const t of spTasks) for (const f of t.changedFiles) if (files.indexOf(f) < 0) files.push(f)
|
|
738
|
-
sections.push('[自报变更文件]\n' + (files.length ? files.join(', ') : '(未申报, 以 git diff 为准)'))
|
|
739
|
-
sections.push('[git 证据]\n' + GIT_EVIDENCE_RULE)
|
|
740
|
-
sections.push(SUBPLAN_REVIEW_CRITERIA)
|
|
741
|
-
} else {
|
|
742
|
-
sections.push('[待审工作结果]\n整个需求的整体交付(全部已完成任务, 明细见交接摘要[已完成子任务])')
|
|
743
|
-
sections.push('[自报变更文件]\n' + (unionChangedFiles().join(', ') || '(无)'))
|
|
744
|
-
sections.push('[git 证据]\n' + GIT_EVIDENCE_RULE)
|
|
745
|
-
sections.push(finalView ? OVERALL_REVIEW_CRITERIA : TASK_REVIEW_CRITERIA)
|
|
746
|
-
if (finalView) sections.push(OVERALL_SCOPE_RULE)
|
|
747
|
-
}
|
|
748
|
-
if (node.fixNote) sections.push('【上一轮驳回意见(检查是否已解决)】\n' + node.fixNote + '\n本轮只判定驳回点是否解决与是否引入新问题, 不扩大审查范围。')
|
|
749
|
-
sections.push(REVIEW_REPORT_CONTRACT)
|
|
750
|
-
return sections.filter(Boolean).join('\n\n')
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// ── 阶段 1: 分诊与规划 ──────────────────────────────────────────────────────
|
|
754
|
-
// prefix 种子在规划前解析, 供 plannerPrompt 注入"已完成工作"清单(续跑不重做)
|
|
755
|
-
PREFIX_ITEMS = validPrefix(A.prefix)
|
|
756
|
-
const seedDescSet = {}
|
|
757
|
-
PREFIX_ITEMS.forEach(function (p) { seedDescSet[p.description] = true })
|
|
758
|
-
|
|
759
|
-
phase('分诊与规划')
|
|
760
|
-
log('planner 正在分诊与拆解')
|
|
761
|
-
const triageCursor = { i: 0 }
|
|
762
|
-
const triageCandidates = slotOpts('planner-triage')
|
|
763
|
-
for (let attempt = 0; attempt < TRIAGE_ATTEMPTS && !triage; attempt++) {
|
|
764
|
-
triage = await callAgent(triageCursor, triageCandidates, { prompt: plannerPrompt(), label: 'planner:分诊', schema: PLAN_SCHEMA })
|
|
765
|
-
if (!triage) log('planner 第 ' + (attempt + 1) + ' 次调用失败' + (attempt === 0 ? ', 重试' : ''))
|
|
766
|
-
}
|
|
767
|
-
if (!triage) {
|
|
768
|
-
log('planner 不可用, 无信号走 defaultTemplate 兜底: ' + DEFAULT_TEMPLATE)
|
|
769
|
-
triage = { complexity: '', risk: '', scope: '', reasoning: 'planner 不可用, 规则兜底', plan: '', tasks: [{ id: 't1', description: REQ }], subplans: [] }
|
|
770
|
-
}
|
|
771
|
-
PLAN_TEXT = String(triage.plan || '')
|
|
772
|
-
|
|
773
|
-
// 模板四级兜底链: 锁定 → planner 声明(合法才采纳) → 信号矩阵 → defaultTemplate
|
|
774
|
-
const declaredTemplate = triage && TEMPLATES.indexOf(triage.templateId) >= 0 ? triage.templateId : ''
|
|
775
|
-
const hasSignals = !!(triage.complexity || triage.risk || triage.scope)
|
|
776
|
-
templateId = LOCKED || declaredTemplate || (hasSignals ? chooseTemplate(triage) : DEFAULT_TEMPLATE)
|
|
777
|
-
// 模板来源: 供汇报注明(planner 全挂无信号同样落 defaultTemplate 链)
|
|
778
|
-
templateSource = LOCKED ? 'locked' : declaredTemplate ? 'declared' : hasSignals ? 'matrix' : 'default-fallback'
|
|
779
|
-
|
|
780
|
-
// 与种子同描述的原始任务先剔除(续跑不重做的机器兜底, normalize 清理悬空引用)
|
|
781
|
-
let rawTasks = Array.isArray(triage.tasks) ? triage.tasks.slice() : []
|
|
782
|
-
if (PREFIX_ITEMS.length) {
|
|
783
|
-
const before = rawTasks.length
|
|
784
|
-
rawTasks = rawTasks.filter(function (t) { return !(t && typeof t.description === 'string' && seedDescSet[t.description.trim()]) })
|
|
785
|
-
if (rawTasks.length < before) log('续跑去重: 剔除与已完成工作重复的任务 ' + (before - rawTasks.length) + ' 个')
|
|
786
|
-
}
|
|
787
|
-
// 兜底/声明选中 lite 但拆了多任务(信号与拆解自相矛盾, lite 蓝图无法承载) → 升 plan-final
|
|
788
|
-
if (!LOCKED && templateId === 'lite' && rawTasks.length > 1) {
|
|
789
|
-
templateId = 'plan-final'
|
|
790
|
-
log('lite 与多任务拆解自相矛盾, 升级为 plan-final')
|
|
791
|
-
}
|
|
792
|
-
// multi-plan 实例化前置检查: 缺大纲按剩余任务降档
|
|
793
|
-
if (templateId === 'multi-plan') {
|
|
794
|
-
OUTLINE = (Array.isArray(triage.subplans) ? triage.subplans : [])
|
|
795
|
-
.filter(function (s) { return s && typeof s.title === 'string' && typeof s.description === 'string' })
|
|
796
|
-
const rawTaskCount = rawTasks.length
|
|
797
|
-
if (!OUTLINE.length && rawTaskCount) { log('multi-plan 缺子计划大纲, 降级为 step-review'); templateId = 'step-review' }
|
|
798
|
-
else if (!OUTLINE.length) { log('multi-plan 缺子计划大纲且无任务, 降级为 lite'); templateId = 'lite' }
|
|
799
|
-
else if (OUTLINE.length > MAX_TASKS) { log('子计划大纲数超预算 ' + MAX_TASKS + ', 截断保留前 ' + MAX_TASKS + ' 个'); OUTLINE.length = MAX_TASKS }
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// prefix 种子: 仅 task 级模板生效, multi-plan 忽略并记日志
|
|
803
|
-
if (PREFIX_ITEMS.length && templateId === 'multi-plan') {
|
|
804
|
-
PREFIX_IDS = []
|
|
805
|
-
log('multi-plan 忽略 prefix 断点续跑种子')
|
|
806
|
-
} else {
|
|
807
|
-
PREFIX_IDS = PREFIX_ITEMS.map(function (p) { return PREFIX_ID_MARK + p.id })
|
|
808
|
-
}
|
|
809
|
-
const seedSeen = {}
|
|
810
|
-
PREFIX_IDS.forEach(function (id) { seedSeen[id] = true })
|
|
811
|
-
// 续跑标注: 兜底单任务共用, 提示执行器只补剩余部分
|
|
812
|
-
const REMAIN_NOTE = PREFIX_IDS.length ? '(续跑: 已完成部分见交接摘要[已完成子任务], 只需完成剩余部分)' : ''
|
|
813
|
-
|
|
814
|
-
let tasks = normalizeTasks(rawTasks, 't-', seedSeen, Math.max(remainingTaskBudget(), 1))
|
|
815
|
-
PREFIX_ITEMS.forEach(function (p, i) {
|
|
816
|
-
taskNode(PREFIX_IDS[i], p.description, [], 'PLAN', { seed: true, status: 'done', output: p.output, changedFiles: p.changedFiles })
|
|
817
|
-
})
|
|
818
|
-
|
|
819
|
-
// ── 阶段 2: 按模板蓝图实例化节点树 ──────────────────────────────────────────
|
|
820
|
-
// step-review 任务段: 计划审先行, 根任务挂 pr(approve), 其余挂前序任务审
|
|
821
|
-
function buildStepReviewTasks(list) {
|
|
822
|
-
list.forEach(function (t) {
|
|
823
|
-
const deps = t.after.length ? t.after.map(function (x) { return 'r-' + x }) : [PLAN_REVIEW_NODE]
|
|
824
|
-
taskNode(t.id, t.description, deps, 'PLAN', { acceptance: t.acceptance, plannedFiles: t.files })
|
|
825
|
-
reviewNode('r-' + t.id, '审批: ' + t.description.slice(0, 60), [t.id], t.id, 'reviewer-task', 'task')
|
|
826
|
-
})
|
|
827
|
-
}
|
|
828
|
-
// plan-final 任务段: 计划审先行, 任务对任务依赖, 末尾终审
|
|
829
|
-
function buildPlanFinalTasks(list) {
|
|
830
|
-
list.forEach(function (t) {
|
|
831
|
-
taskNode(t.id, t.description, [PLAN_REVIEW_NODE].concat(t.after), 'PLAN', { acceptance: t.acceptance, plannedFiles: t.files })
|
|
832
|
-
})
|
|
833
|
-
reviewNode('fr', '终审: 整个需求交付质量', list.map(function (t) { return t.id }).concat(PREFIX_IDS), SUBJECT_OVERALL, 'reviewer-final', 'final')
|
|
834
|
-
}
|
|
835
|
-
// multi-plan 大纲单元: pr → 子计划单元(p → r-* → sr) → 交叉终审串行链
|
|
836
|
-
function buildOutlineUnits(rootId) {
|
|
837
|
-
const outlineIds = OUTLINE.map(function (s, i) { return (typeof s.id === 'string' && s.id.trim()) ? s.id.trim() : 'p' + (i + 1) })
|
|
838
|
-
OUTLINE.forEach(function (s, i) {
|
|
839
|
-
const pid = 'p' + (i + 1)
|
|
840
|
-
const afterPids = Array.isArray(s.after)
|
|
841
|
-
? s.after.map(function (x) { const j = outlineIds.indexOf(x); return j >= 0 ? 'p' + (j + 1) : null }).filter(Boolean)
|
|
842
|
-
: (i > 0 ? ['p' + i] : [])
|
|
843
|
-
const p = planNode(pid, '子计划[' + s.title + ']: ' + s.description, [rootId].concat(afterPids), i)
|
|
844
|
-
p.outlineDeps = afterPids.map(function (x) { return 'sr' + x.slice(1) })
|
|
845
|
-
reviewNode('sr' + (i + 1), '子计划审批[' + s.title + ']', [pid], pid, 'reviewer-subplan', 'subplan')
|
|
846
|
-
})
|
|
847
|
-
const srIds = OUTLINE.map(function (s, i) { return 'sr' + (i + 1) })
|
|
848
|
-
reviewNode('xr1', '交叉终审(正确性)', srIds, SUBJECT_OVERALL, 'reviewer-cross', 'cross')
|
|
849
|
-
reviewNode('xr2', '交叉终审(边界与安全)', ['xr1'], SUBJECT_OVERALL, 'reviewer-cross', 'cross')
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (templateId === 'lite') {
|
|
853
|
-
// lite 恒单任务(原版拆解约束), 任务描述即需求原文
|
|
854
|
-
const liteTasks = [{ id: 't1', description: REQ + REMAIN_NOTE, after: [] }]
|
|
855
|
-
liteTasks.forEach(function (t) { taskNode(t.id, t.description, t.after, 'PLAN') })
|
|
856
|
-
reviewNode('fr', '终审: 整个需求交付质量', ['t1'].concat(PREFIX_IDS), 't1', 'reviewer-final', 'final')
|
|
857
|
-
}
|
|
858
|
-
if (templateId === 'step-review') {
|
|
859
|
-
if (!tasks.length) tasks = [{ id: 't1', description: REQ + REMAIN_NOTE, after: [], acceptance: '', files: [] }]
|
|
860
|
-
reviewNode(PLAN_REVIEW_NODE, '计划审批: 实施计划可执行性/粒度/依赖/文件互斥', [], SUBJECT_PLAN, 'reviewer-plan', 'plan')
|
|
861
|
-
buildStepReviewTasks(tasks)
|
|
862
|
-
}
|
|
863
|
-
if (templateId === 'plan-final') {
|
|
864
|
-
if (!tasks.length) tasks = [{ id: 't1', description: REQ + REMAIN_NOTE, after: [], acceptance: '', files: [] }]
|
|
865
|
-
reviewNode(PLAN_REVIEW_NODE, '计划审批: 实施计划可执行性/粒度/依赖/文件互斥', [], SUBJECT_PLAN, 'reviewer-plan', 'plan')
|
|
866
|
-
buildPlanFinalTasks(tasks)
|
|
867
|
-
}
|
|
868
|
-
if (templateId === 'multi-plan') {
|
|
869
|
-
reviewNode(PLAN_REVIEW_NODE, '大纲审批: 子计划划分/依赖/粒度与文件边界', [], SUBJECT_PLAN, 'reviewer-plan', 'plan')
|
|
870
|
-
buildOutlineUnits(PLAN_REVIEW_NODE)
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// ── 节点执行器 ──────────────────────────────────────────────────────────────
|
|
874
|
-
async function execTask(node) {
|
|
875
|
-
const slot = TASK_SLOT_BY_REASON[node.enterReason] || 'executor-task'
|
|
876
|
-
const candidates = slotOpts(slot)
|
|
877
|
-
let r = await callAgent(node.cursor, candidates, { prompt: buildExecutorPrompt(node, ''), label: 'executor:' + node.id, schema: EXEC_SCHEMA })
|
|
878
|
-
// completed 但 summary 空白 → 按 reportNudgeLimit 预算带补救指引重问
|
|
879
|
-
while (r && r.status === 'completed' && !String(r.summary || '').trim() && node.nudgeCount < BUDGET.reportNudgeLimit) {
|
|
880
|
-
node.nudgeCount++
|
|
881
|
-
log('任务 ' + node.id + ' 报告摘要空白, 补救追问(' + node.nudgeCount + '/' + BUDGET.reportNudgeLimit + ')')
|
|
882
|
-
r = await callAgent(node.cursor, candidates, { prompt: buildExecutorPrompt(node, REPORT_NUDGE_NOTE), label: 'executor:' + node.id + ':报告追问', schema: EXEC_SCHEMA })
|
|
883
|
-
}
|
|
884
|
-
if (!r) {
|
|
885
|
-
node.failedModel = failedModelOf(node.cursor)
|
|
886
|
-
node.failCount++
|
|
887
|
-
node.output = 'executor 子代理调用失败'
|
|
888
|
-
return
|
|
889
|
-
}
|
|
890
|
-
node.changedFiles = Array.isArray(r.changedFiles) ? r.changedFiles.filter(function (x) { return typeof x === 'string' }) : []
|
|
891
|
-
node.output = String(r.summary || '')
|
|
892
|
-
if (r.status !== 'completed') {
|
|
893
|
-
node.failedModel = failedModelOf(node.cursor)
|
|
894
|
-
node.failCount++
|
|
895
|
-
node.output = '任务自报失败: ' + node.output
|
|
896
|
-
return
|
|
897
|
-
}
|
|
898
|
-
if (!node.output.trim()) {
|
|
899
|
-
node.failedModel = failedModelOf(node.cursor)
|
|
900
|
-
node.failCount++
|
|
901
|
-
node.output = '报告摘要空白(补救追问预算耗尽)'
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
async function runTaskWithRetry(node) {
|
|
906
|
-
try {
|
|
907
|
-
let guard = 0
|
|
908
|
-
while (guard++ < FAIL_RETRY_FUSE) {
|
|
909
|
-
node.status = 'active'
|
|
910
|
-
const before = node.failCount
|
|
911
|
-
await execTask(node)
|
|
912
|
-
if (node.failCount === before) {
|
|
913
|
-
node.status = 'done'
|
|
914
|
-
node.reviewNote = ''
|
|
915
|
-
node.rejectPrefix = ''
|
|
916
|
-
return
|
|
917
|
-
}
|
|
918
|
-
// fail/reject 合并记账达阈值即升级
|
|
919
|
-
if (node.failCount + node.rejectCount >= BUDGET.reviewRejectBeforeEscalate) {
|
|
920
|
-
node.status = 'failed'
|
|
921
|
-
await escalateTask(node, [String(node.output || '任务失败')])
|
|
922
|
-
return
|
|
923
|
-
}
|
|
924
|
-
node.enterReason = 'fail'
|
|
925
|
-
log('任务 ' + node.id + ' 失败, 换模型原地重试(合并账 ' + (node.failCount + node.rejectCount) + '/' + BUDGET.reviewRejectBeforeEscalate + ')')
|
|
926
|
-
}
|
|
927
|
-
node.status = 'failed'
|
|
928
|
-
await escalateTask(node, [String(node.output || '任务反复失败')])
|
|
929
|
-
} catch (e) {
|
|
930
|
-
node.status = 'failed'
|
|
931
|
-
node.output = 'executor 异常: ' + (e && e.message ? e.message : String(e))
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// 审批证据判定: evidence 必须是非空白字符串
|
|
936
|
-
function hasEvidence(r) {
|
|
937
|
-
return r && typeof r.evidence === 'string' && !!r.evidence.trim()
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// ── 审批执行与拒绝路由(契约 B7/B8) ──────────────────────────────────────────
|
|
941
|
-
async function runReview(node) {
|
|
942
|
-
node.status = 'active'
|
|
943
|
-
const prompt = buildReviewPrompt(node)
|
|
944
|
-
const candidates = slotOpts(node.slot)
|
|
945
|
-
let r = await callAgent(node.cursor, candidates, { prompt: prompt, label: 'reviewer:' + node.id, schema: REVIEW_SCHEMA })
|
|
946
|
-
if (!r) {
|
|
947
|
-
log('reviewer ' + node.id + ' 调用失败, 重试一次')
|
|
948
|
-
r = await callAgent(node.cursor, candidates, { prompt: prompt + REVIEW_RESEND_NOTE, label: 'reviewer:' + node.id + ':重试', schema: REVIEW_SCHEMA })
|
|
949
|
-
}
|
|
950
|
-
if (!r) {
|
|
951
|
-
// fail-closed: 审批者不可用折算拒绝, 走既有驳回路由; plan/subplan 型走重规划路由, 不用"原样重交"口径
|
|
952
|
-
node.reviewerFault = true
|
|
953
|
-
const planTypeFail = node.kind === 'plan' || node.kind === 'subplan'
|
|
954
|
-
const unavailable = planTypeFail ? REVIEWER_UNAVAILABLE_PLAN_REASON : REVIEWER_UNAVAILABLE_REASON
|
|
955
|
-
log('reviewer 不可用, ' + node.id + ' 视为拒绝')
|
|
956
|
-
r = { verdict: 'REJECTED', reasons: [unavailable], summary: unavailable, evidence: '', reviewerFault: true }
|
|
957
|
-
}
|
|
958
|
-
// 证据契约: APPROVED 空证据按 emptyOutputRetryLimit 预算重问, 耗尽折算拒绝
|
|
959
|
-
let asks = 0
|
|
960
|
-
while (r.verdict === 'APPROVED' && !hasEvidence(r) && asks < BUDGET.emptyOutputRetryLimit) {
|
|
961
|
-
asks++
|
|
962
|
-
log(node.id + ' 通过缺验证证据, 重问(' + asks + '/' + BUDGET.emptyOutputRetryLimit + ')')
|
|
963
|
-
const reask = await callAgent(node.cursor, candidates, { prompt: prompt + EVIDENCE_REASK_NOTE, label: 'reviewer:' + node.id + ':证据重问', schema: REVIEW_SCHEMA })
|
|
964
|
-
if (!reask) break
|
|
965
|
-
r = reask
|
|
966
|
-
}
|
|
967
|
-
if (r.verdict === 'APPROVED' && !hasEvidence(r)) {
|
|
968
|
-
log(node.id + ' 证据预算耗尽, 视为拒绝')
|
|
969
|
-
node.reviewerFault = true
|
|
970
|
-
r = { verdict: 'REJECTED', reasons: [EVIDENCE_REQUIRED_REASON], summary: EVIDENCE_REQUIRED_REASON, evidence: '', reviewerFault: true }
|
|
971
|
-
}
|
|
972
|
-
node.output = (r.verdict === 'APPROVED' ? 'APPROVED: ' : 'REJECTED: ') + String(r.summary || '')
|
|
973
|
-
node.reviewEvidence = String(r.evidence || '')
|
|
974
|
-
node.verdict = r.verdict
|
|
975
|
-
if (r.verdict === 'APPROVED') {
|
|
976
|
-
node.status = 'done'
|
|
977
|
-
const holder = reviewCountHolder(node)
|
|
978
|
-
// approve 只清 rejectCount; 合并账中的 failCount 作为失败历史保留参与后续记账(有意设计, 防误判为遗漏)
|
|
979
|
-
if (holder) holder.rejectCount = 0
|
|
980
|
-
// pr 通过只放行计划, 不清零升级账; 交付类通过清零
|
|
981
|
-
if (node.kind !== 'plan') {
|
|
982
|
-
escalations = 0
|
|
983
|
-
log(node.id + ' 交付审批通过, 升级账清零')
|
|
984
|
-
}
|
|
985
|
-
return
|
|
986
|
-
}
|
|
987
|
-
await routeRejection(node, r)
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
async function routeRejection(node, r) {
|
|
991
|
-
node.status = 'pending'
|
|
992
|
-
const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons.map(String) : [String(r.summary || '')]
|
|
993
|
-
const holder = reviewCountHolder(node)
|
|
994
|
-
if (holder) holder.rejectCount++
|
|
995
|
-
const threshold = planTypeReview(node) ? BUDGET.planRejectBeforeBlocked : BUDGET.reviewRejectBeforeEscalate
|
|
996
|
-
// task 级合并记账: 失败与被拒共占同一阈值账; plan 型/整体交付仅拒绝计数
|
|
997
|
-
const count = holder ? (holder.type === 'task' ? holder.failCount + holder.rejectCount : holder.rejectCount) : 1
|
|
998
|
-
log(node.id + ' 驳回(' + count + '/' + threshold + '): ' + compress(reasons.join('; '), 100))
|
|
999
|
-
// 达阈值: 升级重规划(计升级账)
|
|
1000
|
-
if (count >= threshold) {
|
|
1001
|
-
if (node.kind === 'plan') return replanPlan(node, reasons, true)
|
|
1002
|
-
if (node.kind === 'subplan') return escalateSubplan(holder, reasons)
|
|
1003
|
-
if (holder && holder.type === 'task') return escalateTask(holder, reasons)
|
|
1004
|
-
return rework([node], reasons)
|
|
1005
|
-
}
|
|
1006
|
-
// 未达阈值: 原位重做路由
|
|
1007
|
-
if (node.kind === 'plan') return replanPlan(node, reasons, false)
|
|
1008
|
-
if (node.kind === 'subplan') {
|
|
1009
|
-
// subject 已被尾段替换置 dead 时 holder 为 null: 走 rework 兜底, 不静默吞掉拒绝
|
|
1010
|
-
if (!holder) return rework([node], reasons)
|
|
1011
|
-
node.fixNote = reasons.join('; ')
|
|
1012
|
-
return runPlanNode(holder, reasons)
|
|
1013
|
-
}
|
|
1014
|
-
const prefix = rejectionPrefix(reasons, !!r.reviewerFault)
|
|
1015
|
-
if (holder && holder.type === 'task') {
|
|
1016
|
-
holder.status = 'pending'
|
|
1017
|
-
holder.enterReason = 'reject'
|
|
1018
|
-
holder.rejectPrefix = prefix
|
|
1019
|
-
log('任务 ' + holder.id + ' 被拒, 带驳回意见重做')
|
|
1020
|
-
return
|
|
1021
|
-
}
|
|
1022
|
-
const last = lastDoneTask()
|
|
1023
|
-
if (!last) return rework([node], reasons)
|
|
1024
|
-
last.status = 'pending'
|
|
1025
|
-
last.enterReason = 'reject'
|
|
1026
|
-
last.rejectPrefix = prefix
|
|
1027
|
-
node.deps = [last.id]
|
|
1028
|
-
node.fixNote = reasons.join('; ')
|
|
1029
|
-
log('交付被拒, 最后完成任务 ' + last.id + ' 带驳回意见重做, ' + node.id + ' 重挂其后')
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// ── 子计划细化(planner-subplan): 首次细化与被拒重建共用 ─────────────────────
|
|
1033
|
-
function killSubplanSection(pId) {
|
|
1034
|
-
const killed = {}
|
|
1035
|
-
for (const n of nodes) {
|
|
1036
|
-
if (!n.dead && n.type === 'task' && n.planSource === pId) { n.dead = true; killed[n.id] = true }
|
|
1037
|
-
}
|
|
1038
|
-
for (const n of nodes) {
|
|
1039
|
-
if (!n.dead && n.type === 'review' && n.kind === 'task' && killed[n.subject]) n.dead = true
|
|
1040
|
-
}
|
|
1041
|
-
return Object.keys(killed)
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
async function runPlanNode(node, rejectReasons) {
|
|
1045
|
-
node.status = 'active'
|
|
1046
|
-
const sp = OUTLINE[node.outlineIndex] || { title: node.description, description: node.description }
|
|
1047
|
-
const remaining = remainingTaskBudget()
|
|
1048
|
-
const cap = remaining > 0 ? remaining : 1
|
|
1049
|
-
if (remaining < 1) log('全局任务预算已满, 子计划[' + sp.title + ']仍保底 1 个任务')
|
|
1050
|
-
const parts = [
|
|
1051
|
-
PLANNER_IDENTITY,
|
|
1052
|
-
'',
|
|
1053
|
-
rejectReasons ? '本子计划交付未通过审批,需要带驳回意见重新细化并重建任务段。' : ('请完成规划: ' + node.description),
|
|
1054
|
-
'',
|
|
1055
|
-
'[原始需求] ' + compress(REQ, REQUEST_CHARS),
|
|
1056
|
-
'',
|
|
1057
|
-
'[子计划] ' + sp.title + ' — ' + sp.description,
|
|
1058
|
-
'',
|
|
1059
|
-
'[已完成工作]',
|
|
1060
|
-
doneSummaryText() || NONE,
|
|
1061
|
-
]
|
|
1062
|
-
if (rejectReasons) {
|
|
1063
|
-
parts.push('', '[未通过原因]', rejectReasons.filter(Boolean).join('; ') || '未提供', '', '[待重规划范围]', '本子计划的执行方案与任务段(重建后重审)。')
|
|
1064
|
-
}
|
|
1065
|
-
parts.push('', decompRules(cap), '本子计划任务数不超过 ' + cap + '。', PLAN_FIELD_REQUIREMENT, '严格按 schema 返回: plan=执行方案; tasks=任务段(每任务给 acceptance 验收判据与 files 预期触达文件)。')
|
|
1066
|
-
const r = await callAgent(node.cursor, slotOpts('planner-subplan'), { prompt: parts.join('\n'), label: 'planner:' + node.id, schema: SUBPLAN_GEN_SCHEMA })
|
|
1067
|
-
let gen = r
|
|
1068
|
-
if (!gen || !Array.isArray(gen.tasks) || !gen.tasks.length) {
|
|
1069
|
-
log('子计划生成失败, 降级为单任务: ' + sp.title)
|
|
1070
|
-
gen = { plan: sp.description, tasks: [{ id: 'st1', description: sp.title + ': ' + sp.description }] }
|
|
1071
|
-
}
|
|
1072
|
-
if (rejectReasons) {
|
|
1073
|
-
const removed = killSubplanSection(node.id)
|
|
1074
|
-
log('子计划[' + sp.title + ']任务段重建: 移除 [' + removed.join(', ') + ']')
|
|
1075
|
-
}
|
|
1076
|
-
node.output = String(gen.plan || sp.description)
|
|
1077
|
-
node.status = 'done'
|
|
1078
|
-
const st = normalizeTasks(gen.tasks, node.id + '-', liveIds(), cap)
|
|
1079
|
-
st.forEach(function (t) {
|
|
1080
|
-
const deps = [node.id].concat(node.outlineDeps).concat(t.after.map(function (x) { return 'r-' + x }))
|
|
1081
|
-
taskNode(t.id, t.description, deps, node.id, { acceptance: t.acceptance, plannedFiles: t.files })
|
|
1082
|
-
reviewNode('r-' + t.id, '审批[' + sp.title + ']: ' + t.description.slice(0, 50), [t.id], t.id, 'reviewer-task', 'task')
|
|
1083
|
-
})
|
|
1084
|
-
const sr = subjOf('sr' + (node.outlineIndex + 1))
|
|
1085
|
-
if (sr && sr.kind === 'subplan') sr.deps = st.map(function (t) { return 'r-' + t.id })
|
|
1086
|
-
log('子计划[' + sp.title + '] 产出任务: ' + st.map(function (t) { return t.id }).join(', '))
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// ── 计划审批驳回重规划(planner-command 未达阈值 / planner-escalate 达阈值) ──
|
|
1090
|
-
function rebuildBelowPlanReview(prNode) {
|
|
1091
|
-
const bad = reachableFrom(prNode.id)
|
|
1092
|
-
const removed = []
|
|
1093
|
-
for (const n of nodes) {
|
|
1094
|
-
if (n.id !== prNode.id && bad[n.id] && n.status !== 'done') { n.dead = true; removed.push(n.id) }
|
|
1095
|
-
}
|
|
1096
|
-
return removed
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
async function replanPlan(node, reasons, atThreshold) {
|
|
1100
|
-
if (escalating) {
|
|
1101
|
-
node.status = 'pending'
|
|
1102
|
-
node.fixNote = '(重规划在途, 待重审) ' + reasons.join('; ')
|
|
1103
|
-
return
|
|
1104
|
-
}
|
|
1105
|
-
if (atThreshold && escalations >= ESCALATION_LIMIT) {
|
|
1106
|
-
blocked = { nodeId: node.id, reason: '计划审批被拒且升级重规划次数已达上限', detail: reasons.join('; ') }
|
|
1107
|
-
return
|
|
1108
|
-
}
|
|
1109
|
-
escalating = true
|
|
1110
|
-
try {
|
|
1111
|
-
if (atThreshold) {
|
|
1112
|
-
escalations++
|
|
1113
|
-
lifetimeEscalations++
|
|
1114
|
-
phase('升级重规划')
|
|
1115
|
-
log('计划审批连续被拒, 第 ' + escalations + ' 次升级重规划(计划与任务段重建)')
|
|
1116
|
-
} else {
|
|
1117
|
-
log('计划审批被拒未达阈值, 由 planner-command 重规划, 不计升级账')
|
|
1118
|
-
}
|
|
1119
|
-
replanSeq++
|
|
1120
|
-
const isMulti = templateId === 'multi-plan'
|
|
1121
|
-
const prompt = [
|
|
1122
|
-
atThreshold ? REPLAN_HEAD : '实施计划未通过计划审批,需要吸收驳回意见重新规划。',
|
|
1123
|
-
'',
|
|
1124
|
-
'[原始需求] ' + compress(REQ, REQUEST_CHARS),
|
|
1125
|
-
'',
|
|
1126
|
-
'[已完成工作]',
|
|
1127
|
-
doneSummaryText() || NONE,
|
|
1128
|
-
'',
|
|
1129
|
-
'[未通过原因]',
|
|
1130
|
-
reasons.filter(Boolean).join('; ') || '未提供',
|
|
1131
|
-
'',
|
|
1132
|
-
'[待重规划范围]',
|
|
1133
|
-
isMulti ? '子计划大纲与各子计划任务段(全部重建)。' : '实施计划全文与任务段(重建)。',
|
|
1134
|
-
'',
|
|
1135
|
-
prefixHandoff(),
|
|
1136
|
-
isMulti
|
|
1137
|
-
? '子计划大纲规则: subplans 每项含 id/title/description/after; after 引用其他子计划 id 表达依赖, 省略 after = 链式接续; 彼此独立的子计划声明相同 after 并行; 大纲表达子计划划分, 不直接列任务。'
|
|
1138
|
-
: decompRules(Math.max(remainingTaskBudget(), 1)),
|
|
1139
|
-
PLAN_FIELD_REQUIREMENT,
|
|
1140
|
-
'请重新产出' + (isMulti ? '实施计划与子计划大纲' : '实施计划与任务段') + ', 按 schema 返回。可重排任务依赖: after 列出前置 id, 彼此独立的任务声明相同 after 并行执行, 省略 after = 依赖前一任务。',
|
|
1141
|
-
].join('\n')
|
|
1142
|
-
const r = await callReplanAgent(node.cursor, slotOpts(atThreshold ? 'planner-escalate' : 'planner-command'), { prompt: prompt, label: 'planner:计划重规划#' + replanSeq, schema: PLAN_REPLAN_SCHEMA })
|
|
1143
|
-
const cap = Math.max(remainingTaskBudget(), 1)
|
|
1144
|
-
const planText = r ? String(r.plan || '') : ''
|
|
1145
|
-
if (isMulti) {
|
|
1146
|
-
const ol = r && Array.isArray(r.subplans)
|
|
1147
|
-
? r.subplans.filter(function (s) { return s && typeof s.title === 'string' && typeof s.description === 'string' })
|
|
1148
|
-
: []
|
|
1149
|
-
const hasTasks = r && Array.isArray(r.tasks) && r.tasks.length
|
|
1150
|
-
if (!planText.trim() || (!ol.length && !hasTasks)) {
|
|
1151
|
-
blocked = { nodeId: node.id, reason: '计划重规划无产出', detail: reasons.join('; ') }
|
|
1152
|
-
return
|
|
1153
|
-
}
|
|
1154
|
-
PLAN_TEXT = planText
|
|
1155
|
-
const removed = rebuildBelowPlanReview(node)
|
|
1156
|
-
if (ol.length) {
|
|
1157
|
-
OUTLINE = ol.slice(0, cap)
|
|
1158
|
-
if (ol.length > cap) log('子计划大纲数超预算 ' + cap + ', 截断')
|
|
1159
|
-
buildOutlineUnits(PLAN_REVIEW_NODE)
|
|
1160
|
-
} else {
|
|
1161
|
-
templateId = 'step-review'
|
|
1162
|
-
log('重规划未产出子计划大纲, 降级 step-review 重建任务段')
|
|
1163
|
-
buildStepReviewTasks(normalizeTasks(r.tasks, 'u' + replanSeq + '-', liveIds(), cap))
|
|
1164
|
-
}
|
|
1165
|
-
log('计划重规划#' + replanSeq + ': 移除 [' + removed.join(', ') + '], 大纲/任务段已重建, 待计划复审')
|
|
1166
|
-
} else {
|
|
1167
|
-
const nt = (r && Array.isArray(r.tasks)) ? normalizeTasks(r.tasks, 'u' + replanSeq + '-', liveIds(), cap) : []
|
|
1168
|
-
if (!planText.trim() || !nt.length) {
|
|
1169
|
-
blocked = { nodeId: node.id, reason: '计划重规划无产出', detail: reasons.join('; ') }
|
|
1170
|
-
return
|
|
1171
|
-
}
|
|
1172
|
-
PLAN_TEXT = planText
|
|
1173
|
-
const removed = rebuildBelowPlanReview(node)
|
|
1174
|
-
if (templateId === 'plan-final') buildPlanFinalTasks(nt)
|
|
1175
|
-
else buildStepReviewTasks(nt)
|
|
1176
|
-
log('计划重规划#' + replanSeq + ': 移除 [' + removed.join(', ') + '], 重建 [' + nt.map(function (t) { return t.id }).join(', ') + '], 待计划复审')
|
|
1177
|
-
}
|
|
1178
|
-
node.fixNote = reasons.join('; ')
|
|
1179
|
-
node.status = 'pending'
|
|
1180
|
-
} finally {
|
|
1181
|
-
escalating = false
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// ── 升级重规划: 尾段替换 / 子计划重建 / 交付返工(达阈值, 计升级账) ──────────
|
|
1186
|
-
function tailScopeText(fromId) {
|
|
1187
|
-
const bad = reachableFrom(fromId)
|
|
1188
|
-
const lines = []
|
|
1189
|
-
for (const n of nodes) {
|
|
1190
|
-
if (!bad[n.id] || n.status === 'done' || n.dead) continue
|
|
1191
|
-
lines.push('- [' + n.id + '] ' + n.description)
|
|
1192
|
-
}
|
|
1193
|
-
return lines.length ? lines.join('\n') : '(尾段重建)'
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
async function escalateTask(node, reasons) {
|
|
1197
|
-
if (escalating) {
|
|
1198
|
-
node.status = 'pending'
|
|
1199
|
-
// 覆盖式标记: 并发升级在途可能多次命中, 拼接会让备注无界增长
|
|
1200
|
-
node.reviewNote = '(并发失败, 待重规划后重试)'
|
|
1201
|
-
return
|
|
1202
|
-
}
|
|
1203
|
-
escalating = true
|
|
1204
|
-
try {
|
|
1205
|
-
if (escalations >= ESCALATION_LIMIT) {
|
|
1206
|
-
blocked = { nodeId: node.id, reason: '连续失败/被拒且升级重规划次数已达上限', detail: reasons.join('; ') }
|
|
1207
|
-
return
|
|
1208
|
-
}
|
|
1209
|
-
escalations++
|
|
1210
|
-
lifetimeEscalations++
|
|
1211
|
-
phase('升级重规划')
|
|
1212
|
-
log('任务 ' + node.id + ' 连续失败/被拒, 第 ' + escalations + ' 次升级重规划(尾段替换)')
|
|
1213
|
-
const prompt = [
|
|
1214
|
-
REPLAN_HEAD,
|
|
1215
|
-
'',
|
|
1216
|
-
'[原始需求] ' + compress(REQ, REQUEST_CHARS),
|
|
1217
|
-
'',
|
|
1218
|
-
'[已完成工作]',
|
|
1219
|
-
doneSummaryText() || NONE,
|
|
1220
|
-
'',
|
|
1221
|
-
'[未通过原因]',
|
|
1222
|
-
reasons.filter(Boolean).join('; ') || '未提供',
|
|
1223
|
-
'',
|
|
1224
|
-
'[待重规划范围]',
|
|
1225
|
-
tailScopeText(node.id),
|
|
1226
|
-
'',
|
|
1227
|
-
'请重新评估剩余工作, 按 schema 返回新任务。可重排任务依赖: after 列出前置任务 id, 彼此独立的任务声明相同 after 并行执行, 省略 after = 依赖前一任务。',
|
|
1228
|
-
].join('\n')
|
|
1229
|
-
const r = await callReplanAgent(node.cursor, slotOpts('planner-escalate'), { prompt: prompt, label: 'planner:重规划#' + escalations, schema: REPLAN_SCHEMA })
|
|
1230
|
-
const nt = r && Array.isArray(r.tasks) ? normalizeTasks(r.tasks, 'e' + escalations + '-', liveIds(), Math.max(remainingTaskBudget(), 1)) : []
|
|
1231
|
-
if (!nt.length) {
|
|
1232
|
-
blocked = { nodeId: node.id, reason: '重规划无产出', detail: reasons.join('; ') }
|
|
1233
|
-
return
|
|
1234
|
-
}
|
|
1235
|
-
replaceTail(node, nt)
|
|
1236
|
-
} finally {
|
|
1237
|
-
escalating = false
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function replaceTail(failedNode, newTasks) {
|
|
1242
|
-
const bad = reachableFrom(failedNode.id)
|
|
1243
|
-
const deadSrs = []
|
|
1244
|
-
const deadXrs = []
|
|
1245
|
-
const removed = []
|
|
1246
|
-
for (const n of nodes) {
|
|
1247
|
-
if (!bad[n.id] || n.status === 'done') continue
|
|
1248
|
-
n.dead = true
|
|
1249
|
-
removed.push(n.id)
|
|
1250
|
-
if (n.type === 'review' && n.kind === 'subplan') deadSrs.push(n)
|
|
1251
|
-
if (n.type === 'review' && n.kind === 'cross') deadXrs.push(n)
|
|
1252
|
-
}
|
|
1253
|
-
// 逐任务审批模板(step-review/multi-plan 子计划): 新任务链式经各自审批节点放行
|
|
1254
|
-
const pairedReview = templateId === 'step-review' || templateId === 'multi-plan'
|
|
1255
|
-
const planSource = (deadSrs.length && subjOf(deadSrs[0].subject)) ? deadSrs[0].subject : 'PLAN'
|
|
1256
|
-
newTasks.forEach(function (t, i) {
|
|
1257
|
-
t.after = (pairedReview && i > 0) ? ['r-' + newTasks[i - 1].id] : (i > 0 ? [newTasks[i - 1].id] : [])
|
|
1258
|
-
})
|
|
1259
|
-
newTasks.forEach(function (t) {
|
|
1260
|
-
taskNode(t.id, t.description, t.after, planSource, { acceptance: t.acceptance, plannedFiles: t.files, enterReason: 'escalate' })
|
|
1261
|
-
})
|
|
1262
|
-
if (pairedReview) {
|
|
1263
|
-
newTasks.forEach(function (t) {
|
|
1264
|
-
reviewNode('r-' + t.id, '审批: ' + t.description.slice(0, 60), [t.id], t.id, 'reviewer-task', 'task')
|
|
1265
|
-
})
|
|
1266
|
-
}
|
|
1267
|
-
// 终审重建: 任务级升级不得让 lite/plan-final 静默失去终审(蓝图不变量)
|
|
1268
|
-
if (templateId === 'lite' || templateId === 'plan-final') {
|
|
1269
|
-
reviewNode('fr', '终审: 整个需求交付质量', newTasks.map(function (t) { return t.id }).concat(PREFIX_IDS), SUBJECT_OVERALL, 'reviewer-final', 'final')
|
|
1270
|
-
}
|
|
1271
|
-
// multi-plan: 被波及的子计划审与交叉终审串行链随新任务段重建
|
|
1272
|
-
if (deadSrs.length) {
|
|
1273
|
-
deadSrs.forEach(function (sr) {
|
|
1274
|
-
reviewNode(sr.id, sr.description, newTasks.map(function (t) { return t.id }), sr.subject, 'reviewer-subplan', 'subplan')
|
|
1275
|
-
})
|
|
1276
|
-
const liveSrIds = nodes.filter(function (n) { return !n.dead && n.kind === 'subplan' }).map(function (n) { return n.id })
|
|
1277
|
-
deadXrs.forEach(function (xr, i) {
|
|
1278
|
-
const deps = i === 0 ? liveSrIds : [deadXrs[i - 1].id]
|
|
1279
|
-
reviewNode(xr.id, xr.description, deps, xr.subject, 'reviewer-cross', 'cross')
|
|
1280
|
-
})
|
|
1281
|
-
}
|
|
1282
|
-
log('尾段替换: 移除 [' + removed.join(', ') + '], 接入 [' + newTasks.map(function (t) { return t.id }).join(', ') + ']')
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
async function escalateSubplan(pNode, reasons) {
|
|
1286
|
-
if (!pNode) return
|
|
1287
|
-
if (escalating) {
|
|
1288
|
-
pNode.status = 'pending'
|
|
1289
|
-
return
|
|
1290
|
-
}
|
|
1291
|
-
if (escalations >= ESCALATION_LIMIT) {
|
|
1292
|
-
blocked = { nodeId: pNode.id, reason: '子计划连续被拒且升级重规划次数已达上限', detail: reasons.join('; ') }
|
|
1293
|
-
return
|
|
1294
|
-
}
|
|
1295
|
-
escalating = true
|
|
1296
|
-
try {
|
|
1297
|
-
escalations++
|
|
1298
|
-
lifetimeEscalations++
|
|
1299
|
-
phase('升级重规划')
|
|
1300
|
-
const sp = OUTLINE[pNode.outlineIndex] || { title: pNode.description, description: pNode.description }
|
|
1301
|
-
log('子计划[' + sp.title + ']连续被拒, 第 ' + escalations + ' 次升级重规划(任务段重建)')
|
|
1302
|
-
const prompt = [
|
|
1303
|
-
REPLAN_HEAD,
|
|
1304
|
-
'',
|
|
1305
|
-
'[原始需求] ' + compress(REQ, REQUEST_CHARS),
|
|
1306
|
-
'',
|
|
1307
|
-
'[已完成工作]',
|
|
1308
|
-
doneSummaryText() || NONE,
|
|
1309
|
-
'',
|
|
1310
|
-
'[未通过原因]',
|
|
1311
|
-
reasons.filter(Boolean).join('; ') || '未提供',
|
|
1312
|
-
'',
|
|
1313
|
-
'[待重规划范围]',
|
|
1314
|
-
'子计划[' + sp.title + ']的任务段(重建, 该子计划在途产物作废)。',
|
|
1315
|
-
'',
|
|
1316
|
-
'请重新评估该子计划的剩余工作, 按 schema 返回新任务。可重排任务依赖: after 列出前置任务 id, 彼此独立的任务声明相同 after 并行执行, 省略 after = 依赖前一任务。',
|
|
1317
|
-
].join('\n')
|
|
1318
|
-
const r = await callReplanAgent(pNode.cursor, slotOpts('planner-escalate'), { prompt: prompt, label: 'planner:子计划重规划#' + escalations, schema: REPLAN_SCHEMA })
|
|
1319
|
-
const nt = r && Array.isArray(r.tasks) ? normalizeTasks(r.tasks, 'e' + escalations + '-', liveIds(), Math.max(remainingTaskBudget(), 1)) : []
|
|
1320
|
-
if (!nt.length) {
|
|
1321
|
-
blocked = { nodeId: pNode.id, reason: '子计划重规划无产出', detail: reasons.join('; ') }
|
|
1322
|
-
return
|
|
1323
|
-
}
|
|
1324
|
-
replaceTail(pNode, nt)
|
|
1325
|
-
} finally {
|
|
1326
|
-
escalating = false
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// 交付类(fr/xr)达阈值: 返工重规划, 追加返工任务链, 审批重挂其后
|
|
1331
|
-
async function rework(reviewNodes, reasons) {
|
|
1332
|
-
const label = reviewNodes.map(function (n) { return n.id }).join('/')
|
|
1333
|
-
// 与其余三个升级入口一致的重入保护: 在途升级时待重试, 不叠加升级账
|
|
1334
|
-
if (escalating) {
|
|
1335
|
-
reviewNodes.forEach(function (n) { n.status = 'pending'; n.reviewNote = '(并发失败, 待重规划后重试)' })
|
|
1336
|
-
return
|
|
1337
|
-
}
|
|
1338
|
-
if (escalations >= ESCALATION_LIMIT) {
|
|
1339
|
-
blocked = { nodeId: label, reason: '终审/交叉终审被拒且升级重规划次数已达上限', detail: reasons.join('; ') }
|
|
1340
|
-
return
|
|
1341
|
-
}
|
|
1342
|
-
escalating = true
|
|
1343
|
-
try {
|
|
1344
|
-
escalations++
|
|
1345
|
-
lifetimeEscalations++
|
|
1346
|
-
phase('升级重规划')
|
|
1347
|
-
log(label + ' 被拒, 第 ' + escalations + ' 次返工重规划')
|
|
1348
|
-
const prompt = [
|
|
1349
|
-
REPLAN_HEAD,
|
|
1350
|
-
'',
|
|
1351
|
-
'[原始需求] ' + compress(REQ, REQUEST_CHARS),
|
|
1352
|
-
'',
|
|
1353
|
-
'[已完成工作]',
|
|
1354
|
-
doneSummaryText() || NONE,
|
|
1355
|
-
'',
|
|
1356
|
-
'[未通过原因]',
|
|
1357
|
-
reasons.filter(Boolean).join('; ') || '未提供',
|
|
1358
|
-
'',
|
|
1359
|
-
'[待重规划范围]',
|
|
1360
|
-
'被驳回交付的问题修复(只追加返工任务, 不重复已完成工作)。',
|
|
1361
|
-
'',
|
|
1362
|
-
'请重新评估, 按 schema 返回返工任务。可重排任务依赖: after 列出前置任务 id, 省略 after = 依赖前一任务。',
|
|
1363
|
-
].join('\n')
|
|
1364
|
-
const r = await callReplanAgent(reviewNodes[0].cursor, slotOpts('planner-escalate'), { prompt: prompt, label: 'planner:返工#' + escalations, schema: REPLAN_SCHEMA })
|
|
1365
|
-
const nt = r && Array.isArray(r.tasks) ? normalizeTasks(r.tasks, 'f' + escalations + '-', liveIds(), Math.max(remainingTaskBudget(), 1)) : []
|
|
1366
|
-
if (!nt.length) {
|
|
1367
|
-
blocked = { nodeId: label, reason: '返工重规划无产出', detail: reasons.join('; ') }
|
|
1368
|
-
return
|
|
1369
|
-
}
|
|
1370
|
-
nt.forEach(function (t, i) { t.after = i > 0 ? [nt[i - 1].id] : [] })
|
|
1371
|
-
nt.forEach(function (t) { taskNode(t.id, t.description, t.after, 'PLAN', { acceptance: t.acceptance, plannedFiles: t.files, enterReason: 'escalate' }) })
|
|
1372
|
-
reviewNodes.forEach(function (n) {
|
|
1373
|
-
n.fixNote = reasons.join('; ')
|
|
1374
|
-
n.status = 'pending'
|
|
1375
|
-
n.deps = nt.map(function (t) { return t.id })
|
|
1376
|
-
})
|
|
1377
|
-
log('追加返工任务 [' + nt.map(function (t) { return t.id }).join(', ') + '], 审批重挂到返工之后')
|
|
1378
|
-
} finally {
|
|
1379
|
-
escalating = false
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
// ── 主调度循环: 就绪集合驱动, plan 先行, 任务并行, 审批串行 ──────────────────
|
|
1384
|
-
// 调度预算按当前图规模每轮现算, 大图不误杀
|
|
1385
|
-
// 每个任务节点可被拒绝/失败打回 reviewRejectBeforeEscalate 次, 调度预算按此扩容防大阈值图误杀
|
|
1386
|
-
function loopBudget() {
|
|
1387
|
-
return LOOP_BUDGET_BASE + (nodes.length * (1 + BUDGET.reviewRejectBeforeEscalate) + lifetimeEscalations * LOOP_BUDGET_PER_ESCALATION) * LOOP_BUDGET_PER_NODE
|
|
1388
|
-
}
|
|
1389
|
-
phase('执行与审批')
|
|
1390
|
-
let loops = 0
|
|
1391
|
-
while (!blocked) {
|
|
1392
|
-
loops++
|
|
1393
|
-
if (loops > loopBudget()) { blocked = { nodeId: '-', reason: '调度循环超出预算(' + loopBudget() + ' 轮)', detail: '可能存在无法收敛的审批循环' }; break }
|
|
1394
|
-
const doneMap = {}
|
|
1395
|
-
for (const n of nodes) if (!n.dead && n.status === 'done') doneMap[n.id] = true
|
|
1396
|
-
const ready = nodes.filter(function (n) { return !n.dead && n.status === 'pending' && n.deps.every(function (d) { return doneMap[d] }) })
|
|
1397
|
-
if (!ready.length) {
|
|
1398
|
-
const stuck = nodes.filter(function (n) { return !n.dead && n.status === 'pending' })
|
|
1399
|
-
if (stuck.length) blocked = { nodeId: '-', reason: '存在无法就绪的节点(依赖失败或缺失)', detail: stuck.map(function (n) { return n.id }).join(', ') }
|
|
1400
|
-
break
|
|
1401
|
-
}
|
|
1402
|
-
const planReady = ready.filter(function (n) { return n.type === 'plan' })
|
|
1403
|
-
const taskReady = ready.filter(function (n) { return n.type === 'task' })
|
|
1404
|
-
const reviewReady = ready.filter(function (n) { return n.type === 'review' })
|
|
1405
|
-
for (const n of planReady) { await runPlanNode(n); if (blocked) break }
|
|
1406
|
-
if (blocked) break
|
|
1407
|
-
if (taskReady.length) {
|
|
1408
|
-
taskReady.forEach(function (n) {
|
|
1409
|
-
n.siblings = taskReady.filter(function (x) { return x !== n }).map(function (x) { return x.description })
|
|
1410
|
-
})
|
|
1411
|
-
await parallel(taskReady.map(function (n) { return function () { return runTaskWithRetry(n) } }))
|
|
1412
|
-
}
|
|
1413
|
-
if (blocked) break
|
|
1414
|
-
for (const n of reviewReady) {
|
|
1415
|
-
if (n.dead || n.status !== 'pending') continue
|
|
1416
|
-
let ok = true
|
|
1417
|
-
for (const d of n.deps) if (!doneMap[d]) { ok = false; break }
|
|
1418
|
-
if (!ok) continue // 依赖刚被重挂(重做在途), 下轮再审
|
|
1419
|
-
await runReview(n)
|
|
1420
|
-
if (blocked) break
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// ── 汇总 ────────────────────────────────────────────────────────────────────
|
|
1425
|
-
phase('汇总')
|
|
1426
|
-
const live = nodes.filter(function (n) { return !n.dead })
|
|
1427
|
-
const allDone = live.length > 0 && live.every(function (n) { return n.status === 'done' })
|
|
1428
|
-
return {
|
|
1429
|
-
ok: allDone && !blocked,
|
|
1430
|
-
templateId: templateId,
|
|
1431
|
-
templateSource: templateSource,
|
|
1432
|
-
difficulty: { complexity: triage.complexity || '', risk: triage.risk || '', scope: triage.scope || '' },
|
|
1433
|
-
triageReasoning: String(triage.reasoning || ''),
|
|
1434
|
-
plan: PLAN_TEXT,
|
|
1435
|
-
escalations: escalations,
|
|
1436
|
-
blocked: blocked,
|
|
1437
|
-
reviews: live.filter(function (n) { return n.type === 'review' }).map(function (n) {
|
|
1438
|
-
// verdict 只取实审记录; blocked 时未运行到的评审标 UNREVIEWED, 不冒充 REJECTED
|
|
1439
|
-
const verdict = n.verdict || (blocked ? 'UNREVIEWED' : 'REJECTED')
|
|
1440
|
-
return { id: n.id, description: n.description, verdict: verdict, warn: !!n.warn, reviewerFault: !!n.reviewerFault, summary: String(n.output || ''), evidence: String(n.reviewEvidence || '') }
|
|
1441
|
-
}),
|
|
1442
|
-
tasks: live.filter(function (n) { return n.type !== 'review' }).map(function (n) {
|
|
1443
|
-
return { id: n.id, type: n.type, description: n.description, status: n.status, summary: String(n.output || ''), changedFiles: n.changedFiles || [] }
|
|
1444
|
-
}),
|
|
1445
|
-
changedFiles: unionChangedFiles(),
|
|
1446
|
-
}
|