@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 +12 -4
- package/CHANGELOG.md +21 -1
- package/dist/application/dag/generate-task-dag.js +8 -5
- package/dist/commands/init.js +3 -3
- package/dist/task/task-demand-routing.js +14 -27
- package/dist/worker/console/operator-actions.js +135 -24
- package/dist/worker/console/static/assets/index-CSRIhuzh.js +29 -0
- package/dist/worker/console/static/assets/index-Yyn3ynVv.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/prd-file-import.js +218 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +8 -9
- package/dist/workflows/dag/l5-report-metrics.js +0 -3
- package/docs/architecture/evolution.md +1 -1
- package/docs/architecture/system-overview.md +1 -1
- package/docs/operations/local-development-environment.md +19 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/backend-test-dag.retrospect.prompt.md +0 -3
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -2
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/index-Cv68Ge5n.js +0 -27
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +0 -1
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
243
|
-
|
|
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
|
|
258
|
-
//
|
|
259
|
-
|
|
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, {
|
package/dist/commands/init.js
CHANGED
|
@@ -2049,11 +2049,11 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
2049
2049
|
"",
|
|
2050
2050
|
"## Specialized Task Kinds",
|
|
2051
2051
|
"",
|
|
2052
|
-
"- standard tasks
|
|
2053
|
-
"-
|
|
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
|
|
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
|
-
|
|
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
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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:
|
|
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) {
|