@tea-agent/loop-agent 0.24.0 → 0.24.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/commands/init.js +2 -2
  3. package/dist/task/dag-source-paths.js +50 -0
  4. package/dist/task/frontend-project-capability.js +316 -0
  5. package/dist/task/task-demand-routing.js +432 -0
  6. package/dist/worker/console/chat/compaction-errors.js +64 -0
  7. package/dist/worker/console/chat/interview-adapter.js +17 -4
  8. package/dist/worker/console/chat/routes.js +6 -1
  9. package/dist/worker/console/interview/grill-me.js +28 -5
  10. package/dist/worker/console/static/assets/index-DePjoS_G.js +253 -0
  11. package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
  12. package/dist/worker/console/static/index.html +2 -2
  13. package/dist/worker/observe/routes.js +8 -4
  14. package/dist/workflows/dag/backend-test-intake-context.js +10 -12
  15. package/dist/workflows/dag/backend-test-markdown-workflow.js +223 -42
  16. package/dist/workflows/dag/backend-test-result-contract.js +6 -2
  17. package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
  18. package/dist/workflows/dag/frontend-project-capability.js +1 -316
  19. package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
  20. package/dist/workflows/dag/init-hybrid.js +44 -42
  21. package/dist/workflows/dag/rerun-plan.js +6 -10
  22. package/dist/workflows/dag/task-demand-routing.js +1 -383
  23. package/docs/templates/frontend-implementation-contract.schema.json +1 -1
  24. package/package.json +1 -1
  25. package/skills/frontend-implementation/SKILL.md +7 -0
  26. package/skills/frontend-implementation/references/node-contracts.md +1 -1
  27. package/skills/frontend-verification/SKILL.md +4 -3
  28. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  29. package/dist/worker/console/static/assets/index-BTbrEHnO.css +0 -1
  30. package/dist/worker/console/static/assets/index-D9qLevoP.js +0 -27
@@ -0,0 +1,432 @@
1
+ const OUT_OF_SCOPE_HEADING = /^(?:非目标|不包含|范围外|不在范围|out\s+of\s+scope|non[-\s]?goals?)(?:\s*(?:[((].*[))]|[::].*|[/—–-]\s*.*))?$/i;
2
+ const FRONTEND_TASK_TYPE = /^\s*(?:[-*]\s*)?task\s+type\s*:\s*frontend-feature\s*$/im;
3
+ const BACKEND_TASK_TYPE = /^\s*(?:[-*]\s*)?task\s+type\s*:\s*backend-feature\s*$/im;
4
+ const FRONTEND_NEGATION_PATTERNS = [
5
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?:前端(?:页面|组件|界面|功能|交互)?|页面|组件|界面|用户界面|浏览器交互)/,
6
+ /(?:前端(?:页面|组件|界面|功能|交互)?|页面|组件|界面|用户界面).{0,12}(?:不在(?:本次)?范围|无需|不需要|不修改|不实现|不交付|不要|禁止|不得)/,
7
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?<![A-Za-z0-9_])UI(?![A-Za-z0-9_])/i,
8
+ /(?<![A-Za-z0-9_])UI(?![A-Za-z0-9_]).{0,12}(?:不在(?:本次)?范围|无需|不需要|不修改|不实现|不交付|不要|禁止|不得)/i,
9
+ /\b(?:backend|server[-\s]?side)\s+only\b/i,
10
+ /\b(?:no|without)\s+(?:frontend|front[-\s]?end|ui|user\s+interface)\b/i,
11
+ /\b(?:do\s+not|don't|must\s+not|should\s+not)\s+(?:(?:add|implement|build|create|update|modify|fix|refactor|render|style|change|touch)\s+)?(?:(?:the|any|a|an)\s+)?(?:frontend|front[-\s]?end|ui|user\s+interface|page|component|form|modal|dialog)\b/i,
12
+ /\bwithout\s+(?:(?:adding|implementing|building|creating|updating|modifying|fixing|refactoring|rendering|styling|changing|touching)\s+)?(?:(?:the|any|a|an)\s+)?(?:frontend|front[-\s]?end|ui|user\s+interface|page|component|form|modal|dialog)\b/i,
13
+ /\b(?:frontend|front[-\s]?end|ui|user\s+interface|page|component|form|modal|dialog)\b[^.?!;\n]{0,20}\b(?:is|are)?\s*(?:out\s+of\s+scope|excluded|not\s+(?:required|needed)|unchanged)\b/i,
14
+ ];
15
+ const FRONTEND_DELIVERY_PATTERNS = [
16
+ /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?:前端(?:页面|组件|界面|功能|交互)|页面|组件|界面|表单|弹窗|对话框|用户界面|浏览器(?:页面|交互|行为))/,
17
+ /(?:前端(?:页面|组件|界面|功能|交互)|页面|组件|界面|表单|弹窗|对话框|用户界面|浏览器交互)[^。!?;,,\n]{0,16}(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)/,
18
+ /\b(?:add|implement|build|create|update|modify|fix|refactor|render|style)\b[^.?!;\n]{0,48}\b(?:front[-\s]?end\s+(?:page|component|view|form|modal|interaction|ui)|ui|form|modal|dialog|dashboard|navigation|toast|button|dropdown|user\s+interface|browser\s+interaction)\b/i,
19
+ ];
20
+ const PATH_SUPPORTED_FRONTEND_DELIVERY_PATTERNS = [
21
+ /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?:页(?!\p{Script=Han})|表格|列表|按钮|导航|菜单|标签页|下拉框|选择器|提示|通知|加载状态|空状态|错误状态|交互|样式|布局)/u,
22
+ /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?<![A-Za-z0-9_])(?:DOM|UI|React|Vue|Svelte|Angular)(?![A-Za-z0-9_])/i,
23
+ /\b(?:add|implement|build|create|update|modify|fix|refactor|render|style)\b[^.?!;\n]{0,48}\b(?:page|component|screen|layout|dom|react|vue|svelte|angular)\b/i,
24
+ ];
25
+ /**
26
+ * User-visible product behavior that is commonly described without naming the
27
+ * frontend technology or artifact. These signals are never sufficient alone:
28
+ * they must be backed by a frontend source path or strong project framework
29
+ * evidence.
30
+ */
31
+ const CONTEXT_SUPPORTED_FRONTEND_DELIVERY_PATTERNS = [
32
+ /(?:用户|运营人员|管理员|访客|客户)[^。!?;,,\n]{0,24}(?:列表|筛选|搜索|表单|按钮|弹窗|提示|通知|状态|工作台|导航|菜单|导出|下载)/,
33
+ /(?:点击|输入|选择|提交|筛选|搜索|翻页|跳转|关闭|打开|导出|下载)[^。!?;,,\n]{0,24}(?:显示|展示|提示|状态|结果|页面|工作台|错误|成功|失败|加载|下载)/,
34
+ /(?:字段错误|下载状态|加载中|加载失败|空状态|错误状态|成功状态|结果数量|表单校验|用户可见)/,
35
+ /\b(?:user|operator|admin|visitor|customer)\b[^.?!;\n]{0,48}\b(?:list|filter|search|form|button|modal|toast|notification|state|dashboard|navigation|menu|export|download)\b/i,
36
+ /\b(?:click|type|select|submit|filter|search|paginate|navigate|close|open|export|download)\b[^.?!;\n]{0,48}\b(?:show|display|feedback|state|result|page|dashboard|error|success|failure|loading|download)\b/i,
37
+ /\b(?:field error|download state|loading state|empty state|error state|success state|result count|form validation|user-visible)\b/i,
38
+ ];
39
+ const BACKEND_DELIVERY_PATTERNS = [
40
+ /(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)[^。!?;,,\n]{0,24}(?:后端(?:服务|接口|逻辑|API)?|服务端(?:服务|接口|逻辑)?|接口|数据库|数据表(?!格)|数据库索引|索引|控制器|服务层|仓储层)/i,
41
+ /(?:后端(?:服务|接口|逻辑|API)?|服务端(?:服务|接口|逻辑)?|接口|数据库|数据表(?!格)|数据库索引|控制器|服务层|仓储层)[^。!?;,,\n]{0,16}(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)/i,
42
+ /(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)[^。!?;,,\n]{0,24}(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
43
+ /(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])[^。!?;,,\n]{0,16}(?:新增|实现|开发|修改|重构|修复|提供|创建|调整|优化|迁移|扩展)/i,
44
+ /\b(?:add|implement|build|create|update|modify|fix|refactor|migrate|extend)\b[^.?!;\n]{0,48}\b(?:backend|server[-\s]?side|api|endpoint|database|migration|controller|service|repository)\b/i,
45
+ ];
46
+ const BACKEND_NEGATION_PATTERNS = [
47
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?:后端|服务端|接口|数据库|数据表(?!格)|索引)/,
48
+ /(?:不涉及|无需|不需要|不包含|不修改|不实现|不交付|不要|禁止|不得|排除).{0,16}(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
49
+ /\b(?:no|without)\s+(?:backend|server[-\s]?side|api|endpoint|database)\b/i,
50
+ /\b(?:do\s+not|don't|must\s+not|should\s+not)\s+(?:(?:add|implement|build|create|update|modify|fix|refactor|migrate|change|touch)\s+)?(?:(?:the|any|a|an)\s+)?(?:backend|server[-\s]?side|api|endpoint|database)\b/i,
51
+ ];
52
+ const FRONTEND_PATH_PATTERNS = [
53
+ /(?:^|\/)apps\/web\/src(?:\/|$)/i,
54
+ /(?:^|\/)(?:frontend|client)(?:\/|$)/i,
55
+ /(?:^|\/)src\/(?:pages?|components?|views?)(?:\/|$)/i,
56
+ /\.(?:tsx|jsx|vue|svelte|css|scss|less)(?:$|[*?])/i,
57
+ ];
58
+ const BACKEND_PATH_PATTERNS = [
59
+ /(?:^|\/)(?:backend|server|api|controllers?|services?|repositories?|db|database|migrations?)(?:\/|$)/i,
60
+ /\.(?:sql)(?:$|[*?])/i,
61
+ ];
62
+ const NON_PRODUCT_PATH_SEGMENT = /(?:^|\/)(?:docs?|skills?|tests?|__tests__|e2e|cypress)(?:\/|$)/i;
63
+ const FRONTEND_PATH_EXCLUSIONS = NON_PRODUCT_PATH_SEGMENT;
64
+ const DOCUMENTATION_ONLY_MARKERS = /(?:文档|说明|指南|规范|测试用例|测试计划|评审|审查|\b(?:docs?|documentation|guide|specification|test\s+cases?|test\s+plan|review)\b)/i;
65
+ const DOCUMENTATION_ONLY_ACTIONS = /(?:新增|实现|开发|创建|编写|更新|修改|重构|修复|优化|渲染|搭建|改造|补充|生成|评审|审查|\b(?:add|implement|build|create|write|update|modify|refactor|fix|optimize|render|style|document|review|generate)\b)/i;
66
+ const PRODUCT_AND_DOCUMENT_DELIVERY = /(?:新增|实现|开发|修改|重构|修复|优化|创建|渲染|搭建|改造)[^。!?;,,\n]{0,24}(?:前端|页面|组件|界面|表单|交互|样式|布局|DOM|UI|React|Vue|Svelte|Angular|源码|代码)[^。!?;,,\n]{0,16}(?:并|同时|以及|和|与|\band\b)[^。!?;,,\n]{0,16}(?:文档|说明|指南|规范|\b(?:docs?|documentation|guide)\b)/i;
67
+ const TEST_ONLY_MARKERS = /(?:测试|用例|\b(?:e2e|unit|integration|component)\s+tests?\b|\btests?\b)/i;
68
+ const PRODUCT_AND_TEST_DELIVERY = /(?:并|同时|以及|和|与|\band\b).{0,12}(?:添加|新增|编写|补充|测试|\b(?:add|write|test)\b)/i;
69
+ const NON_PRODUCT_ALLOWED_PATH_PATTERNS = [
70
+ NON_PRODUCT_PATH_SEGMENT,
71
+ /\.(?:test|spec)\.[^/]+$/i,
72
+ /\.cy\.[^/]+$/i,
73
+ /\.mdx?(?:$|[*?])/i,
74
+ ];
75
+ const SPECIAL_TEMPLATE_BY_TASK_KIND = {
76
+ "frontend-implementation": "frontend-implementation",
77
+ "frontend-test": "frontend-test-dag",
78
+ "backend-test": "backend-test-dag",
79
+ "knowledge-sync": "knowledge-sync-dag",
80
+ "knowledge-graph-bootstrap": "knowledge-graph-bootstrap-dag",
81
+ };
82
+ const TASK_KIND_ROUTING_REASON = {
83
+ "frontend-implementation": 'taskKind "frontend-implementation" selects the dedicated frontend DAG template',
84
+ "frontend-test": 'taskKind "frontend-test" selects the dedicated frontend test DAG template',
85
+ "backend-test": 'taskKind "backend-test" selects the dedicated backend test DAG template',
86
+ "knowledge-sync": 'taskKind "knowledge-sync" selects the dedicated knowledge-sync DAG template',
87
+ "knowledge-graph-bootstrap": 'taskKind "knowledge-graph-bootstrap" selects the dedicated knowledge-graph bootstrap DAG template',
88
+ };
89
+ const SPECIAL_REQUESTED_TEMPLATES = new Set([
90
+ "frontend-implementation",
91
+ "frontend-test-dag",
92
+ "backend-test-dag",
93
+ "knowledge-sync-dag",
94
+ "knowledge-graph-bootstrap-dag",
95
+ ]);
96
+ function stripOutOfScopeSections(markdown) {
97
+ const output = [];
98
+ let skippedHeadingLevel = null;
99
+ for (const line of markdown.split(/\r?\n/)) {
100
+ const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line.trim());
101
+ if (heading) {
102
+ const level = heading[1].length;
103
+ if (skippedHeadingLevel !== null && level <= skippedHeadingLevel) {
104
+ skippedHeadingLevel = null;
105
+ }
106
+ if (OUT_OF_SCOPE_HEADING.test(heading[2].trim())) {
107
+ skippedHeadingLevel = level;
108
+ continue;
109
+ }
110
+ }
111
+ if (skippedHeadingLevel === null)
112
+ output.push(line);
113
+ }
114
+ return output.join("\n");
115
+ }
116
+ function stripFencedCodeBlocks(markdown) {
117
+ const output = [];
118
+ let fenceCharacter = null;
119
+ for (const line of markdown.split(/\r?\n/)) {
120
+ const fence = /^\s*(`{3,}|~{3,})/.exec(line);
121
+ if (fence) {
122
+ const character = fence[1][0];
123
+ if (fenceCharacter === null)
124
+ fenceCharacter = character;
125
+ else if (fenceCharacter === character)
126
+ fenceCharacter = null;
127
+ continue;
128
+ }
129
+ if (fenceCharacter === null)
130
+ output.push(line);
131
+ }
132
+ return output.join("\n");
133
+ }
134
+ /**
135
+ * Normalize task prose before deterministic demand/capability signal checks.
136
+ * Examples and explicit non-goals must not become positive routing evidence.
137
+ */
138
+ export function normalizeTaskRequirementText(markdown) {
139
+ return stripOutOfScopeSections(stripFencedCodeBlocks(markdown));
140
+ }
141
+ function splitClauses(text) {
142
+ return text
143
+ .split(/[。!?!?;;,,\r\n]+/)
144
+ .map((clause) => clause.trim())
145
+ .filter(Boolean);
146
+ }
147
+ function matchesAny(value, patterns) {
148
+ return patterns.some((pattern) => pattern.test(value));
149
+ }
150
+ function isFrontendNegated(clause) {
151
+ return matchesAny(clause, FRONTEND_NEGATION_PATTERNS);
152
+ }
153
+ function isDocumentationOnlyClause(clause) {
154
+ if (!DOCUMENTATION_ONLY_ACTIONS.test(clause))
155
+ return false;
156
+ if (DOCUMENTATION_ONLY_MARKERS.test(clause)) {
157
+ return !PRODUCT_AND_DOCUMENT_DELIVERY.test(clause);
158
+ }
159
+ return TEST_ONLY_MARKERS.test(clause) && !PRODUCT_AND_TEST_DELIVERY.test(clause);
160
+ }
161
+ function masksExistingBackendDependency(clause) {
162
+ return clause
163
+ .replace(/(?:调用|使用|消费|对接|依赖)(?:现有|已有)?[^。!?;,,\n]{0,12}(?:后端|服务端|API|接口)/gi, " ")
164
+ .replace(/\b(?:call|consume|use|integrate\s+with|depend\s+on)\b[^.?!;\n]{0,24}\b(?:existing|current)?\s*(?:backend|server[-\s]?side|api|endpoint)\b/gi, " ");
165
+ }
166
+ function addSignal(signals, signal) {
167
+ if (signals.some((current) => current.id === signal.id))
168
+ return;
169
+ signals.push(signal);
170
+ }
171
+ function collectTextSignals(input) {
172
+ let frontendDelivery = false;
173
+ let pathSupportedFrontendDelivery = false;
174
+ let contextSupportedFrontendDelivery = false;
175
+ let backendDelivery = false;
176
+ let frontendNegated = false;
177
+ for (const clause of splitClauses(input.text)) {
178
+ const negated = isFrontendNegated(clause);
179
+ frontendNegated ||= negated;
180
+ const documentationOnly = isDocumentationOnlyClause(clause);
181
+ if (!negated &&
182
+ !documentationOnly &&
183
+ matchesAny(clause, FRONTEND_DELIVERY_PATTERNS)) {
184
+ frontendDelivery = true;
185
+ addSignal(input.signals, {
186
+ id: `frontend-delivery-${input.source}`,
187
+ source: input.source,
188
+ message: `${input.source} contains an implementation action and a frontend UI deliverable`,
189
+ });
190
+ }
191
+ if (!negated &&
192
+ !documentationOnly &&
193
+ matchesAny(clause, PATH_SUPPORTED_FRONTEND_DELIVERY_PATTERNS)) {
194
+ pathSupportedFrontendDelivery = true;
195
+ }
196
+ if (!negated &&
197
+ !documentationOnly &&
198
+ matchesAny(clause, CONTEXT_SUPPORTED_FRONTEND_DELIVERY_PATTERNS)) {
199
+ contextSupportedFrontendDelivery = true;
200
+ }
201
+ const backendClause = masksExistingBackendDependency(clause);
202
+ if (!matchesAny(clause, BACKEND_NEGATION_PATTERNS) &&
203
+ matchesAny(backendClause, BACKEND_DELIVERY_PATTERNS)) {
204
+ backendDelivery = true;
205
+ addSignal(input.signals, {
206
+ id: `backend-delivery-${input.source}`,
207
+ source: input.source,
208
+ message: `${input.source} contains an implementation action and a backend deliverable`,
209
+ });
210
+ }
211
+ }
212
+ return {
213
+ frontendDelivery,
214
+ pathSupportedFrontendDelivery,
215
+ contextSupportedFrontendDelivery,
216
+ backendDelivery,
217
+ frontendNegated,
218
+ };
219
+ }
220
+ function normalizedAllowedPath(value) {
221
+ return value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
222
+ }
223
+ function reasonsForClassification(input) {
224
+ const signalIds = input.signals.map((signal) => signal.id);
225
+ const reasons = [`task demand classified as ${input.kind}`];
226
+ if (signalIds.length > 0)
227
+ reasons.push(`signals: ${signalIds.join(", ")}`);
228
+ if (input.blockers.length > 0)
229
+ reasons.push(`blockers: ${input.blockers.join(", ")}`);
230
+ return reasons;
231
+ }
232
+ export function classifyTaskDemand(input) {
233
+ const signals = [];
234
+ const blockers = [];
235
+ const requirement = normalizeTaskRequirementText(input.requirementMarkdown);
236
+ const hasFrontendTaskType = FRONTEND_TASK_TYPE.test(requirement);
237
+ const hasBackendTaskType = BACKEND_TASK_TYPE.test(requirement);
238
+ if (hasFrontendTaskType) {
239
+ addSignal(signals, {
240
+ id: "task-type-frontend-feature",
241
+ source: "requirement",
242
+ message: "requirement contains the materialized frontend-feature task type",
243
+ });
244
+ }
245
+ if (hasBackendTaskType) {
246
+ addSignal(signals, {
247
+ id: "task-type-backend-feature",
248
+ source: "requirement",
249
+ message: "requirement contains the materialized backend-feature task type",
250
+ });
251
+ }
252
+ const titleSignals = collectTextSignals({
253
+ text: input.title,
254
+ source: "title",
255
+ signals,
256
+ });
257
+ const requirementSignals = collectTextSignals({
258
+ text: requirement,
259
+ source: "requirement",
260
+ signals,
261
+ });
262
+ const frontendNegated = titleSignals.frontendNegated || requirementSignals.frontendNegated;
263
+ if (frontendNegated)
264
+ blockers.push("frontend-negated");
265
+ const frontendFramework = input.frontendProject?.framework ?? "unknown";
266
+ const hasStrongFrontendProjectEvidence = frontendFramework === "react" ||
267
+ frontendFramework === "next" ||
268
+ frontendFramework === "vue";
269
+ if (hasStrongFrontendProjectEvidence) {
270
+ addSignal(signals, {
271
+ id: `frontend-project-${frontendFramework}`,
272
+ source: "project",
273
+ message: `project has strong ${frontendFramework} framework evidence`,
274
+ });
275
+ }
276
+ let frontendPath = false;
277
+ let backendPath = false;
278
+ for (const rawPath of input.allowedPaths) {
279
+ const allowedPath = normalizedAllowedPath(rawPath);
280
+ if (!FRONTEND_PATH_EXCLUSIONS.test(allowedPath) &&
281
+ matchesAny(allowedPath, FRONTEND_PATH_PATTERNS)) {
282
+ frontendPath = true;
283
+ }
284
+ if (matchesAny(allowedPath, BACKEND_PATH_PATTERNS))
285
+ backendPath = true;
286
+ }
287
+ if (frontendPath) {
288
+ addSignal(signals, {
289
+ id: "frontend-path",
290
+ source: "allowed-path",
291
+ message: "allowedPaths include a frontend-specific source path",
292
+ });
293
+ }
294
+ if (backendPath) {
295
+ addSignal(signals, {
296
+ id: "backend-path",
297
+ source: "allowed-path",
298
+ message: "allowedPaths include a backend-specific source path",
299
+ });
300
+ }
301
+ const pathSupportedFrontendDelivery = frontendPath &&
302
+ (titleSignals.pathSupportedFrontendDelivery ||
303
+ requirementSignals.pathSupportedFrontendDelivery ||
304
+ titleSignals.contextSupportedFrontendDelivery ||
305
+ requirementSignals.contextSupportedFrontendDelivery);
306
+ if (pathSupportedFrontendDelivery) {
307
+ const supportedByRequirement = requirementSignals.pathSupportedFrontendDelivery ||
308
+ requirementSignals.contextSupportedFrontendDelivery;
309
+ addSignal(signals, {
310
+ id: supportedByRequirement
311
+ ? "frontend-path-supported-delivery-requirement"
312
+ : "frontend-path-supported-delivery-title",
313
+ source: supportedByRequirement ? "requirement" : "title",
314
+ message: "task text contains a frontend UI artifact and allowedPaths provide frontend source-path evidence",
315
+ });
316
+ }
317
+ const projectSupportedFrontendDelivery = hasStrongFrontendProjectEvidence &&
318
+ (titleSignals.contextSupportedFrontendDelivery ||
319
+ requirementSignals.contextSupportedFrontendDelivery);
320
+ if (projectSupportedFrontendDelivery) {
321
+ addSignal(signals, {
322
+ id: requirementSignals.contextSupportedFrontendDelivery
323
+ ? "frontend-project-supported-delivery-requirement"
324
+ : "frontend-project-supported-delivery-title",
325
+ source: requirementSignals.contextSupportedFrontendDelivery
326
+ ? "requirement"
327
+ : "title",
328
+ message: "task text contains user-visible interaction/state delivery and the project has strong frontend framework evidence",
329
+ });
330
+ }
331
+ const frontendDelivery = titleSignals.frontendDelivery ||
332
+ requirementSignals.frontendDelivery ||
333
+ pathSupportedFrontendDelivery ||
334
+ projectSupportedFrontendDelivery;
335
+ const allowedPathsOnlyCoverNonProductArtifacts = input.allowedPaths.length > 0 &&
336
+ input.allowedPaths.every((rawPath) => {
337
+ const allowedPath = normalizedAllowedPath(rawPath);
338
+ return matchesAny(allowedPath, NON_PRODUCT_ALLOWED_PATH_PATTERNS);
339
+ });
340
+ if (allowedPathsOnlyCoverNonProductArtifacts && frontendDelivery) {
341
+ blockers.push("non-product-allowed-paths");
342
+ }
343
+ const eligibleFrontendDelivery = frontendDelivery && !allowedPathsOnlyCoverNonProductArtifacts;
344
+ const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
345
+ const taskTypeConflict = (hasFrontendTaskType && (backendDelivery || frontendNegated)) ||
346
+ (hasBackendTaskType && eligibleFrontendDelivery);
347
+ if (taskTypeConflict)
348
+ blockers.push("task-type-conflict");
349
+ let kind;
350
+ if ((hasFrontendTaskType && hasBackendTaskType) || taskTypeConflict) {
351
+ kind = "mixed";
352
+ }
353
+ else if (hasFrontendTaskType) {
354
+ kind = "frontend";
355
+ }
356
+ else if (hasBackendTaskType) {
357
+ kind = "backend";
358
+ }
359
+ else if (eligibleFrontendDelivery && backendDelivery) {
360
+ kind = "mixed";
361
+ }
362
+ else if (eligibleFrontendDelivery && frontendNegated) {
363
+ kind = "mixed";
364
+ }
365
+ else if (eligibleFrontendDelivery) {
366
+ kind = "frontend";
367
+ }
368
+ else if (backendDelivery) {
369
+ kind = "backend";
370
+ }
371
+ else {
372
+ kind = "unknown";
373
+ }
374
+ return {
375
+ kind,
376
+ signals,
377
+ blockers,
378
+ reasons: reasonsForClassification({ kind, signals, blockers }),
379
+ };
380
+ }
381
+ export function resolveTaskDagTemplateSelection(input) {
382
+ const requestedTemplate = input.requestedTemplate ?? "standard-dag";
383
+ const taskKindTemplate = SPECIAL_TEMPLATE_BY_TASK_KIND[input.taskKind];
384
+ if (taskKindTemplate) {
385
+ return {
386
+ template: taskKindTemplate,
387
+ source: "taskKind",
388
+ reasons: [TASK_KIND_ROUTING_REASON[input.taskKind]],
389
+ };
390
+ }
391
+ if (SPECIAL_REQUESTED_TEMPLATES.has(requestedTemplate)) {
392
+ return {
393
+ template: requestedTemplate,
394
+ source: "requested-template",
395
+ reasons: [],
396
+ };
397
+ }
398
+ if (input.taskKind !== "standard") {
399
+ return {
400
+ template: requestedTemplate,
401
+ source: "requested-template",
402
+ reasons: [],
403
+ };
404
+ }
405
+ if (requestedTemplate !== "standard-dag") {
406
+ return {
407
+ template: requestedTemplate,
408
+ source: "requested-template",
409
+ reasons: [],
410
+ };
411
+ }
412
+ const classification = classifyTaskDemand(input);
413
+ if (classification.kind === "frontend") {
414
+ const frontendSignals = classification.signals
415
+ .map((signal) => signal.id)
416
+ .filter((id) => id === "task-type-frontend-feature" || id.startsWith("frontend-"));
417
+ return {
418
+ template: "frontend-implementation",
419
+ source: "task-source",
420
+ reasons: [
421
+ `task source classified as frontend from signals: ${frontendSignals.join(", ")}`,
422
+ ],
423
+ classification,
424
+ };
425
+ }
426
+ return {
427
+ template: requestedTemplate,
428
+ source: "requested-template",
429
+ reasons: [],
430
+ classification,
431
+ };
432
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Map Pi SDK / runtime compact failures to Operator Console HTTP errors.
3
+ * Keeps user-facing copy Chinese and actionable; raw SDK strings stay in `raw`.
4
+ */
5
+ export function classifyChatCompactionFailure(error) {
6
+ const raw = error instanceof Error ? error.message : String(error);
7
+ if (raw === "CHAT_TURN_ACTIVE" || /cannot compact an active turn/i.test(raw)) {
8
+ return {
9
+ status: 409,
10
+ code: "TURN_ACTIVE",
11
+ message: "对话进行中,请等待当前回合结束后再压缩。",
12
+ raw,
13
+ };
14
+ }
15
+ if (raw === "COMPACTION_UNSUPPORTED") {
16
+ return {
17
+ status: 501,
18
+ code: "COMPACTION_UNSUPPORTED",
19
+ message: "当前会话运行时不支持上下文压缩。",
20
+ raw,
21
+ };
22
+ }
23
+ if (/Nothing to compact|session too small/i.test(raw)) {
24
+ return {
25
+ status: 400,
26
+ code: "COMPACTION_TOO_SMALL",
27
+ message: "会话内容太少,暂无法压缩。多轮对话后再试。",
28
+ raw,
29
+ };
30
+ }
31
+ if (/Already compacted/i.test(raw)) {
32
+ return {
33
+ status: 400,
34
+ code: "COMPACTION_ALREADY_DONE",
35
+ message: "最近已压缩,暂无更多可压缩内容。",
36
+ raw,
37
+ };
38
+ }
39
+ if (/No model|model selected|未选择模型/i.test(raw)) {
40
+ return {
41
+ status: 400,
42
+ code: "COMPACTION_NO_MODEL",
43
+ message: "未选择可用模型,无法压缩上下文。",
44
+ raw,
45
+ };
46
+ }
47
+ if (/Compaction cancelled/i.test(raw)) {
48
+ return {
49
+ status: 409,
50
+ code: "COMPACTION_CANCELLED",
51
+ message: "上下文压缩已取消。",
52
+ raw,
53
+ };
54
+ }
55
+ if (/chat session not found/i.test(raw)) {
56
+ return { status: 404, code: "NOT_FOUND", message: raw, raw };
57
+ }
58
+ return {
59
+ status: 409,
60
+ code: "COMPACTION_FAILED",
61
+ message: raw.trim() || "上下文压缩失败",
62
+ raw,
63
+ };
64
+ }
@@ -3,6 +3,7 @@ import { applyGrillMeAnswer, emptyDraft, isDraftStructurallyComplete, listDraftG
3
3
  import { InterviewSessionStore } from "../interview/session.js";
4
4
  import { DraftStore } from "../draft-store.js";
5
5
  import { normalizeConsoleWorkflowKind } from "../workflow-kinds.js";
6
+ import { discoverFrontendProjectCapability } from "../../../task/frontend-project-capability.js";
6
7
  import { scrubSecrets } from "./explore-tools.js";
7
8
  function redact(value) {
8
9
  const raw = JSON.stringify(value);
@@ -38,11 +39,23 @@ export class ChatInterviewAdapter {
38
39
  const missingFields = listDraftGaps(draft.draft);
39
40
  return { outcome: missingFields.length === 0 ? "complete" : "questions-required", fresh, missingFields, ...(reason ? { reason } : {}), ...(assessmentId ? { assessmentId } : {}), draftSha256: draft.draftSha256 };
40
41
  }
42
+ async nextQuestion(draft) {
43
+ const question = nextGrillMeQuestion(draft);
44
+ if (question?.gapId !== "taskKind")
45
+ return question;
46
+ const capability = await discoverFrontendProjectCapability(this.appData.repoRoot);
47
+ return nextGrillMeQuestion(draft, {
48
+ frontendProject: {
49
+ framework: capability.framework,
50
+ evidencePaths: capability.evidencePaths,
51
+ },
52
+ });
53
+ }
41
54
  async start(input) {
42
55
  const interview = await this.interviews.create({ taskId: input.taskId, operatorSessionId: input.operatorSessionId });
43
56
  const base = emptyDraft(input.taskId, input.title);
44
57
  const draft = await this.drafts.save({ ...base, ...input.initialDraft, requirement: { ...base.requirement, ...input.initialDraft?.requirement }, constraints: { ...base.constraints, ...input.initialDraft?.constraints }, verification: { ...base.verification, ...input.initialDraft?.verification }, schemaVersion: 1, taskId: input.taskId });
45
- const question = nextGrillMeQuestion(draft.draft);
58
+ const question = await this.nextQuestion(draft.draft);
46
59
  const savedInterview = await this.interviews.setCurrentQuestion(interview.sessionId, question ? { id: question.id, text: question.text, affectsFields: question.affectsFields, risk: question.risk } : undefined, question ? "awaiting-user-answer" : "draft-ready");
47
60
  this.append(input.operatorSessionId, interview.sessionId, "interview-turn", { interviewSessionId: interview.sessionId, taskId: input.taskId, state: savedInterview.state, ...(question ? { question } : {}) });
48
61
  this.append(input.operatorSessionId, interview.sessionId, "draft", { taskId: input.taskId, draft: draft.draft, draftSha256: draft.draftSha256 });
@@ -59,7 +72,7 @@ export class ChatInterviewAdapter {
59
72
  const current = await this.drafts.get(interview.taskId);
60
73
  if (!current)
61
74
  throw new Error(`draft not found: ${interview.taskId}`);
62
- const question = nextGrillMeQuestion(current.draft);
75
+ const question = await this.nextQuestion(current.draft);
63
76
  if (!question || question.id !== input.questionId)
64
77
  throw new Error("draft gap does not match current question");
65
78
  const applied = applyGrillMeAnswer(current.draft, question, { response: input.response, text: input.text });
@@ -70,7 +83,7 @@ export class ChatInterviewAdapter {
70
83
  const draft = await this.drafts.save(applied);
71
84
  await this.interviews.recordAnswer(interview.sessionId, { questionId: input.questionId, response: input.response, ...(input.text ? { text: input.text } : {}), at: new Date().toISOString() });
72
85
  const projected = recommendation ? { ...draft.draft, taskKind: recommendation } : draft.draft;
73
- const nextQuestion = nextGrillMeQuestion(projected);
86
+ const nextQuestion = await this.nextQuestion(projected);
74
87
  const savedInterview = await this.interviews.setCurrentQuestion(interview.sessionId, nextQuestion ? { id: nextQuestion.id, text: nextQuestion.text, affectsFields: nextQuestion.affectsFields, risk: nextQuestion.risk } : undefined, nextQuestion ? "awaiting-user-answer" : "awaiting-confirmation");
75
88
  this.append(input.operatorSessionId, interview.sessionId, "interview-turn", { interviewSessionId: interview.sessionId, taskId: interview.taskId, state: savedInterview.state, answer: { questionId: input.questionId, response: input.response, ...(input.text ? { text: input.text } : {}) }, ...(nextQuestion ? { question: nextQuestion } : {}) });
76
89
  this.append(input.operatorSessionId, interview.sessionId, "draft", { taskId: interview.taskId, draft: draft.draft, draftSha256: draft.draftSha256, ...(recommendation ? { taskKindRecommendation: recommendation } : {}) });
@@ -112,7 +125,7 @@ export class ChatInterviewAdapter {
112
125
  const draft = await this.drafts.get(interview.taskId);
113
126
  if (!draft)
114
127
  return undefined;
115
- const question = nextGrillMeQuestion(draft.draft);
128
+ const question = await this.nextQuestion(draft.draft);
116
129
  let fresh = false;
117
130
  let reason;
118
131
  if (interview.assessmentId) {
@@ -25,6 +25,7 @@ import { listRepoDirectory, readRepoPreview, RepoBrowserError, } from "./repo-br
25
25
  import { projectRuntimeContext } from "./runtime-context.js";
26
26
  import { patchModelsConfig, readModelsConfig, readPackageInventory, readSkillPreferences, writeSkillPreferences, } from "./pi-console-config.js";
27
27
  import { ChatInterviewAdapter, projectChatInterviewState } from "./interview-adapter.js";
28
+ import { classifyChatCompactionFailure } from "./compaction-errors.js";
28
29
  import { evaluateMutationGate, isMutationMethod, } from "../security.js";
29
30
  import { dispatchOperatorAction } from "../operator-actions.js";
30
31
  import { makeHumanGateCard } from "./human-gate-card.js";
@@ -1414,7 +1415,11 @@ export async function handleChatCompact(req, res, deps, sessionId) {
1414
1415
  sendJson(res, 200, { ok: true, snapshot, eventId: event.eventId });
1415
1416
  }
1416
1417
  catch (error) {
1417
- sendJson(res, 409, { ok: false, error: { code: "COMPACTION_FAILED", message: error instanceof Error ? error.message : String(error) } });
1418
+ const classified = classifyChatCompactionFailure(error);
1419
+ sendJson(res, classified.status, {
1420
+ ok: false,
1421
+ error: { code: classified.code, message: classified.message },
1422
+ });
1418
1423
  }
1419
1424
  }
1420
1425
  export async function handleChatMessages(res, deps, sessionId, url) {
@@ -4,16 +4,39 @@
4
4
  * assessment when required fields are present.
5
5
  */
6
6
  import { consoleWorkflowKindPromptList, normalizeConsoleWorkflowKind, } from "../workflow-kinds.js";
7
+ import { classifyTaskDemand, } from "../../../task/task-demand-routing.js";
7
8
  const GAP_ORDER = [
8
9
  "objective",
9
10
  "scope",
10
11
  "nonGoals",
11
12
  "acceptanceCriteria",
12
- "taskKind",
13
13
  "allowedPaths",
14
+ "taskKind",
14
15
  "forbiddenPaths",
15
16
  "verifyCommands",
16
17
  ];
18
+ function requirementMarkdownFromDraft(draft) {
19
+ const requirement = draft.requirement ?? {};
20
+ return [
21
+ "# 需求",
22
+ requirement.objective ?? "",
23
+ ...(requirement.scope ?? []),
24
+ ...(requirement.acceptanceCriteria ?? []).map((item) => `${item.id}: ${item.text}`),
25
+ "## 非目标",
26
+ ...(requirement.nonGoals ?? []),
27
+ ].join("\n");
28
+ }
29
+ export function recommendTaskKindForDraft(draft, context = {}) {
30
+ const classification = classifyTaskDemand({
31
+ title: draft.title,
32
+ requirementMarkdown: requirementMarkdownFromDraft(draft),
33
+ allowedPaths: draft.constraints?.allowedPaths ?? [],
34
+ frontendProject: context.frontendProject,
35
+ });
36
+ return classification.kind === "frontend"
37
+ ? "frontend-implementation"
38
+ : "standard";
39
+ }
17
40
  function hasText(value) {
18
41
  return typeof value === "string" && value.trim().length > 0;
19
42
  }
@@ -44,7 +67,7 @@ export function listDraftGaps(draft) {
44
67
  export function isDraftStructurallyComplete(draft) {
45
68
  return listDraftGaps(draft).length === 0;
46
69
  }
47
- export function buildQuestionForGap(gapId, draft) {
70
+ export function buildQuestionForGap(gapId, draft, context = {}) {
48
71
  const taskId = draft.taskId;
49
72
  const questions = {
50
73
  objective: {
@@ -83,7 +106,7 @@ export function buildQuestionForGap(gapId, draft) {
83
106
  id: `q-${gapId}`,
84
107
  gapId,
85
108
  text: `流程类型(taskKind)应设为?(${consoleWorkflowKindPromptList()})`,
86
- recommendation: "standard",
109
+ recommendation: recommendTaskKindForDraft(draft, context),
87
110
  affectsFields: ["taskKind"],
88
111
  risk: "medium",
89
112
  },
@@ -114,11 +137,11 @@ export function buildQuestionForGap(gapId, draft) {
114
137
  };
115
138
  return questions[gapId];
116
139
  }
117
- export function nextGrillMeQuestion(draft) {
140
+ export function nextGrillMeQuestion(draft, context = {}) {
118
141
  const gaps = listDraftGaps(draft);
119
142
  for (const gapId of GAP_ORDER) {
120
143
  if (gaps.includes(gapId))
121
- return buildQuestionForGap(gapId, draft);
144
+ return buildQuestionForGap(gapId, draft, context);
122
145
  }
123
146
  return undefined;
124
147
  }