@tea-agent/loop-agent 0.24.4-beta.0 → 0.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -64,13 +64,21 @@
64
64
 
65
65
  ## 验证
66
66
 
67
- 权威源:`docs/governance/verification-matrix.md`。常用:
67
+ 权威源:`docs/governance/verification-matrix.md`。
68
+
69
+ 默认节奏:编辑中只跑矩阵「最低验证」(typecheck + 定向 Vitest);`git commit` 的 `pre-commit` 只做 `check-repo`;`git push` 的 `pre-push` 跑 `bash scripts/ci.sh`(全量 typecheck + `npm test`)。每个 clone 安装一次 hooks:`bash scripts/install-git-hooks.sh`。
70
+
71
+ 常用:
68
72
 
69
73
  ```bash
70
- npm run typecheck && npm test && npm run build
71
- node bin/loop-agent.js --help
72
- bash scripts/check-repo.sh
74
+ # 编辑中
75
+ npm run typecheck
76
+ npx vitest run <相关测试路径>
77
+
78
+ # push / 交付前(或依赖 pre-push)
73
79
  bash scripts/ci.sh
80
+ npm run build
81
+ node bin/loop-agent.js --help
74
82
  ```
75
83
 
76
84
  文档站变更:`npm run docs:build`。init / architecture / skill entry / pack 定向验证见 verification-matrix。
package/CHANGELOG.md CHANGED
@@ -2,10 +2,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.24.4] - 2026-07-28
6
+
7
+ ### 重点更新
8
+
9
+ - 控制台「导入需求」支持多文件批量导入,自动合成可编辑正文后创建任务
10
+ - 后端测试移除重复运行稳定性门槛,每次执行同时生成用户可读的 HTML 与 Markdown 报告
11
+ - 本地 pre-push 钩子升级为执行完整 CI 流程,覆盖治理检查、类型检查与全量测试
12
+
13
+ ### 新增
14
+
15
+ - 控制台「导入需求」支持选择或拖入一份或多份 Markdown/TXT 文档(严格 UTF-8,单文件 ≤8 MiB,最多 8 个),多文件分卷归档并合成可编辑正文后创建任务
16
+
5
17
  ### 改进
6
18
 
19
+ - 后端测试 L-5 结论不再将重复运行稳定性作为门槛,每次执行同时产出用户可读的 HTML 与 Markdown 报告,内部 facts 文件保留用于审计
20
+ - 本地 pre-push 钩子改为执行完整 CI 流程(治理检查 + 类型检查 + 全量测试),pre-commit 保持仅执行 check-repo
21
+ - harness 中等复杂度(MED)模型调整为 grok-4.5,并同步对齐相关 smoke/init 断言
22
+ - 归档已完成的设计文档与执行计划,刷新 0.24.x 六个月规划与能力摘要,并同步验证与 hooks 文档
23
+
24
+ ## [0.24.3] - 2026-07-28
25
+
26
+ ### 重点更新
27
+
7
28
  - 发布工作流改为发布已通过完整门禁并经 SHA-256 校验的 npm tarball,避免 `npm publish` 再次触发全量测试、类型检查与构建。
8
- - 默认 `standard` 实现任务在 React、Next.js 或 Vue 项目中直接选择 `frontend-implementation` DAG,不再依赖需求关键词;明确后端、文档/测试范围或前端否定仍保留对应通用流程。
9
29
 
10
30
  ## [0.24.2] - 2026-07-28
11
31
 
@@ -239,8 +239,10 @@ export async function generateTaskDagUseCase(input) {
239
239
  reasons: ["dag run-task validate did not report governanceProfile"],
240
240
  });
241
241
  const hasExplicitSpecializedTaskKind = candidateResult.templateSelection.source === "taskKind";
242
- const hasAutomaticTaskSourceRoute = candidateResult.templateSelection.source === "task-source";
243
- if (hasExplicitSpecializedTaskKind || hasAutomaticTaskSourceRoute) {
242
+ const hasSafeAutomaticTaskSourceRoute = candidateResult.templateSelection.source === "task-source" &&
243
+ profileRouting.selectedByProfile !== "supervised" &&
244
+ profileRouting.selectedTemplate !== "supervised-implementation";
245
+ if (hasExplicitSpecializedTaskKind || hasSafeAutomaticTaskSourceRoute) {
244
246
  profileRouting.selectedTemplate = candidateResult.template;
245
247
  profileRouting.source = hasExplicitSpecializedTaskKind
246
248
  ? "taskKind"
@@ -254,9 +256,10 @@ export async function generateTaskDagUseCase(input) {
254
256
  profileRouting.selectedByProfile = parsed.profile;
255
257
  }
256
258
  }
257
- // Persist the automatic business workflow route while preserving the
258
- // governance profile as a separate routing dimension.
259
- if (hasAutomaticTaskSourceRoute && candidateResult.template === "frontend-implementation") {
259
+ // Persist only a safe automatic route. Explicit supervised/profile policy
260
+ // must remain authoritative and must not be weakened by this convenience
261
+ // marker. Keep taskId stable; frontend-implementation is the taskKind.
262
+ if (hasSafeAutomaticTaskSourceRoute && candidateResult.template === "frontend-implementation") {
260
263
  const taskConfig = await loadTaskConfig(repoRoot, parsed.taskId);
261
264
  if (!taskConfig.taskKind || taskConfig.taskKind === "standard") {
262
265
  await writeTaskConfig(repoRoot, parsed.taskId, {
@@ -2049,11 +2049,11 @@ function buildTargetFeatureWorkflow(input) {
2049
2049
  "",
2050
2050
  "## Specialized Task Kinds",
2051
2051
  "",
2052
- "- standard tasks first use the structured task type in `source/需求.md`, then combine `allowedPaths` with strong React/Next/Vue project evidence for deterministic routing.",
2053
- "- A frontend project defaults eligible implementation work to the frontend DAG without requirement keyword matching. Explicit backend, mixed, frontend-negated, and documentation/test-only scopes keep the template selected by the normal governance profile.",
2052
+ "- standard tasks are classified conservatively by combining the task title, `source/需求.md`, structured `allowedPaths`, strong React/Next/Vue project evidence, and user-visible interaction/state semantics.",
2053
+ "- An implicit frontend demand selects the frontend DAG only when project or frontend-path evidence is paired with product-delivery semantics; framework evidence alone is insufficient. Backend, mixed, negated, documentation/test maintenance, and unknown demands keep the template selected by the normal governance profile.",
2054
2054
  "- `frontend-mock-assess-pi` reads Mock/API/schema evidence and selects `native|browser-intercept|request-adapter|not-needed|blocked` before frontend planning; its deterministic gate rejects blocked or malformed output, while the existing frontend implementer remains the only writer.",
2055
2055
  "- Mock-backed verification never proves real API integration. When the backend was not exercised, closeout must retain the gap and name `<task-id>-real-api-integration-verify`; that follow-up is explicitly created/run after backend readiness, never automatic.",
2056
- "- Automatic frontend classification selects the frontend implementation workflow and persists taskKind; explicit profiles, workflowPolicy, and supervised quality gates record governance strength without switching the business workflow back to a generic DAG.",
2056
+ "- Automatic frontend classification never replaces a supervised template selected by an explicit profile, workflowPolicy, or the supervised quality gate.",
2057
2057
  "- A backend implementation does not select the backend test DAG. `backend-test` remains an explicit test-engineering workflow.",
2058
2058
  "- Explicit specialized `taskKind` values remain compatible and take precedence over task-source classification.",
2059
2059
  "",
@@ -298,11 +298,6 @@ export function classifyTaskDemand(input) {
298
298
  message: "allowedPaths include a backend-specific source path",
299
299
  });
300
300
  }
301
- const allowedPathsOnlyCoverNonProductArtifacts = input.allowedPaths.length > 0 &&
302
- input.allowedPaths.every((rawPath) => {
303
- const allowedPath = normalizedAllowedPath(rawPath);
304
- return matchesAny(allowedPath, NON_PRODUCT_ALLOWED_PATH_PATTERNS);
305
- });
306
301
  const pathSupportedFrontendDelivery = frontendPath &&
307
302
  (titleSignals.pathSupportedFrontendDelivery ||
308
303
  requirementSignals.pathSupportedFrontendDelivery ||
@@ -333,27 +328,20 @@ export function classifyTaskDemand(input) {
333
328
  message: "task text contains user-visible interaction/state delivery and the project has strong frontend framework evidence",
334
329
  });
335
330
  }
336
- const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
337
- const frontendProjectDefaultImplementation = hasStrongFrontendProjectEvidence &&
338
- !backendDelivery &&
339
- !frontendNegated &&
340
- !allowedPathsOnlyCoverNonProductArtifacts;
341
- if (frontendProjectDefaultImplementation) {
342
- addSignal(signals, {
343
- id: "frontend-project-default-implementation",
344
- source: "project",
345
- message: "frontend project evidence selects the frontend implementation workflow by default",
346
- });
347
- }
348
331
  const frontendDelivery = titleSignals.frontendDelivery ||
349
332
  requirementSignals.frontendDelivery ||
350
333
  pathSupportedFrontendDelivery ||
351
- projectSupportedFrontendDelivery ||
352
- frontendProjectDefaultImplementation;
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
+ });
353
340
  if (allowedPathsOnlyCoverNonProductArtifacts && frontendDelivery) {
354
341
  blockers.push("non-product-allowed-paths");
355
342
  }
356
343
  const eligibleFrontendDelivery = frontendDelivery && !allowedPathsOnlyCoverNonProductArtifacts;
344
+ const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
357
345
  const taskTypeConflict = (hasFrontendTaskType && (backendDelivery || frontendNegated)) ||
358
346
  (hasBackendTaskType && eligibleFrontendDelivery);
359
347
  if (taskTypeConflict)
@@ -414,6 +402,13 @@ export function resolveTaskDagTemplateSelection(input) {
414
402
  reasons: [],
415
403
  };
416
404
  }
405
+ if (requestedTemplate !== "standard-dag") {
406
+ return {
407
+ template: requestedTemplate,
408
+ source: "requested-template",
409
+ reasons: [],
410
+ };
411
+ }
417
412
  const classification = classifyTaskDemand(input);
418
413
  if (classification.kind === "frontend") {
419
414
  const frontendSignals = classification.signals
@@ -428,14 +423,6 @@ export function resolveTaskDagTemplateSelection(input) {
428
423
  classification,
429
424
  };
430
425
  }
431
- if (requestedTemplate !== "standard-dag") {
432
- return {
433
- template: requestedTemplate,
434
- source: "requested-template",
435
- reasons: [],
436
- classification,
437
- };
438
- }
439
426
  return {
440
427
  template: requestedTemplate,
441
428
  source: "requested-template",
@@ -1490,9 +1490,14 @@ async function allocateUniqueTaskId(repoRoot, preferredId) {
1490
1490
  }
1491
1491
  async function dispatchBootstrapFromPrd(ctx, p) {
1492
1492
  const content = str(p.content) ?? str(p.fileText);
1493
- if (!content) {
1493
+ if (!content?.trim()) {
1494
1494
  return invalid("bootstrapFromPrd", "content is required");
1495
1495
  }
1496
+ const parsedDocs = parseBootstrapDocuments(p.documents);
1497
+ if (!parsedDocs.ok) {
1498
+ return invalid("bootstrapFromPrd", parsedDocs.message);
1499
+ }
1500
+ const documents = parsedDocs.documents;
1496
1501
  const taskKind = normalizeConsoleWorkflowKind(str(p.taskKind), "standard");
1497
1502
  const identity = deriveTaskIdentityFromPrd(content);
1498
1503
  let taskId;
@@ -1539,28 +1544,86 @@ async function dispatchBootstrapFromPrd(ctx, p) {
1539
1544
  }),
1540
1545
  };
1541
1546
  }
1542
- const staged = await stageUtf8Text(ctx.appData, content, {
1543
- extension: ".md",
1544
- });
1545
- const imported = await run(["import-prd", taskId, "--file", staged.path, "--json"], {
1546
- cwd: ctx.repoRoot,
1547
- artifactName: "bootstrap-import-prd",
1548
- expectJson: true,
1549
- timeoutMs: 60_000,
1550
- });
1551
- if (!imported.ok || imported.exitCode !== 0) {
1552
- return {
1553
- kind: "error",
1554
- status: 400,
1555
- body: operatorFailed({
1556
- command: "bootstrapFromPrd",
1557
- outcome: "rejected",
1558
- code: "INTERNAL_ERROR",
1559
- message: imported.stderr?.trim() ||
1560
- `import-prd failed with exit ${imported.exitCode}`,
1561
- details: { exitCode: imported.exitCode, taskId },
1562
- }),
1563
- };
1547
+ const succeededImports = [];
1548
+ const importResults = [];
1549
+ const runImportPrd = async (label, text, name) => {
1550
+ const staged = await stageUtf8Text(ctx.appData, text, {
1551
+ extension: ".md",
1552
+ });
1553
+ const imported = await run([
1554
+ "import-prd",
1555
+ taskId,
1556
+ "--file",
1557
+ staged.path,
1558
+ "--name",
1559
+ name,
1560
+ "--role",
1561
+ "requirement",
1562
+ "--json",
1563
+ ], {
1564
+ cwd: ctx.repoRoot,
1565
+ artifactName: `bootstrap-import-prd-${label}`,
1566
+ expectJson: true,
1567
+ timeoutMs: 60_000,
1568
+ });
1569
+ if (!imported.ok || imported.exitCode !== 0) {
1570
+ return {
1571
+ kind: "error",
1572
+ status: 400,
1573
+ body: operatorFailed({
1574
+ command: "bootstrapFromPrd",
1575
+ outcome: "rejected",
1576
+ code: "INTERNAL_ERROR",
1577
+ message: imported.stderr?.trim() ||
1578
+ `import-prd failed for "${name}" with exit ${imported.exitCode}. 任务已创建但不完整,请用 CLI 补 import-prd 或换 taskId 重试。`,
1579
+ details: {
1580
+ exitCode: imported.exitCode,
1581
+ taskId,
1582
+ failedName: name,
1583
+ succeededImports: [...succeededImports],
1584
+ },
1585
+ }),
1586
+ };
1587
+ }
1588
+ succeededImports.push(name);
1589
+ importResults.push(imported.json ?? null);
1590
+ return null;
1591
+ };
1592
+ if (documents && documents.length > 0) {
1593
+ for (const doc of documents) {
1594
+ const failure = await runImportPrd(doc.name, doc.content, doc.name);
1595
+ if (failure)
1596
+ return failure;
1597
+ }
1598
+ const composedFailure = await runImportPrd("composed", content, "composed");
1599
+ if (composedFailure)
1600
+ return composedFailure;
1601
+ }
1602
+ else {
1603
+ const staged = await stageUtf8Text(ctx.appData, content, {
1604
+ extension: ".md",
1605
+ });
1606
+ const imported = await run(["import-prd", taskId, "--file", staged.path, "--json"], {
1607
+ cwd: ctx.repoRoot,
1608
+ artifactName: "bootstrap-import-prd",
1609
+ expectJson: true,
1610
+ timeoutMs: 60_000,
1611
+ });
1612
+ if (!imported.ok || imported.exitCode !== 0) {
1613
+ return {
1614
+ kind: "error",
1615
+ status: 400,
1616
+ body: operatorFailed({
1617
+ command: "bootstrapFromPrd",
1618
+ outcome: "rejected",
1619
+ code: "INTERNAL_ERROR",
1620
+ message: imported.stderr?.trim() ||
1621
+ `import-prd failed with exit ${imported.exitCode}`,
1622
+ details: { exitCode: imported.exitCode, taskId },
1623
+ }),
1624
+ };
1625
+ }
1626
+ importResults.push(imported.json ?? null);
1564
1627
  }
1565
1628
  const draftSaved = await ctx.drafts.save({
1566
1629
  ...emptyDraft(taskId, title, { taskKind }),
@@ -1578,10 +1641,58 @@ async function dispatchBootstrapFromPrd(ctx, p) {
1578
1641
  identitySource: identity.source,
1579
1642
  preferredTaskId: identity.taskId,
1580
1643
  draftSha256: draftSaved.draftSha256,
1581
- import: imported.json ?? null,
1644
+ import: importResults.length === 1 ? importResults[0] : null,
1645
+ imports: importResults,
1646
+ documentNames: documents && documents.length > 0
1647
+ ? [...documents.map((d) => d.name), "composed"]
1648
+ : undefined,
1582
1649
  }),
1583
1650
  };
1584
1651
  }
1652
+ const MAX_BOOTSTRAP_DOCUMENTS = 8;
1653
+ function parseBootstrapDocuments(raw) {
1654
+ if (raw === undefined || raw === null) {
1655
+ return { ok: true, documents: null };
1656
+ }
1657
+ if (!Array.isArray(raw)) {
1658
+ return { ok: false, message: "documents must be an array" };
1659
+ }
1660
+ if (raw.length === 0) {
1661
+ return { ok: true, documents: null };
1662
+ }
1663
+ if (raw.length > MAX_BOOTSTRAP_DOCUMENTS) {
1664
+ return {
1665
+ ok: false,
1666
+ message: `documents supports at most ${MAX_BOOTSTRAP_DOCUMENTS} entries`,
1667
+ };
1668
+ }
1669
+ const documents = [];
1670
+ for (let i = 0; i < raw.length; i += 1) {
1671
+ const entry = raw[i];
1672
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1673
+ return { ok: false, message: `documents[${i}] must be an object` };
1674
+ }
1675
+ const rec = entry;
1676
+ const name = str(rec.name)?.trim();
1677
+ const content = str(rec.content);
1678
+ const fileName = str(rec.fileName)?.trim();
1679
+ if (!name) {
1680
+ return { ok: false, message: `documents[${i}].name is required` };
1681
+ }
1682
+ if (!content?.trim()) {
1683
+ return {
1684
+ ok: false,
1685
+ message: `documents[${i}].content is required`,
1686
+ };
1687
+ }
1688
+ documents.push({
1689
+ name,
1690
+ content,
1691
+ ...(fileName ? { fileName } : {}),
1692
+ });
1693
+ }
1694
+ return { ok: true, documents };
1695
+ }
1585
1696
  async function ensureDraft(ctx, taskId, title, options) {
1586
1697
  const existing = await ctx.drafts.get(taskId);
1587
1698
  if (existing) {