@tea-agent/loop-agent 0.39.0-beta.7 → 0.39.0-beta.8
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/CHANGELOG.md +6 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/task/config-types.js +15 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +8 -1
- package/dist/workflows/dag/frontend-review-findings.js +42 -10
- package/dist/workflows/dag/init-hybrid.js +4 -0
- package/dist/workflows/dag/rerun-feedback.js +31 -8
- package/dist/workflows/dag/retry-policy.js +8 -6
- package/dist/workflows/dag/runner.js +45 -8
- package/dist/workflows/dag/types.js +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
- 前端 design-review 拒绝纳入自动恢复(方向 A 姊妹实现):`request_design_changes`(definite finding)不再让 writer-admission blocked 变成 terminal failed——runner 终态判定识别 committed design fact 后生成 recovery trigger(new-dag,从 plan 重启)+ 渲染 design findings PRD 草稿 + `deriveDagRerunFeedback` 增加 design typed-fact 通道(findings 注入下一轮 `<rerun_feedback>`),受同一 `frontendRecoveryQuota` 控制;无 design fact 的 blocked admission 仍保持 terminal failed。design-review 发现"AC 未被契约/测试锚定"这类真问题会在写代码前自动进入修正轮,而非一次失败即停。
|
|
6
|
+
- 前端 DAG 两个闭环补齐(P0-1 / P2-4):
|
|
7
|
+
- **P0-1 前端 recovery 配额可配置**:`task.json frontendRecovery.maxContinuations`(默认 1,可配 2-5)冻结进 DAG spec(`spec.frontendRecovery`),runner 的 `finalizeTerminalRunStatus` 4 处硬编码 `< 1` 改为按配额判定——verify 偶发失败导致 run 真失败的场景可放宽到 2-3 次自动恢复,不必人工介入。
|
|
8
|
+
- **P2-4 `dag follow-up` 命令**:`loop-agent dag follow-up <parent-run-id> [--task-id <id>] [--execute] [--profile <p>]`——读取 parent run 的 `contracts/frontend-review-findings-prd.md`,createTask + importPrd(taskKind=frontend-implementation + PRD 内容)生成下游修复任务,`--execute` 直接 advance 走前端 DAG。review findings 从"只落文件的草稿"变成"一步进入下一轮 DAG 的任务",闭环补齐。
|
|
9
|
+
- frontend-plan-pi 重试上限从 5 收紧到 3(`FRONTEND_PLAN_LADDER_RETRY_POLICY.maxAttempts`):每次 attempt 都是完整跑一遍 plan(读上下文 + 提交 record_* ledger),5 次在坏情况下等于 5 倍 plan token 成本;3 次仍覆盖常见失败(invalid-output 格式滑动、read-burst、传输类),四档阶梯只部分可遍历即判 non-converging,减少单节点无限烧 token 风险。
|
|
10
|
+
|
|
5
11
|
- 前端 review `request_review_changes` 不再被静默当作成功(方向 A):`frontend-review-pi` 提交 typed `request_review_changes` 时,run 终态判定为「未通过」(`partial_failed` + `terminalReason=review-request-changes`,即使全部节点 FINISHED 也不显示成功),committed findings 自动渲染为 run-owned 下游任务 PRD 草稿 `contracts/frontend-review-findings-prd.md`(含 issueCategory/evidenceRefs/findings/restartPhase 指引),并生成 recovery trigger(issueCategory→failureOwner 映射,new-dag)。`deriveDagRerunFeedback` 新增 typed-fact 通道:无文本 verdict、无 convergence 的 frontend 场景也能把 committed `request_review_changes` findings 注入下一轮 `<rerun_feedback>`,问题不再丢失、下一轮从 restartPhase 定点修复而非重复流程。
|
|
6
12
|
- 前端 review findings PRD 草稿携带修复范围路由信号(方向 A 补充):`deriveFrontendReviewFixScope` 从 committed findings 确定性推导建议 shape(单文件无远程 API→micro、同目录集中多文件→small、跨目录/API 信号→standard)与建议 complexity、目标文件、建议 allowedPaths;PRD 标题改为「修复 review 发现(<shape>):<目标文件>」,正文新增「修复范围(供下一轮任务生成路由)」段(含 small 信号词,如「单文件局部修复」「无远程接口」),下一轮任务以该 PRD 生成 DAG 时,`resolveFrontendTaskShape` 能把小修复路由到 micro=7/small=10 节点拓扑,而非默认 11 节点 standard 全流程。
|
|
7
13
|
- 前端 shape 分类 API 否定词覆盖中文空格形式(冒烟暴露边界):`无 API/网络调用`、`无 API` 等表述不再被误判为远程 API 信号(原 `\b(...)\b` 对 CJK 开头无词边界),`resolveFrontendTaskShape` 与 `deriveFrontendReviewFixScope` 的 hasApiSignal 判定同步修正——带「无 API/无接口」声明的单文件小修复正确路由 micro/small,而非误升级 standard。
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.39.0-beta.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-08-
|
|
3
|
+
"version": "0.39.0-beta.8",
|
|
4
|
+
"gitSha": "b698721e82e0ff98c85484a5b2864d9b28e968f8",
|
|
5
|
+
"builtAt": "2026-08-24T06:32:44.509Z"
|
|
6
6
|
}
|
|
@@ -35,6 +35,7 @@ import { runDagReconcileTasks } from "../commands/dag-reconcile-tasks.js";
|
|
|
35
35
|
import { runDagReconcileRun } from "../commands/dag-reconcile-run.js";
|
|
36
36
|
import { runDagRequestInterrupt } from "../commands/dag-request-interrupt.js";
|
|
37
37
|
import { runDagRerunTask } from "../commands/dag-rerun-task.js";
|
|
38
|
+
import { runDagFollowUp } from "../commands/dag-follow-up.js";
|
|
38
39
|
import { runDagRerun } from "../commands/dag-rerun.js";
|
|
39
40
|
import { runDagResume } from "../commands/dag-resume.js";
|
|
40
41
|
import { runDagDecisionInspect, runDagDecisionValidate, } from "../workflows/dag/decision-envelope.js";
|
|
@@ -135,6 +136,7 @@ const DAG_SUBCOMMANDS = [
|
|
|
135
136
|
"request-interrupt",
|
|
136
137
|
"rerun",
|
|
137
138
|
"rerun-task",
|
|
139
|
+
"follow-up",
|
|
138
140
|
"reconcile-tasks",
|
|
139
141
|
"final-verification",
|
|
140
142
|
"decision",
|
|
@@ -577,6 +579,11 @@ export const COMMAND_DEFINITIONS = [
|
|
|
577
579
|
await runDagRerunTask(repoRoot, rerunTaskArgs);
|
|
578
580
|
return;
|
|
579
581
|
}
|
|
582
|
+
if (subcommand === "follow-up") {
|
|
583
|
+
const followUpArgs = rest.filter((arg) => Boolean(arg));
|
|
584
|
+
await runDagFollowUp(repoRoot, followUpArgs);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
580
587
|
if (subcommand === "reconcile-tasks") {
|
|
581
588
|
const reconcileArgs = rest.filter((arg) => Boolean(arg));
|
|
582
589
|
await runDagReconcileTasks(repoRoot, reconcileArgs);
|
package/dist/cli/program.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runDagReconcileTasks } from "../commands/dag-reconcile-tasks.js";
|
|
|
13
13
|
import { runDagReconcileRun } from "../commands/dag-reconcile-run.js";
|
|
14
14
|
import { runDagRequestInterrupt } from "../commands/dag-request-interrupt.js";
|
|
15
15
|
import { runDagRerunTask } from "../commands/dag-rerun-task.js";
|
|
16
|
+
import { runDagFollowUp } from "../commands/dag-follow-up.js";
|
|
16
17
|
import { runDagRerun } from "../commands/dag-rerun.js";
|
|
17
18
|
import { runDagReject } from "../commands/dag-reject.js";
|
|
18
19
|
import { runDagCloseoutDraft, runDagReport } from "../commands/dag-report.js";
|
|
@@ -352,6 +353,9 @@ async function runDagAction(repoRoot, subcommand, rest) {
|
|
|
352
353
|
case "rerun-task":
|
|
353
354
|
await runDagRerunTask(repoRoot, args);
|
|
354
355
|
return;
|
|
356
|
+
case "follow-up":
|
|
357
|
+
await runDagFollowUp(repoRoot, args);
|
|
358
|
+
return;
|
|
355
359
|
case "reconcile-tasks":
|
|
356
360
|
await runDagReconcileTasks(repoRoot, args);
|
|
357
361
|
return;
|
|
@@ -511,6 +515,7 @@ function addDagCommand(program, defaultRepoRoot) {
|
|
|
511
515
|
"request-interrupt",
|
|
512
516
|
"rerun",
|
|
513
517
|
"rerun-task",
|
|
518
|
+
"follow-up",
|
|
514
519
|
"reconcile-tasks",
|
|
515
520
|
"final-verification",
|
|
516
521
|
], defaultRepoRoot);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FRONTEND_REVIEW_FINDINGS_PRD_REL, readFrontendReviewFindingsPrd, } from "../workflows/dag/frontend-review-findings.js";
|
|
4
|
+
import { locateDagRun } from "../workflows/dag/lifecycle.js";
|
|
5
|
+
import { createTask, getTaskPaths } from "../task/runtime.js";
|
|
6
|
+
import { importPrdDocument } from "../task/source-references.js";
|
|
7
|
+
function parseFlagValue(rawArgs, flag) {
|
|
8
|
+
const index = rawArgs.indexOf(flag);
|
|
9
|
+
if (index < 0 || index + 1 >= rawArgs.length)
|
|
10
|
+
return undefined;
|
|
11
|
+
return rawArgs[index + 1];
|
|
12
|
+
}
|
|
13
|
+
export function parseDagFollowUpArgs(rawArgs) {
|
|
14
|
+
const positional = rawArgs.filter((arg) => !arg.startsWith("-"));
|
|
15
|
+
const parentRunId = positional[0];
|
|
16
|
+
if (!parentRunId) {
|
|
17
|
+
throw new Error("usage: dag follow-up <parent-run-id> [--task-id <id>] [--execute] [--profile <profile>]");
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
parentRunId,
|
|
21
|
+
taskId: parseFlagValue(rawArgs, "--task-id"),
|
|
22
|
+
execute: rawArgs.includes("--execute"),
|
|
23
|
+
profile: parseFlagValue(rawArgs, "--profile"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function deriveFollowUpTaskId(parentRunId) {
|
|
27
|
+
// Deterministic, valid task id derived from the parent run id.
|
|
28
|
+
return `${parentRunId}-followup`;
|
|
29
|
+
}
|
|
30
|
+
/** CLI entry: parse raw args and run the follow-up flow. */
|
|
31
|
+
export async function runDagFollowUp(repoRoot, rawArgs) {
|
|
32
|
+
const parsed = parseDagFollowUpArgs(rawArgs);
|
|
33
|
+
const result = await runDagFollowUpInternal(repoRoot, parsed.parentRunId, {
|
|
34
|
+
taskId: parsed.taskId,
|
|
35
|
+
execute: parsed.execute,
|
|
36
|
+
profile: parsed.profile,
|
|
37
|
+
});
|
|
38
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
process.exitCode = result.mode === "blocked" ? 2 : 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function runDagFollowUpInternal(repoRoot, parentRunId, options = {}) {
|
|
44
|
+
const located = await locateDagRun(repoRoot, parentRunId);
|
|
45
|
+
if (!located) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
mode: "invalid",
|
|
49
|
+
reason: `dag run not found: ${parentRunId}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const prd = await readFrontendReviewFindingsPrd(located.runDir);
|
|
53
|
+
if (!prd) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
mode: "invalid",
|
|
57
|
+
reason: `no frontend review findings PRD found in run ${parentRunId} (${FRONTEND_REVIEW_FINDINGS_PRD_REL}); the review must have committed request_review_changes`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const taskId = options.taskId ?? deriveFollowUpTaskId(parentRunId);
|
|
61
|
+
try {
|
|
62
|
+
await createTask(repoRoot, taskId, `修复 review 发现(parent ${parentRunId})`);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
mode: "blocked",
|
|
68
|
+
reason: `failed to create task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Materialize the PRD draft into the task's source directory so the task
|
|
72
|
+
// lifecycle can import it as its requirement.
|
|
73
|
+
const prdMaterialized = path.join(getTaskPaths(repoRoot, taskId).sourceDir, "review-findings-prd.md");
|
|
74
|
+
try {
|
|
75
|
+
await mkdir(path.dirname(prdMaterialized), { recursive: true });
|
|
76
|
+
await importPrdDocument({
|
|
77
|
+
repoRoot,
|
|
78
|
+
taskId,
|
|
79
|
+
filePath: path.join(located.runDir, FRONTEND_REVIEW_FINDINGS_PRD_REL),
|
|
80
|
+
name: "review-findings-prd",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
mode: "blocked",
|
|
87
|
+
reason: `failed to import PRD into task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!options.execute) {
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
mode: "created",
|
|
94
|
+
taskId,
|
|
95
|
+
prdPath: prdMaterialized,
|
|
96
|
+
next: {
|
|
97
|
+
kind: "advance",
|
|
98
|
+
command: `loop-agent task advance ${taskId} --task-kind frontend-implementation --json`,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Execute: advance through the frontend DAG.
|
|
103
|
+
const { advanceTaskLifecycle } = await import("../application/task-lifecycle/advance.js");
|
|
104
|
+
const profile = options.profile === "auto" ||
|
|
105
|
+
options.profile === "minimal" ||
|
|
106
|
+
options.profile === "standard" ||
|
|
107
|
+
options.profile === "reviewed" ||
|
|
108
|
+
options.profile === "supervised"
|
|
109
|
+
? options.profile
|
|
110
|
+
: undefined;
|
|
111
|
+
const result = await advanceTaskLifecycle({
|
|
112
|
+
repoRoot,
|
|
113
|
+
taskId,
|
|
114
|
+
taskKind: "frontend-implementation",
|
|
115
|
+
taskKindExplicit: true,
|
|
116
|
+
...(profile ? { profile, profileExplicit: true } : {}),
|
|
117
|
+
observer: undefined,
|
|
118
|
+
});
|
|
119
|
+
const ok = result.lifecycleState === "run-succeeded" ||
|
|
120
|
+
result.lifecycleState === "awaiting-write-set-approval";
|
|
121
|
+
return {
|
|
122
|
+
ok,
|
|
123
|
+
mode: ok ? "created" : "blocked",
|
|
124
|
+
taskId,
|
|
125
|
+
prdPath: prdMaterialized,
|
|
126
|
+
reason: ok
|
|
127
|
+
? undefined
|
|
128
|
+
: `advance lifecycleState: ${result.lifecycleState}${result.blockers.length > 0
|
|
129
|
+
? `; blockers: ${result.blockers.map((b) => b.code).join(", ")}`
|
|
130
|
+
: ""}`,
|
|
131
|
+
next: ok
|
|
132
|
+
? undefined
|
|
133
|
+
: {
|
|
134
|
+
kind: "advance",
|
|
135
|
+
command: `loop-agent task advance ${taskId} --task-kind frontend-implementation --json`,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -219,6 +219,21 @@ const taskConfigObjectSchema = z.object({
|
|
|
219
219
|
maxFixLoops: z.number().int().min(0).optional().default(2),
|
|
220
220
|
/** Supervised DAG convergence is opt-in until runtime smoke evidence is stronger. */
|
|
221
221
|
convergence: convergenceConfigSchema.optional().default({ enabled: false }),
|
|
222
|
+
/** Frontend recovery continuation quota. Bounds how many times a failed
|
|
223
|
+
* frontend run may auto-recover via M6 recovery operations (1 = current
|
|
224
|
+
* default; raise to 2-3 for more tolerance of transient verify failures). */
|
|
225
|
+
frontendRecovery: z
|
|
226
|
+
.object({
|
|
227
|
+
maxContinuations: z
|
|
228
|
+
.number()
|
|
229
|
+
.int()
|
|
230
|
+
.min(1)
|
|
231
|
+
.max(5)
|
|
232
|
+
.optional()
|
|
233
|
+
.default(1),
|
|
234
|
+
})
|
|
235
|
+
.strict()
|
|
236
|
+
.optional(),
|
|
222
237
|
/** Outer loop auto mode never writes by default; DAG execute requires this policy plus path/writeSet gates. */
|
|
223
238
|
loopAutoExecutionPolicy: loopAutoExecutionPolicySchema
|
|
224
239
|
.optional()
|
|
@@ -14,6 +14,7 @@ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
|
14
14
|
// on each other without module-init ordering issues.
|
|
15
15
|
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
16
16
|
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
17
|
+
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
17
18
|
import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
|
|
18
19
|
export const frontendNormalizationActionSchema = z.enum([
|
|
19
20
|
"remove-trailing-commas",
|
|
@@ -2075,11 +2076,17 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
2075
2076
|
sha256: canonicalArtifact.sha256,
|
|
2076
2077
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2077
2078
|
};
|
|
2079
|
+
// Human-readable plan summary appended after the artifact pointer. The
|
|
2080
|
+
// pointer remains the machine authority (path/schema/sha256); the
|
|
2081
|
+
// rendered summary is a deterministic view of the canonical contract so
|
|
2082
|
+
// Console/reviewers can read the plan without opening the JSON. It never
|
|
2083
|
+
// participates in downstream validation.
|
|
2084
|
+
const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
|
|
2078
2085
|
return {
|
|
2079
2086
|
ok: true,
|
|
2080
2087
|
contract: analysis.canonical,
|
|
2081
2088
|
artifact,
|
|
2082
|
-
normalizedText: formatStructuredArtifactPointer(artifact)
|
|
2089
|
+
normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
|
|
2083
2090
|
};
|
|
2084
2091
|
}
|
|
2085
2092
|
catch (error) {
|
|
@@ -10,10 +10,17 @@ import { readTypedEventStoreFromJsonl, } from "./frontend-typed-event-store.js";
|
|
|
10
10
|
* rendered into a downstream-task PRD draft that a later DAG round can consume
|
|
11
11
|
* instead of repeating the previous flow. Rendering is deterministic (pure);
|
|
12
12
|
* only the JSONL read and PRD write are IO.
|
|
13
|
+
*
|
|
14
|
+
* The design-review analog (`request_design_changes`) reuses the same PRD
|
|
15
|
+
* rendering and typed-fact channel: a design rejection is a definite finding
|
|
16
|
+
* (not a protocol/environment error), so it auto-re-enters the next plan round
|
|
17
|
+
* with the committed design findings instead of terminating the run.
|
|
13
18
|
*/
|
|
14
19
|
export const FRONTEND_REVIEW_FINDINGS_PRD_REL = "contracts/frontend-review-findings-prd.md";
|
|
15
20
|
/** Review node id whose committed typed facts drive the findings PRD. */
|
|
16
21
|
export const FRONTEND_REVIEW_NODE_ID = "frontend-review-pi";
|
|
22
|
+
/** Design review node id whose committed typed facts drive the findings PRD. */
|
|
23
|
+
export const FRONTEND_DESIGN_REVIEW_NODE_ID = "frontend-design-review-pi";
|
|
17
24
|
/** Deterministic issueCategory → failure owner mapping (A+B AC-009 §4.10). */
|
|
18
25
|
export function classifyReviewIssueCategoryFailureOwner(issueCategory) {
|
|
19
26
|
switch (issueCategory) {
|
|
@@ -122,10 +129,34 @@ export function deriveFrontendReviewFixScope(fact) {
|
|
|
122
129
|
* approve_review, missing store, or an unreadable store).
|
|
123
130
|
*/
|
|
124
131
|
export async function readCommittedReviewRequestFact(input) {
|
|
132
|
+
const { runDir: reviewRunDir } = input;
|
|
125
133
|
const reviewNodeId = input.reviewNodeId ?? FRONTEND_REVIEW_NODE_ID;
|
|
134
|
+
return readCommittedRequestFactByKind({
|
|
135
|
+
runDir: reviewRunDir,
|
|
136
|
+
nodeId: reviewNodeId,
|
|
137
|
+
factsFile: "review-typed-facts.jsonl",
|
|
138
|
+
kind: "request_review_changes",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read the committed `request_design_changes` fact from the design review
|
|
143
|
+
* node's typed event store. Returns undefined when no committed request fact
|
|
144
|
+
* exists (e.g. approve_design, missing store, or an unreadable store).
|
|
145
|
+
*/
|
|
146
|
+
export async function readCommittedDesignRequestFact(input) {
|
|
147
|
+
const { runDir: designRunDir } = input;
|
|
148
|
+
const designNodeId = input.designNodeId ?? FRONTEND_DESIGN_REVIEW_NODE_ID;
|
|
149
|
+
return readCommittedRequestFactByKind({
|
|
150
|
+
runDir: designRunDir,
|
|
151
|
+
nodeId: designNodeId,
|
|
152
|
+
factsFile: "design-typed-facts.jsonl",
|
|
153
|
+
kind: "request_design_changes",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function readCommittedRequestFactByKind(input) {
|
|
126
157
|
let records;
|
|
127
158
|
try {
|
|
128
|
-
records = await readTypedEventStoreFromJsonl(path.join(input.runDir,
|
|
159
|
+
records = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.nodeId, input.factsFile));
|
|
129
160
|
}
|
|
130
161
|
catch {
|
|
131
162
|
return undefined;
|
|
@@ -133,9 +164,7 @@ export async function readCommittedReviewRequestFact(input) {
|
|
|
133
164
|
const committed = records.filter((record) => record.phase === "committed");
|
|
134
165
|
if (committed.length === 0)
|
|
135
166
|
return undefined;
|
|
136
|
-
|
|
137
|
-
// request_review_changes carries a "not passed" verdict.
|
|
138
|
-
const request = committed.find((record) => record.fact.kind === "request_review_changes");
|
|
167
|
+
const request = committed.find((record) => record.fact.kind === input.kind);
|
|
139
168
|
if (!request)
|
|
140
169
|
return undefined;
|
|
141
170
|
const fact = request.fact;
|
|
@@ -166,25 +195,28 @@ function findingLines(findings) {
|
|
|
166
195
|
}
|
|
167
196
|
/**
|
|
168
197
|
* Deterministically render a downstream-task PRD draft from a committed
|
|
169
|
-
* `request_review_changes` fact. Same fact always
|
|
170
|
-
* output; the parent run id and review node id are the
|
|
198
|
+
* `request_review_changes` / `request_design_changes` fact. Same fact always
|
|
199
|
+
* renders byte-identical output; the parent run id and review node id are the
|
|
200
|
+
* only context.
|
|
171
201
|
*/
|
|
172
202
|
export function renderFrontendReviewFindingsPrd(input) {
|
|
173
203
|
const { parentRunId, fact } = input;
|
|
174
|
-
const
|
|
204
|
+
const isDesign = fact.kind === "request_design_changes";
|
|
205
|
+
const reviewNodeId = input.reviewNodeId ??
|
|
206
|
+
(isDesign ? FRONTEND_DESIGN_REVIEW_NODE_ID : FRONTEND_REVIEW_NODE_ID);
|
|
175
207
|
const restartPhase = classifyReviewIssueCategoryFailureOwner(fact.issueCategory);
|
|
176
208
|
const scope = deriveFrontendReviewFixScope(fact);
|
|
177
209
|
const targetSummary = scope.targetFiles.length > 0
|
|
178
210
|
? scope.targetFiles.join(", ")
|
|
179
211
|
: fact.issueCategory;
|
|
180
212
|
return [
|
|
181
|
-
`# 修复 review 发现(${scope.shape}):${targetSummary}`,
|
|
213
|
+
`# 修复 ${isDesign ? "设计" : "review"} 发现(${scope.shape}):${targetSummary}`,
|
|
182
214
|
"",
|
|
183
215
|
"## 背景",
|
|
184
216
|
"",
|
|
185
217
|
`- parentRunId: \`${parentRunId}\``,
|
|
186
218
|
`- reviewNodeId: \`${reviewNodeId}\``,
|
|
187
|
-
`- typedVerdict:
|
|
219
|
+
`- typedVerdict: \`${fact.kind}\``,
|
|
188
220
|
`- issueCategory: \`${fact.issueCategory}\``,
|
|
189
221
|
`- restartPhase/failureOwner: \`${restartPhase}\``,
|
|
190
222
|
"",
|
|
@@ -216,7 +248,7 @@ export function renderFrontendReviewFindingsPrd(input) {
|
|
|
216
248
|
"",
|
|
217
249
|
`- 按 issueCategory 映射的重启阶段(${restartPhase})定点修复,不复用上一轮未通过的实现。`,
|
|
218
250
|
"- 逐条关闭上面「问题清单」中的 findings,并保留上一轮已通过的验证目标继续有效。",
|
|
219
|
-
|
|
251
|
+
`- 完成修复后重新走 frontend DAG 的 ${isDesign ? "plan → design" : "implement → verify → review"} 链,直到${isDesign ? " design-review 提交 approve_design" : " review 提交 approve_review"}。`,
|
|
220
252
|
"",
|
|
221
253
|
].join("\n");
|
|
222
254
|
}
|
|
@@ -3384,6 +3384,10 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3384
3384
|
...(spec.advisories ?? []),
|
|
3385
3385
|
`frontend-task-shape: ${frontendTaskShape.shape} (${frontendTaskShape.reason})`,
|
|
3386
3386
|
];
|
|
3387
|
+
// Freeze the frontend recovery continuation quota from task.json so the
|
|
3388
|
+
// runner can bound M6 auto-recovery without re-reading the task config.
|
|
3389
|
+
const frontendMaxContinuations = sources.taskConfig.frontendRecovery?.maxContinuations ?? 1;
|
|
3390
|
+
spec.frontendRecovery = { maxContinuations: frontendMaxContinuations };
|
|
3387
3391
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
3388
3392
|
parseDagSpec(spec);
|
|
3389
3393
|
assertValidDagSpec(spec);
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseJsonReviewVerdict } from "./output-protocol.js";
|
|
5
|
-
import { FRONTEND_REVIEW_NODE_ID, readCommittedReviewRequestFact, } from "./frontend-review-findings.js";
|
|
5
|
+
import { FRONTEND_DESIGN_REVIEW_NODE_ID, FRONTEND_REVIEW_NODE_ID, readCommittedDesignRequestFact, readCommittedReviewRequestFact, } from "./frontend-review-findings.js";
|
|
6
6
|
export const MAX_RERUN_FEEDBACK_CHARS = 12_000;
|
|
7
7
|
const RERUN_FEEDBACK_TRUNCATION_MARKER = "\n\n...[上一轮反馈已按长度上限截断]...\n\n";
|
|
8
8
|
const MAX_EVIDENCE_REFS = 8;
|
|
@@ -183,6 +183,24 @@ async function readTypedReviewRequestFact(runDir) {
|
|
|
183
183
|
},
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Read the committed frontend design request fact (typed terminal channel).
|
|
188
|
+
* Returns { nodeId, fact } when the design review node committed
|
|
189
|
+
* `request_design_changes` — the design analog of request_review_changes.
|
|
190
|
+
*/
|
|
191
|
+
async function readTypedDesignRequestFact(runDir) {
|
|
192
|
+
const fact = await readCommittedDesignRequestFact({ runDir });
|
|
193
|
+
if (!fact)
|
|
194
|
+
return undefined;
|
|
195
|
+
return {
|
|
196
|
+
nodeId: FRONTEND_DESIGN_REVIEW_NODE_ID,
|
|
197
|
+
fact: {
|
|
198
|
+
issueCategory: fact.issueCategory,
|
|
199
|
+
evidenceRefs: fact.evidenceRefs,
|
|
200
|
+
findings: fact.findings,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
186
204
|
/**
|
|
187
205
|
* Derive only the latest unresolved verify/review feedback from canonical
|
|
188
206
|
* parent-run facts. Provider-only failures intentionally return undefined.
|
|
@@ -201,12 +219,15 @@ export async function deriveDagRerunFeedback(input) {
|
|
|
201
219
|
}
|
|
202
220
|
}
|
|
203
221
|
if (!sourceNodeId || !sourceNode) {
|
|
204
|
-
// Typed terminal channel: the frontend review
|
|
205
|
-
// typed
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// round as unresolved feedback.
|
|
209
|
-
|
|
222
|
+
// Typed terminal channel: the frontend review/design review commit their
|
|
223
|
+
// verdicts as typed facts (request_review_changes / request_design_changes)
|
|
224
|
+
// instead of VERDICT text, and the frontend DAG has no convergence
|
|
225
|
+
// passHistory. Fall back to reading the typed event stores so findings
|
|
226
|
+
// still flow into the next round as unresolved feedback. Prefer review;
|
|
227
|
+
// if absent, fall through to the design review (a design rejection also
|
|
228
|
+
// carries actionable findings the next plan round must consume).
|
|
229
|
+
const typedFact = (await readTypedReviewRequestFact(input.runDir)) ??
|
|
230
|
+
(await readTypedDesignRequestFact(input.runDir));
|
|
210
231
|
if (typedFact) {
|
|
211
232
|
const parentSourceBindingHash = sourceBindingHash(input.spec);
|
|
212
233
|
return withDigest({
|
|
@@ -216,7 +237,9 @@ export async function deriveDagRerunFeedback(input) {
|
|
|
216
237
|
taskId: input.taskId,
|
|
217
238
|
sourceNodeId: typedFact.nodeId,
|
|
218
239
|
verdict: "request-revision",
|
|
219
|
-
failureCategory:
|
|
240
|
+
failureCategory: typedFact.nodeId === FRONTEND_REVIEW_NODE_ID
|
|
241
|
+
? "review-request-changes"
|
|
242
|
+
: "design-request-changes",
|
|
220
243
|
feedbackText: scrubAndBoundFeedbackText(formatTypedReviewRequestFeedback(typedFact.fact), input.runDir, input.state.cwd),
|
|
221
244
|
evidenceRefs: [
|
|
222
245
|
{
|
|
@@ -536,14 +536,16 @@ function reasonMandatedFrontendPlanRetryStep(reason) {
|
|
|
536
536
|
}
|
|
537
537
|
}
|
|
538
538
|
/**
|
|
539
|
-
* Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
539
|
+
* Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=3
|
|
540
|
+
* bounds plan retry cost: the four-rung ladder is only partially traversable
|
|
541
|
+
* before the run fails non-converging (rungs: normal → bounded-tool-only →
|
|
542
|
+
* compact-terminal-first → backup-model). `invalid-output` (typed-fact schema
|
|
543
|
+
* violations, e.g. verification targets referencing undeclared UI states)
|
|
544
|
+
* retries at the `normal` rung with the contractCheck reason injected via
|
|
545
|
+
* `<retry_instruction>` (§5.1 ladder).
|
|
544
546
|
*/
|
|
545
547
|
export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
|
|
546
|
-
maxAttempts:
|
|
548
|
+
maxAttempts: 3,
|
|
547
549
|
backoff: "exponential",
|
|
548
550
|
initialDelayMs: 2000,
|
|
549
551
|
maxDelayMs: 30000,
|
|
@@ -26,7 +26,7 @@ import { runConvergencePassController, } from "./convergence/controller.js";
|
|
|
26
26
|
import { executeDagRanksOnce, FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isConditionSkippedReason, readFrontendPrewriteResult, } from "./scheduler.js";
|
|
27
27
|
import { topoSortToRanks } from "./topo.js";
|
|
28
28
|
import { isFrontendWriterTransientPartialWrite } from "./frontend-writer-recovery.js";
|
|
29
|
-
import { classifyReviewIssueCategoryFailureOwner, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd, } from "./frontend-review-findings.js";
|
|
29
|
+
import { classifyReviewIssueCategoryFailureOwner, readCommittedDesignRequestFact, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd, } from "./frontend-review-findings.js";
|
|
30
30
|
import { bindFrontendShapeStateToRunTopology, commitFrontendShapeTransitionCas, commitRuntimeFrontendShapeFact, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
|
|
31
31
|
import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFrontendShapeSourceDigest, } from "./frontend-shape.js";
|
|
32
32
|
import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
|
|
@@ -794,7 +794,7 @@ async function executeDagCheckpoint(input) {
|
|
|
794
794
|
runDir = await moveToPausedRunDir(runDir, pausedRunDir);
|
|
795
795
|
}
|
|
796
796
|
else {
|
|
797
|
-
const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
|
|
797
|
+
const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd, spec.frontendRecovery?.maxContinuations ?? 1);
|
|
798
798
|
await finalizeRunOwnedInterrupt({
|
|
799
799
|
runDir,
|
|
800
800
|
state,
|
|
@@ -1042,7 +1042,7 @@ function buildRecoveryResultForTrigger(state, trigger, outcome) {
|
|
|
1042
1042
|
evidenceRefs: [],
|
|
1043
1043
|
};
|
|
1044
1044
|
}
|
|
1045
|
-
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
1045
|
+
export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, frontendRecoveryQuota = 1) {
|
|
1046
1046
|
const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
|
|
1047
1047
|
if (state.frontendShapeState?.terminalOutcome &&
|
|
1048
1048
|
state.frontendShapeState.transitionCapsule) {
|
|
@@ -1064,16 +1064,53 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1064
1064
|
// every other blocked admission is terminal.
|
|
1065
1065
|
const planContractInvalid = admission.result.classification === "blocked" &&
|
|
1066
1066
|
admission.result.failureSource === "frontend-plan-pi";
|
|
1067
|
+
// A blocked admission caused by the design review committing
|
|
1068
|
+
// `request_design_changes` is a definite finding (not a protocol or
|
|
1069
|
+
// environment error): the design was reviewed and rejected. It is the
|
|
1070
|
+
// design-review analog of review-request-changes — not terminal, but
|
|
1071
|
+
// re-enters the next plan round with the committed design findings.
|
|
1072
|
+
// The failureSource may be frontend-writer-admission-shell or
|
|
1073
|
+
// frontend-design-policy-shell; the committed design fact is the
|
|
1074
|
+
// authoritative signal.
|
|
1075
|
+
const designRequestFact = await readCommittedDesignRequestFact({
|
|
1076
|
+
runDir,
|
|
1077
|
+
});
|
|
1078
|
+
const designRejected = admission.result.classification === "blocked" &&
|
|
1079
|
+
designRequestFact !== undefined;
|
|
1067
1080
|
const terminalAdmissionDenial = (admission.result.classification === "blocked" ||
|
|
1068
1081
|
admission.result.classification === "stale") &&
|
|
1069
|
-
!planContractInvalid
|
|
1082
|
+
!planContractInvalid &&
|
|
1083
|
+
!designRejected;
|
|
1070
1084
|
if (terminalAdmissionDenial) {
|
|
1071
1085
|
state.status = "failed";
|
|
1072
1086
|
return { recoveryPending: false };
|
|
1073
1087
|
}
|
|
1088
|
+
if (designRejected && designRequestFact) {
|
|
1089
|
+
state.terminalReason = "design-request-changes";
|
|
1090
|
+
try {
|
|
1091
|
+
await writeFrontendReviewFindingsPrd({
|
|
1092
|
+
runDir,
|
|
1093
|
+
parentRunId: state.runId,
|
|
1094
|
+
fact: designRequestFact,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
catch (error) {
|
|
1098
|
+
console.warn(`[run-dag] warning: failed to write frontend design findings PRD: ${error instanceof Error ? error.message : String(error)}`);
|
|
1099
|
+
}
|
|
1100
|
+
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1101
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1102
|
+
recoveryTrigger = {
|
|
1103
|
+
requestId: state.runId,
|
|
1104
|
+
failureSource: "frontend-design-review-pi",
|
|
1105
|
+
failureOwner: classifyReviewIssueCategoryFailureOwner(designRequestFact.issueCategory),
|
|
1106
|
+
protocolFailureReason: `frontend design review request_design_changes (issueCategory=${designRequestFact.issueCategory})`,
|
|
1107
|
+
recoveryMode: "new-dag",
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1074
1111
|
if (planContractInvalid) {
|
|
1075
1112
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1076
|
-
if (continuationCount >=
|
|
1113
|
+
if (continuationCount >= frontendRecoveryQuota) {
|
|
1077
1114
|
// Continuation quota exhausted: a second contract-invalid candidate
|
|
1078
1115
|
// is authoritative candidate-contract-invalid → forced failed.
|
|
1079
1116
|
state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
|
|
@@ -1109,7 +1146,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1109
1146
|
});
|
|
1110
1147
|
if (writerNodeId) {
|
|
1111
1148
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1112
|
-
if (continuationCount <
|
|
1149
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1113
1150
|
const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
|
|
1114
1151
|
const rollback = await rollbackFrontendWriter({
|
|
1115
1152
|
cwd: repoRoot,
|
|
@@ -1141,7 +1178,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1141
1178
|
const verifyFailed = state.nodes["frontend-verify-shell"]?.status === "ERROR";
|
|
1142
1179
|
if (verifyFailed) {
|
|
1143
1180
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1144
|
-
if (continuationCount <
|
|
1181
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1145
1182
|
recoveryTrigger = {
|
|
1146
1183
|
requestId: state.runId,
|
|
1147
1184
|
failureSource: "frontend-verify-shell",
|
|
@@ -1179,7 +1216,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
|
|
|
1179
1216
|
}
|
|
1180
1217
|
if (!recoveryTrigger && !skipFrontendRecovery) {
|
|
1181
1218
|
const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
|
|
1182
|
-
if (continuationCount <
|
|
1219
|
+
if (continuationCount < frontendRecoveryQuota) {
|
|
1183
1220
|
recoveryTrigger = {
|
|
1184
1221
|
requestId: state.runId,
|
|
1185
1222
|
failureSource: "frontend-review-pi",
|
|
@@ -1147,6 +1147,22 @@ export const dagSpecSchema = z
|
|
|
1147
1147
|
taskContractBinding: dagTaskContractBindingSchema.optional(),
|
|
1148
1148
|
/** Optional unresolved parent-run feedback for a full task rerun. */
|
|
1149
1149
|
rerunFeedback: dagRerunFeedbackSchema.optional(),
|
|
1150
|
+
/** Frontend recovery continuation quota (frozen at generation time from
|
|
1151
|
+
* task.json frontendRecovery.maxContinuations). Bounds how many times a
|
|
1152
|
+
* failed frontend run may auto-recover via M6 recovery operations before
|
|
1153
|
+
* the run stays failed and needs human intervention. */
|
|
1154
|
+
frontendRecovery: z
|
|
1155
|
+
.object({
|
|
1156
|
+
maxContinuations: z
|
|
1157
|
+
.number()
|
|
1158
|
+
.int()
|
|
1159
|
+
.min(1)
|
|
1160
|
+
.max(5)
|
|
1161
|
+
.optional()
|
|
1162
|
+
.default(1),
|
|
1163
|
+
})
|
|
1164
|
+
.strict()
|
|
1165
|
+
.optional(),
|
|
1150
1166
|
/** Frozen backend-test artifact layout (plan A); absent = historical default testcase/ layout. */
|
|
1151
1167
|
backendTestLayout: z
|
|
1152
1168
|
.object({
|