@tea-agent/loop-agent 0.25.3 → 0.25.5

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 (49) hide show
  1. package/AGENTS.md +6 -0
  2. package/CHANGELOG.md +55 -0
  3. package/dist/application/dag/args.js +21 -1
  4. package/dist/application/dag/run-dag.js +1 -0
  5. package/dist/cli/command-definitions.js +1 -1
  6. package/dist/cli/program.js +82 -63
  7. package/dist/commands/client-recovery.js +209 -62
  8. package/dist/commands/init.js +206 -82
  9. package/dist/commands/run-dag-progress.js +109 -0
  10. package/dist/commands/run-dag.js +16 -5
  11. package/dist/executors/dag-pi-executor.js +80 -15
  12. package/dist/executors/model-routing.js +1 -1
  13. package/dist/executors/shell-executor.js +159 -0
  14. package/dist/executors/shell-write-guard.js +21 -7
  15. package/dist/worker/console/repo-fingerprint.js +7 -1
  16. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +964 -0
  17. package/dist/workflows/dag/backend-test-case-manifest.js +39 -1
  18. package/dist/workflows/dag/backend-test-markdown-workflow.js +306 -30
  19. package/dist/workflows/dag/backend-test-result-contract.js +35 -9
  20. package/dist/workflows/dag/convergence/controller.js +134 -9
  21. package/dist/workflows/dag/frontend-test-case-checklist.js +71 -0
  22. package/dist/workflows/dag/frontend-test-html-report.js +77 -0
  23. package/dist/workflows/dag/frontend-test-l5-report.js +138 -0
  24. package/dist/workflows/dag/frontend-test-result-contract.js +44 -1
  25. package/dist/workflows/dag/init-hybrid.js +267 -80
  26. package/dist/workflows/dag/node-execution.js +64 -11
  27. package/dist/workflows/dag/prompt.js +118 -4
  28. package/dist/workflows/dag/retry-policy.js +5 -4
  29. package/dist/workflows/dag/scheduler.js +32 -5
  30. package/dist/workflows/dag/types.js +10 -3
  31. package/dist/workflows/dag/validate.js +6 -3
  32. package/docs/architecture/dag-execution.md +7 -4
  33. package/docs/architecture/runtime-boundaries.md +1 -1
  34. package/docs/templates/agent-dag.base.json +1 -1
  35. package/docs/templates/agent-dag.final-verification.json +1 -1
  36. package/docs/templates/agent-dag.schema.json +6 -0
  37. package/docs/templates/agent-dag.supervised-implementation.json +1 -1
  38. package/docs/templates/backend-test-dag.json +40 -13
  39. package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
  40. package/docs/templates/frontend-test-dag.json +62 -5
  41. package/docs/templates/hybrid-dag.json +1 -1
  42. package/examples/decision-gate-agent-dag.json +1 -1
  43. package/examples/example-dag.json +1 -1
  44. package/examples/hybrid-loop-agent-dag.json +1 -1
  45. package/harness.json +3 -2
  46. package/package.json +1 -1
  47. package/skills/loop-agent/references/command-reference.md +2 -1
  48. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  49. package/skills/loop-agent/references/model-routing.md +1 -1
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { access, copyFile, mkdir, readdir, readFile, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
2
+ import { access, copyFile, mkdir, readdir, readFile, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { isDeepStrictEqual } from "node:util";
@@ -16,18 +16,36 @@ const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
16
16
  const DEFAULT_GOVERNANCE_ROOT = "ai_workspace/loop-agent";
17
17
  const CORE_DOC_FILES = [
18
18
  { source: "README.md", target: "README.md" },
19
- { source: "architecture/runtime-boundaries.md", target: "architecture/runtime-boundaries.md" },
20
- { source: "governance/development-principles.md", target: "development-principles.md" },
19
+ {
20
+ source: "architecture/runtime-boundaries.md",
21
+ target: "architecture/runtime-boundaries.md",
22
+ },
23
+ {
24
+ source: "governance/development-principles.md",
25
+ target: "development-principles.md",
26
+ },
21
27
  { source: "governance/feature-workflow.md", target: "feature-workflow.md" },
22
- { source: "governance/verification-matrix.md", target: "verification-matrix.md" },
28
+ {
29
+ source: "governance/verification-matrix.md",
30
+ target: "verification-matrix.md",
31
+ },
23
32
  { source: "runtime/loop-agent-harness.md", target: "loop-agent-harness.md" },
24
- { source: "governance/harness-methodology-tdd.md", target: "harness-methodology-tdd.md" },
25
- { source: "governance/harness-methodology-verification.md", target: "harness-methodology-verification.md" },
26
- { source: "governance/harness-methodology-debugging.md", target: "harness-methodology-debugging.md" },
33
+ {
34
+ source: "governance/harness-methodology-tdd.md",
35
+ target: "harness-methodology-tdd.md",
36
+ },
37
+ {
38
+ source: "governance/harness-methodology-verification.md",
39
+ target: "harness-methodology-verification.md",
40
+ },
41
+ {
42
+ source: "governance/harness-methodology-debugging.md",
43
+ target: "harness-methodology-debugging.md",
44
+ },
27
45
  { source: "templates/README.md", target: "templates/README.md" },
28
46
  ];
29
47
  function coreDocSourceForTarget(target) {
30
- return CORE_DOC_FILES.find((entry) => entry.target === target)?.source ?? target;
48
+ return (CORE_DOC_FILES.find((entry) => entry.target === target)?.source ?? target);
31
49
  }
32
50
  const GOVERNANCE_README_DIRS = [
33
51
  "decisions",
@@ -856,7 +874,7 @@ function buildManagedAgentsBlock(input) {
856
874
  "### Agent DAG 路径",
857
875
  "",
858
876
  "```bash",
859
- "loop-agent new-task <task-id> \"任务标题\"",
877
+ 'loop-agent new-task <task-id> "任务标题"',
860
878
  "# write .harness/tasks/<task-id>/source/需求.md",
861
879
  "# write .harness/tasks/<task-id>/source/执行约束.md",
862
880
  "loop-agent dag run-task <task-id> --profile auto --strict-models",
@@ -870,12 +888,12 @@ function buildManagedAgentsBlock(input) {
870
888
  "",
871
889
  "### 任务类型路由(taskKind)",
872
890
  "",
873
- "- 用户明确提出后端测试、接口/API 测试、pytest,或语境明确为后端的自动化测试时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `\"backend-test\"`,不得保留默认 `standard`。",
891
+ '- 用户明确提出后端测试、接口/API 测试、pytest,或语境明确为后端的自动化测试时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。',
874
892
  "- `backend-test` 是 `taskKind`,不是 `--profile` 的可选值;运行 `dag run-task` 时继续使用 `--profile auto`。",
875
893
  "- 仅出现“自动化测试”且无法判断前后端时,先阅读任务源与目标项目技术栈再决定,禁止无条件路由到 `backend-test`。",
876
- "- 用户提示词明确是前端实现需求(例如前端页面、UI、组件或交互开发)时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `\"frontend-implementation\"`,不得保留默认 `standard`。",
894
+ '- 用户提示词明确是前端实现需求(例如前端页面、UI、组件或交互开发)时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"frontend-implementation"`,不得保留默认 `standard`。',
877
895
  "- `frontend-implementation` 是 `taskKind`,不是 `--profile` 的可选值;运行 `dag run-task` 时继续使用 `--profile auto`,也可按需显式选择 `minimal` / `standard` / `reviewed` / `supervised`,不要把业务模板名当作 profile。",
878
- "- 前端自动化测试(浏览器/UI 自动化、Playwright、E2E)继续使用 `taskKind: \"frontend-test\"`,不得设置为 `frontend-implementation`。",
896
+ '- 前端自动化测试(浏览器/UI 自动化、Playwright、E2E)继续使用 `taskKind: "frontend-test"`,不得设置为 `frontend-implementation`。',
879
897
  "",
880
898
  "### 运行看板(只读)",
881
899
  "",
@@ -909,7 +927,8 @@ function mergeManagedBlock(existing, block) {
909
927
  const start = existing.indexOf(MANAGED_BLOCK_START);
910
928
  const end = existing.indexOf(MANAGED_BLOCK_END);
911
929
  if (start >= 0 && end > start) {
912
- return `${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(end + MANAGED_BLOCK_END.length).trimStart()}`.trimEnd() + "\n";
930
+ return (`${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(end + MANAGED_BLOCK_END.length).trimStart()}`.trimEnd() +
931
+ "\n");
913
932
  }
914
933
  return `${existing.trimEnd()}\n\n${block}\n`;
915
934
  }
@@ -917,7 +936,8 @@ function mergeGitignoreManagedBlock(existing, block) {
917
936
  const start = existing.indexOf(GITIGNORE_BLOCK_START);
918
937
  const end = existing.indexOf(GITIGNORE_BLOCK_END);
919
938
  if (start >= 0 && end > start) {
920
- return `${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(end + GITIGNORE_BLOCK_END.length).trimStart()}`.trimEnd() + "\n";
939
+ return (`${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(end + GITIGNORE_BLOCK_END.length).trimStart()}`.trimEnd() +
940
+ "\n");
921
941
  }
922
942
  if (!existing.trim())
923
943
  return `${block}\n`;
@@ -1056,19 +1076,22 @@ function buildHarness(input) {
1056
1076
  // Do not project legacy step-routing models / modelProfiles / modelRouting into
1057
1077
  // new or refreshed harness.json. Keep them only when the target already has them
1058
1078
  // so old files remain loadable until the user migrates.
1059
- if (isRecord(input.existing.models) && Object.keys(input.existing.models).length > 0) {
1079
+ if (isRecord(input.existing.models) &&
1080
+ Object.keys(input.existing.models).length > 0) {
1060
1081
  harness.models = input.existing.models;
1061
1082
  }
1062
1083
  else {
1063
1084
  delete harness.models;
1064
1085
  }
1065
- if (isRecord(input.existing.modelProfiles) && Object.keys(input.existing.modelProfiles).length > 0) {
1086
+ if (isRecord(input.existing.modelProfiles) &&
1087
+ Object.keys(input.existing.modelProfiles).length > 0) {
1066
1088
  harness.modelProfiles = input.existing.modelProfiles;
1067
1089
  }
1068
1090
  else {
1069
1091
  delete harness.modelProfiles;
1070
1092
  }
1071
- if (isRecord(input.existing.modelRouting) && Object.keys(input.existing.modelRouting).length > 0) {
1093
+ if (isRecord(input.existing.modelRouting) &&
1094
+ Object.keys(input.existing.modelRouting).length > 0) {
1072
1095
  harness.modelRouting = input.existing.modelRouting;
1073
1096
  }
1074
1097
  else {
@@ -1115,7 +1138,9 @@ async function copyDirMerge(input) {
1115
1138
  const source = path.join(input.assetRoot, input.sourceRelativePath);
1116
1139
  const target = path.join(input.repoRoot, input.targetRelativePath);
1117
1140
  await copyDir(source, target);
1118
- input.written.push(input.targetRelativePath.endsWith("/") ? input.targetRelativePath : `${input.targetRelativePath}/`);
1141
+ input.written.push(input.targetRelativePath.endsWith("/")
1142
+ ? input.targetRelativePath
1143
+ : `${input.targetRelativePath}/`);
1119
1144
  }
1120
1145
  async function ensureHarnessDirs(repoRoot, written) {
1121
1146
  const dirs = [
@@ -1192,7 +1217,8 @@ function manifestPathToTargetPath(relativePath, governanceRoot) {
1192
1217
  return relativePath;
1193
1218
  }
1194
1219
  function targetPathToManifestPath(relativePath, governanceRoot) {
1195
- if (governanceRoot !== "docs" && relativePath.startsWith(`${governanceRoot}/`)) {
1220
+ if (governanceRoot !== "docs" &&
1221
+ relativePath.startsWith(`${governanceRoot}/`)) {
1196
1222
  return `docs/${relativePath.slice(`${governanceRoot}/`.length)}`;
1197
1223
  }
1198
1224
  if (relativePath.startsWith(".agents/skills/")) {
@@ -1202,7 +1228,9 @@ function targetPathToManifestPath(relativePath, governanceRoot) {
1202
1228
  }
1203
1229
  async function readPackageVersion(assetRoot) {
1204
1230
  const packageJson = JSON.parse(await readFile(path.join(assetRoot, "package.json"), "utf-8"));
1205
- return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
1231
+ return typeof packageJson.version === "string"
1232
+ ? packageJson.version
1233
+ : "0.0.0";
1206
1234
  }
1207
1235
  async function readInitSurfaceManifest(assetRoot) {
1208
1236
  const manifestPath = path.join(assetRoot, "docs", "init-surface.manifest.json");
@@ -1344,7 +1372,10 @@ async function buildDesiredSurfaceContent(input) {
1344
1372
  const sourceManifestPath = manifestPath.slice(".agents/".length);
1345
1373
  const mirrorSourcePath = path.join(input.assetRoot, sourceManifestPath);
1346
1374
  if (await exists(mirrorSourcePath)) {
1347
- return { content: await readFile(mirrorSourcePath, "utf-8"), sourcePath: mirrorSourcePath };
1375
+ return {
1376
+ content: await readFile(mirrorSourcePath, "utf-8"),
1377
+ sourcePath: mirrorSourcePath,
1378
+ };
1348
1379
  }
1349
1380
  return {};
1350
1381
  }
@@ -1359,7 +1390,10 @@ async function buildDesiredSurfaceContent(input) {
1359
1390
  return { content: generated };
1360
1391
  const mappedSourcePath = path.join(input.assetRoot, "docs", coreDocSourceForTarget(doc));
1361
1392
  if (await exists(mappedSourcePath)) {
1362
- return { content: await readFile(mappedSourcePath, "utf-8"), sourcePath: mappedSourcePath };
1393
+ return {
1394
+ content: await readFile(mappedSourcePath, "utf-8"),
1395
+ sourcePath: mappedSourcePath,
1396
+ };
1363
1397
  }
1364
1398
  }
1365
1399
  const sourcePath = path.join(input.assetRoot, manifestPath);
@@ -1401,7 +1435,10 @@ export async function readRecordedSurfaceControllerVersion(repoRoot) {
1401
1435
  }
1402
1436
  if (!state || typeof state.controllerVersion !== "string")
1403
1437
  return undefined;
1404
- return { controllerVersion: state.controllerVersion, stateKind: state.stateKind };
1438
+ return {
1439
+ controllerVersion: state.controllerVersion,
1440
+ stateKind: state.stateKind,
1441
+ };
1405
1442
  }
1406
1443
  async function buildCurrentSurfaceState(input) {
1407
1444
  const assetRoot = await findPackageRoot();
@@ -1410,7 +1447,10 @@ async function buildCurrentSurfaceState(input) {
1410
1447
  const files = {};
1411
1448
  for (const manifestEntry of manifest.entries) {
1412
1449
  const targetRelativePath = normalizeRelativePath(manifestPathToTargetPath(manifestEntry.path, input.governanceRoot));
1413
- const entry = { ...manifestEntry, path: targetRelativePath };
1450
+ const entry = {
1451
+ ...manifestEntry,
1452
+ path: targetRelativePath,
1453
+ };
1414
1454
  const targetPath = path.join(input.repoRoot, targetRelativePath);
1415
1455
  const targetStat = await stat(targetPath).catch(() => null);
1416
1456
  const desired = await buildDesiredSurfaceContent({
@@ -1425,7 +1465,9 @@ async function buildCurrentSurfaceState(input) {
1425
1465
  status: targetStat ? "present" : "missing",
1426
1466
  relationship: "missing-from-target",
1427
1467
  sourceSha256,
1428
- sourcePath: desired.sourcePath ? repoRelative(assetRoot, desired.sourcePath) : undefined,
1468
+ sourcePath: desired.sourcePath
1469
+ ? repoRelative(assetRoot, desired.sourcePath)
1470
+ : undefined,
1429
1471
  mode: entry.mode,
1430
1472
  };
1431
1473
  if (entry.mode === "state") {
@@ -1443,7 +1485,9 @@ async function buildCurrentSurfaceState(input) {
1443
1485
  if (entry.mode === "directory") {
1444
1486
  files[targetRelativePath] = {
1445
1487
  ...base,
1446
- relationship: targetStat.isDirectory() ? "directory-present" : "local-existing-unknown",
1488
+ relationship: targetStat.isDirectory()
1489
+ ? "directory-present"
1490
+ : "local-existing-unknown",
1447
1491
  };
1448
1492
  continue;
1449
1493
  }
@@ -1457,14 +1501,17 @@ async function buildCurrentSurfaceState(input) {
1457
1501
  files[targetRelativePath] = {
1458
1502
  ...base,
1459
1503
  currentSha256,
1460
- relationship: currentBlock && sourceSha256 && sha256Text(currentBlock) === sourceSha256
1504
+ relationship: currentBlock &&
1505
+ sourceSha256 &&
1506
+ sha256Text(currentBlock) === sourceSha256
1461
1507
  ? "managed-block-current"
1462
1508
  : "managed-block-present",
1463
1509
  };
1464
1510
  continue;
1465
1511
  }
1466
1512
  let semanticallyMatchesGeneratedJson = false;
1467
- if (targetRelativePath === "harness.json" && desired.content !== undefined) {
1513
+ if (targetRelativePath === "harness.json" &&
1514
+ desired.content !== undefined) {
1468
1515
  try {
1469
1516
  semanticallyMatchesGeneratedJson = isDeepStrictEqual(JSON.parse(current.toString("utf-8")), JSON.parse(desired.content));
1470
1517
  }
@@ -1538,7 +1585,9 @@ async function listRelativeFiles(rootPath) {
1538
1585
  const files = [];
1539
1586
  async function walk(currentPath, relativeDir) {
1540
1587
  for (const entry of await readdir(currentPath, { withFileTypes: true })) {
1541
- const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
1588
+ const relativePath = relativeDir
1589
+ ? `${relativeDir}/${entry.name}`
1590
+ : entry.name;
1542
1591
  const absolutePath = path.join(currentPath, entry.name);
1543
1592
  if (entry.isDirectory())
1544
1593
  await walk(absolutePath, relativePath);
@@ -1567,7 +1616,8 @@ async function expectedLegacyInitFileSha(input) {
1567
1616
  }));
1568
1617
  }
1569
1618
  }
1570
- if (!input.legacyPath.startsWith("docs/") && !input.legacyPath.startsWith("skills/")) {
1619
+ if (!input.legacyPath.startsWith("docs/") &&
1620
+ !input.legacyPath.startsWith("skills/")) {
1571
1621
  return undefined;
1572
1622
  }
1573
1623
  if (input.legacyPath.startsWith("docs/")) {
@@ -1581,7 +1631,10 @@ function collectSafeRetiredDirectories(paths) {
1581
1631
  for (const retiredPath of paths) {
1582
1632
  let dir = path.posix.dirname(retiredPath);
1583
1633
  while (dir !== "." && dir !== "/") {
1584
- if (dir === "skills" || dir.startsWith("skills/") || dir === "docs" || dir.startsWith("docs/")) {
1634
+ if (dir === "skills" ||
1635
+ dir.startsWith("skills/") ||
1636
+ dir === "docs" ||
1637
+ dir.startsWith("docs/")) {
1585
1638
  dirs.add(dir);
1586
1639
  }
1587
1640
  dir = path.posix.dirname(dir);
@@ -1592,28 +1645,24 @@ function collectSafeRetiredDirectories(paths) {
1592
1645
  return [...dirs].sort((left, right) => right.split("/").length - left.split("/").length);
1593
1646
  }
1594
1647
  function needsHarnessModelMigration(harness) {
1595
- if ("model" in harness || "models" in harness || "modelProfiles" in harness || "modelRouting" in harness) {
1648
+ if ("model" in harness ||
1649
+ "models" in harness ||
1650
+ "modelProfiles" in harness ||
1651
+ "modelRouting" in harness) {
1596
1652
  return true;
1597
1653
  }
1598
1654
  if (isRecord(harness.executors)) {
1599
1655
  if ("cursor" in harness.executors)
1600
1656
  return true;
1601
1657
  if (isRecord(harness.executors.pi)) {
1602
- return ("requiresApiKey" in harness.executors.pi ||
1603
- "defaultModel" in harness.executors.pi);
1658
+ return "requiresApiKey" in harness.executors.pi;
1604
1659
  }
1605
1660
  }
1606
1661
  return false;
1607
1662
  }
1608
1663
  function migrateHarnessModelFields(harness) {
1609
1664
  const next = { ...harness };
1610
- const legacyModel = typeof next.model === "string"
1611
- ? next.model
1612
- : isRecord(next.executors) &&
1613
- isRecord(next.executors.pi) &&
1614
- typeof next.executors.pi.defaultModel === "string"
1615
- ? next.executors.pi.defaultModel
1616
- : undefined;
1665
+ const legacyModel = typeof next.model === "string" ? next.model : undefined;
1617
1666
  delete next.model;
1618
1667
  delete next.models;
1619
1668
  delete next.modelProfiles;
@@ -1622,7 +1671,6 @@ function migrateHarnessModelFields(harness) {
1622
1671
  delete executors.cursor;
1623
1672
  const pi = isRecord(executors.pi) ? { ...executors.pi } : {};
1624
1673
  delete pi.requiresApiKey;
1625
- delete pi.defaultModel;
1626
1674
  if (legacyModel) {
1627
1675
  for (const level of ["LOW", "MED", "HIGH"]) {
1628
1676
  if (typeof pi[level] !== "string")
@@ -1778,7 +1826,9 @@ async function collectRetiredLayoutActions(input) {
1778
1826
  }
1779
1827
  async function resolveInitProjectContext(input) {
1780
1828
  const harness = await readJsonIfExists(path.join(input.repoRoot, "harness.json"));
1781
- const recordedGovernanceRoot = typeof harness.governanceRoot === "string" ? harness.governanceRoot : undefined;
1829
+ const recordedGovernanceRoot = typeof harness.governanceRoot === "string"
1830
+ ? harness.governanceRoot
1831
+ : undefined;
1782
1832
  return {
1783
1833
  projectName: input.projectName ??
1784
1834
  (typeof harness.project === "string" ? harness.project : undefined) ??
@@ -1825,7 +1875,7 @@ function buildManagedReadmeBlock(input) {
1825
1875
  "默认使用 Agent DAG 作为实现工作流:",
1826
1876
  "",
1827
1877
  "```bash",
1828
- "loop-agent new-task <task-id> \"任务标题\"",
1878
+ 'loop-agent new-task <task-id> "任务标题"',
1829
1879
  "# write .harness/tasks/<task-id>/source/需求.md",
1830
1880
  "# write .harness/tasks/<task-id>/source/执行约束.md",
1831
1881
  "loop-agent dag run-task <task-id> --profile auto --strict-models",
@@ -2014,7 +2064,7 @@ function buildTargetDevelopmentPrinciples(input) {
2014
2064
  "Principle 1 covers **granularity** (one bounded block). This section covers **shape**: each slice should cross the real integration layers the work needs and leave an independently verifiable narrow loop.",
2015
2065
  "",
2016
2066
  "- Every slice needs its own acceptance criteria, verification commands, and failure conditions.",
2017
- "- Prefer vertical tracer bullets over horizontal layering. Paths like \"schema → API → UI → tests\" are *possible* examples only; do not assume every project has those layers.",
2067
+ '- Prefer vertical tracer bullets over horizontal layering. Paths like "schema → API → UI → tests" are *possible* examples only; do not assume every project has those layers.',
2018
2068
  "- Horizontal anti-patterns: finish all of one layer before the next; or write every test first, then implement everything.",
2019
2069
  "- For behavior changes, use one failing test → minimal implementation → green → next behavior. Do not batch all RED then all GREEN.",
2020
2070
  "- Split large features into multiple independently runnable tasks/DAGs instead of one oversized writer across every layer.",
@@ -2057,7 +2107,7 @@ function buildTargetFeatureWorkflow(input) {
2057
2107
  "## Agent DAG Path",
2058
2108
  "",
2059
2109
  "```bash",
2060
- "loop-agent new-task <task-id> \"Task title\"",
2110
+ 'loop-agent new-task <task-id> "Task title"',
2061
2111
  "# write .harness/tasks/<task-id>/source/需求.md",
2062
2112
  "# write .harness/tasks/<task-id>/source/执行约束.md",
2063
2113
  "loop-agent dag run-task <task-id> --profile auto --strict-models",
@@ -2083,11 +2133,11 @@ function buildTargetFeatureWorkflow(input) {
2083
2133
  "",
2084
2134
  "Set an explicit specialized `taskKind` in `.harness/tasks/<task-id>/task.json` only when the dedicated workflow itself is part of the task contract:",
2085
2135
  "",
2086
- "- `taskKind: \"frontend-implementation\"` explicitly selects the frontend DAG for compatibility or intentional override. Optional `frontendMock` config sets `policy: auto|required|disabled`, an existing `serviceRoot`, and generation-time-frozen `verifyCommands`; an unsafe or incomplete explicit required contract produces an assessment-only DAG with no writer, while a complete required contract adds Mock-specific verification only when trusted commands exist. Auto API tasks without a native service may use an existing browser interception harness or reversible request adapter, then continue through static and behavior verification.",
2087
- "- `taskKind: \"backend-test\"` selects the dedicated backend test DAG. Its Pi nodes analyze requirements, generate and review backend cases, generate pytest, and retrospect on results; shell gate/execution nodes enforce the review verdict and run the target project's pytest. The backend test templates (`backend-test-dag.json` and the `backend-test-dag.*.prompt.md` files) ship inside the loop-agent package as static references and are projected to target projects under the governance `templates/` directory.",
2088
- "- `taskKind: \"knowledge-sync\"` selects the Feature-scoped test-knowledge write-back DAG (collect → draft → validate → apply → pointer). Bind `featureId` in `task.json` (or hardConstraints / requirement text). It writes only under `features/<featureId>/…` after final verification evidence exists.",
2089
- "- `taskKind: \"knowledge-graph-bootstrap\"` selects the business knowledge-graph bootstrap DAG (preflight → inventory → propose → validate → review → gate → promote → materialize). AI writes only `knowledge/bootstrap/staging/**`; promote is merge-new-only.",
2090
- "- `taskKind: \"frontend-test\"` selects the FE-test RAG DAG. It writes a traceable frontend RAG package and Markdown case manifest, then executes manifest cases serially with `playwright-cli` in isolated test environments and retains per-case evidence. It never generates pytest or Playwright source code. `frontendTest.maxCasesPerBatch` defaults to 20 (maximum 50); optional `maxTokensPerCase` and `maxTotalTokens` stop only later cases after a completed case's token usage is recorded, marking them `blocked: token-budget-exhausted`. Generated browser startup uses `playwright-cli open --browser=chrome --headed <base-url>`; the generic playwright-cli skill is unchanged.",
2136
+ '- `taskKind: "frontend-implementation"` explicitly selects the frontend DAG for compatibility or intentional override. Optional `frontendMock` config sets `policy: auto|required|disabled`, an existing `serviceRoot`, and generation-time-frozen `verifyCommands`; an unsafe or incomplete explicit required contract produces an assessment-only DAG with no writer, while a complete required contract adds Mock-specific verification only when trusted commands exist. Auto API tasks without a native service may use an existing browser interception harness or reversible request adapter, then continue through static and behavior verification.',
2137
+ '- `taskKind: "backend-test"` selects the dedicated backend test DAG. Its Pi nodes analyze requirements, generate and review backend cases, generate pytest, and retrospect on results; shell gate/execution nodes enforce the review verdict and run the target project\'s pytest. The backend test templates (`backend-test-dag.json` and the `backend-test-dag.*.prompt.md` files) ship inside the loop-agent package as static references and are projected to target projects under the governance `templates/` directory.',
2138
+ '- `taskKind: "knowledge-sync"` selects the Feature-scoped test-knowledge write-back DAG (collect → draft → validate → apply → pointer). Bind `featureId` in `task.json` (or hardConstraints / requirement text). It writes only under `features/<featureId>/…` after final verification evidence exists.',
2139
+ '- `taskKind: "knowledge-graph-bootstrap"` selects the business knowledge-graph bootstrap DAG (preflight → inventory → propose → validate → review → gate → promote → materialize). AI writes only `knowledge/bootstrap/staging/**`; promote is merge-new-only.',
2140
+ '- `taskKind: "frontend-test"` selects the FE-test RAG DAG. It writes a traceable frontend RAG package and Markdown case manifest, then executes manifest cases serially with `playwright-cli` in isolated test environments and retains per-case evidence. It never generates pytest or Playwright source code. `frontendTest.maxCasesPerBatch` defaults to 20 (maximum 50); optional `maxTokensPerCase` and `maxTotalTokens` stop only later cases after a completed case\'s token usage is recorded, marking them `blocked: token-budget-exhausted`. Generated browser startup uses `playwright-cli open --browser=chrome --headed <base-url>`; the generic playwright-cli skill is unchanged.',
2091
2141
  "- Only eligible read-only Pi nodes (planner, scout, reviewer, verifier, closeout with no write-capable tool profile) receive the conservative automatic retry policy. Supervisor, implementer, writer, docs-only, dynamic, shell, static, and decision-gate nodes are not retried automatically. Eligible nodes cannot write repository files; the controller only records immutable attempt evidence under `.harness/dag-runs/<state>/<run-id>/<node-id>/attempt-<n>.json`.",
2092
2142
  "",
2093
2143
  "Use the package-backed public knowledge CLI for graph operations. Do not require target projects to run package-only kb runtime scripts:",
@@ -2098,14 +2148,14 @@ function buildTargetFeatureWorkflow(input) {
2098
2148
  "loop-agent dag run-task <task-id> # taskKind: knowledge-graph-bootstrap",
2099
2149
  "loop-agent knowledge query --mode by_feature --feature F-2026-004 --json",
2100
2150
  "loop-agent knowledge query --mode by_id --id SVC-order --json",
2101
- "loop-agent knowledge query --mode search --text \"keyword\" --json",
2151
+ 'loop-agent knowledge query --mode search --text "keyword" --json',
2102
2152
  "loop-agent knowledge graph-incremental-prepare --feature F-2026-004 --service <service>",
2103
2153
  "# review the prepared scope/staging; for a manual reviewed promotion:",
2104
2154
  "loop-agent knowledge graph-promote",
2105
2155
  "loop-agent knowledge graph-materialize",
2106
2156
  "```",
2107
2157
  "",
2108
- "Daily Feature test-knowledge write-back still uses `taskKind: \"knowledge-sync\"` with a bound `featureId`, separate from graph bootstrap/incremental entry points.",
2158
+ 'Daily Feature test-knowledge write-back still uses `taskKind: "knowledge-sync"` with a bound `featureId`, separate from graph bootstrap/incremental entry points.',
2109
2159
  "",
2110
2160
  "## Verification",
2111
2161
  "",
@@ -2205,11 +2255,16 @@ function buildGeneratedCoreDoc(input) {
2205
2255
  governanceRoot: input.governanceRoot,
2206
2256
  });
2207
2257
  case "feature-workflow.md":
2208
- return buildTargetFeatureWorkflow({ governanceRoot: input.governanceRoot });
2258
+ return buildTargetFeatureWorkflow({
2259
+ governanceRoot: input.governanceRoot,
2260
+ });
2209
2261
  case "verification-matrix.md":
2210
2262
  return buildTargetVerificationMatrix();
2211
2263
  case "loop-agent-harness.md":
2212
- return buildTargetLoopAgentHarness({ projectName: input.projectName, governanceRoot: input.governanceRoot });
2264
+ return buildTargetLoopAgentHarness({
2265
+ projectName: input.projectName,
2266
+ governanceRoot: input.governanceRoot,
2267
+ });
2213
2268
  default:
2214
2269
  return undefined;
2215
2270
  }
@@ -2267,7 +2322,7 @@ export function buildInitInstructions(input) {
2267
2322
  "",
2268
2323
  "## Apply Defaults",
2269
2324
  "",
2270
- "- Use the current loop-agent harness.json as the default template, but write target project name plus `adapter: \"loop-agent\"`.",
2325
+ '- Use the current loop-agent harness.json as the default template, but write target project name plus `adapter: "loop-agent"`.',
2271
2326
  `- ${DAG_HARD_GATE_TRIGGER}`,
2272
2327
  "- Generate the target project's loop-agent script matrix from templates: structure check, docs index/link checks, active plan status, exec-plan index sync, harness runtime cleanliness, architecture boundaries, skill entry integrity, governance CI, project-test CI, and full CI.",
2273
2328
  "- Copy or project only stack-agnostic governance scripts. For project-specific verification, packaging, release, or maintenance commands, generate the target-project version from templates plus the target repository's actual files instead of copying loop-agent's own TypeScript-specific scripts.",
@@ -2335,7 +2390,9 @@ export async function initializeLoopAgentProject(options) {
2335
2390
  const existingHarness = await readJsonIfExists(path.join(repoRoot, "harness.json"));
2336
2391
  await mkdir(repoRoot, { recursive: true });
2337
2392
  const readmePath = path.join(repoRoot, "README.md");
2338
- const existingReadme = (await exists(readmePath)) ? await readFile(readmePath, "utf-8") : undefined;
2393
+ const existingReadme = (await exists(readmePath))
2394
+ ? await readFile(readmePath, "utf-8")
2395
+ : undefined;
2339
2396
  const readmeContent = existingReadme
2340
2397
  ? mergeManagedBlock(existingReadme, buildManagedReadmeBlock({ projectName, governanceRoot }))
2341
2398
  : buildTargetReadme({ projectName, governanceRoot });
@@ -2364,7 +2421,9 @@ export async function initializeLoopAgentProject(options) {
2364
2421
  skipped,
2365
2422
  });
2366
2423
  const agentsPath = path.join(repoRoot, "AGENTS.md");
2367
- const existingAgents = (await exists(agentsPath)) ? await readFile(agentsPath, "utf-8") : `# AGENTS.md\n`;
2424
+ const existingAgents = (await exists(agentsPath))
2425
+ ? await readFile(agentsPath, "utf-8")
2426
+ : `# AGENTS.md\n`;
2368
2427
  await writeText({
2369
2428
  repoRoot,
2370
2429
  relativePath: "AGENTS.md",
@@ -2386,7 +2445,11 @@ export async function initializeLoopAgentProject(options) {
2386
2445
  skipped,
2387
2446
  });
2388
2447
  for (const doc of CORE_DOC_FILES) {
2389
- const generated = buildGeneratedCoreDoc({ doc: doc.target, projectName, governanceRoot });
2448
+ const generated = buildGeneratedCoreDoc({
2449
+ doc: doc.target,
2450
+ projectName,
2451
+ governanceRoot,
2452
+ });
2390
2453
  if (generated) {
2391
2454
  await writeTextIfMissing({
2392
2455
  repoRoot,
@@ -2465,7 +2528,12 @@ export async function initializeLoopAgentProject(options) {
2465
2528
  else if (clientRecoveryMode === "off") {
2466
2529
  skipped.push(OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH);
2467
2530
  }
2468
- await writeInitSurfaceState({ repoRoot, projectName, governanceRoot, stateKind: "recorded" });
2531
+ await writeInitSurfaceState({
2532
+ repoRoot,
2533
+ projectName,
2534
+ governanceRoot,
2535
+ stateKind: "recorded",
2536
+ });
2469
2537
  written.push(INIT_SURFACE_STATE_PATH);
2470
2538
  return {
2471
2539
  repoRoot,
@@ -2481,15 +2549,31 @@ function actionForMissing(pathName, state) {
2481
2549
  if (state.mode === "state")
2482
2550
  return undefined;
2483
2551
  if (state.mode === "directory") {
2484
- return { type: "create-directory", path: pathName, reason: "required init directory is missing" };
2552
+ return {
2553
+ type: "create-directory",
2554
+ path: pathName,
2555
+ reason: "required init directory is missing",
2556
+ };
2485
2557
  }
2486
2558
  if (state.mode === "copied") {
2487
- return { type: "copy-missing", path: pathName, reason: "required bundled init file is missing" };
2559
+ return {
2560
+ type: "copy-missing",
2561
+ path: pathName,
2562
+ reason: "required bundled init file is missing",
2563
+ };
2488
2564
  }
2489
2565
  if (state.mode === "generated") {
2490
- return { type: "write-generated-missing", path: pathName, reason: "required generated init file is missing" };
2566
+ return {
2567
+ type: "write-generated-missing",
2568
+ path: pathName,
2569
+ reason: "required generated init file is missing",
2570
+ };
2491
2571
  }
2492
- return { type: "refresh-managed-block", path: pathName, reason: "managed block file is missing or stale" };
2572
+ return {
2573
+ type: "refresh-managed-block",
2574
+ path: pathName,
2575
+ reason: "managed block file is missing or stale",
2576
+ };
2493
2577
  }
2494
2578
  function modelMergeTaskFor(pathName, state, allPaths) {
2495
2579
  return {
@@ -2639,7 +2723,8 @@ export async function checkInitUpdate(input) {
2639
2723
  if (recordedState &&
2640
2724
  isRecord(harness) &&
2641
2725
  governanceRoot !== "docs" &&
2642
- (harness.governanceRoot === "docs" || hasLegacyHarnessGovernancePaths(harness))) {
2726
+ (harness.governanceRoot === "docs" ||
2727
+ hasLegacyHarnessGovernancePaths(harness))) {
2643
2728
  deterministicActions.push({
2644
2729
  type: "migrate-harness-governance-root",
2645
2730
  path: "harness.json",
@@ -2709,7 +2794,9 @@ async function applySafeAction(input) {
2709
2794
  if (!(await exists(target)))
2710
2795
  return false;
2711
2796
  const harness = JSON.parse(await readFile(target, "utf-8"));
2712
- if (!isRecord(harness) || !isRecord(harness.executors) || !isRecord(harness.executors.pi)) {
2797
+ if (!isRecord(harness) ||
2798
+ !isRecord(harness.executors) ||
2799
+ !isRecord(harness.executors.pi)) {
2713
2800
  return false;
2714
2801
  }
2715
2802
  if (!Object.hasOwn(harness.executors.pi, "requiresApiKey")) {
@@ -2743,7 +2830,8 @@ async function applySafeAction(input) {
2743
2830
  return false;
2744
2831
  const harness = JSON.parse(await readFile(target, "utf-8"));
2745
2832
  if (!isRecord(harness) ||
2746
- (harness.governanceRoot !== "docs" && !hasLegacyHarnessGovernancePaths(harness))) {
2833
+ (harness.governanceRoot !== "docs" &&
2834
+ !hasLegacyHarnessGovernancePaths(harness))) {
2747
2835
  return false;
2748
2836
  }
2749
2837
  const next = {
@@ -2819,11 +2907,16 @@ async function applySafeAction(input) {
2819
2907
  return false;
2820
2908
  await mkdir(path.dirname(target), { recursive: true });
2821
2909
  if (input.action.type === "refresh-managed-block") {
2822
- const existing = (await exists(target)) ? await readFile(target, "utf-8") : "";
2910
+ const existing = (await exists(target))
2911
+ ? await readFile(target, "utf-8")
2912
+ : "";
2823
2913
  const next = input.action.path === ".gitignore"
2824
2914
  ? mergeGitignoreManagedBlock(existing, desired.content)
2825
2915
  : input.action.path === "README.md" && existing.trim().length === 0
2826
- ? buildTargetReadme({ projectName: input.projectName, governanceRoot: input.governanceRoot })
2916
+ ? buildTargetReadme({
2917
+ projectName: input.projectName,
2918
+ governanceRoot: input.governanceRoot,
2919
+ })
2827
2920
  : input.action.path === "AGENTS.md" && existing.trim().length === 0
2828
2921
  ? `# AGENTS.md\n\n${desired.content}\n`
2829
2922
  : mergeManagedBlock(existing, desired.content);
@@ -2850,7 +2943,12 @@ export async function applyInitUpdate(input) {
2850
2943
  const skipped = [];
2851
2944
  const clientRecoveryMode = input.clientRecovery ?? "auto";
2852
2945
  if (input.bootstrapSurface) {
2853
- await writeInitSurfaceState({ repoRoot, projectName, governanceRoot, stateKind: "inferred-baseline" });
2946
+ await writeInitSurfaceState({
2947
+ repoRoot,
2948
+ projectName,
2949
+ governanceRoot,
2950
+ stateKind: "inferred-baseline",
2951
+ });
2854
2952
  applied.push({
2855
2953
  type: "bootstrap-surface",
2856
2954
  path: INIT_SURFACE_STATE_PATH,
@@ -2861,7 +2959,9 @@ export async function applyInitUpdate(input) {
2861
2959
  const existingSurface = await readExistingSurfaceState(repoRoot);
2862
2960
  // Preserve source strength: recorded stays recorded so unchanged owned files
2863
2961
  // remain deterministic refresh candidates; bootstrap/inferred stays inferred.
2864
- const preservedStateKind = existingSurface?.stateKind === "recorded" ? "recorded" : "inferred-baseline";
2962
+ const preservedStateKind = existingSurface?.stateKind === "recorded"
2963
+ ? "recorded"
2964
+ : "inferred-baseline";
2865
2965
  const report = await checkInitUpdate({
2866
2966
  repoRoot,
2867
2967
  projectName,
@@ -2941,7 +3041,9 @@ function formatCheckUpdateMarkdown(report) {
2941
3041
  `- repoRoot: \`${report.repoRoot}\``,
2942
3042
  `- controllerVersion: \`${report.controllerVersion}\``,
2943
3043
  `- surfaceState: \`${report.surfaceState}\``,
2944
- ...(piLines.length > 0 ? ["", "## Client Recovery (Pi user config, read-only)", "", ...piLines] : []),
3044
+ ...(piLines.length > 0
3045
+ ? ["", "## Client Recovery (Pi user config, read-only)", "", ...piLines]
3046
+ : []),
2945
3047
  "",
2946
3048
  "## Deterministic Actions",
2947
3049
  "",
@@ -2978,21 +3080,29 @@ export async function runInitDoctor(input) {
2978
3080
  add("harness.json", false, error instanceof Error ? error.message : String(error));
2979
3081
  }
2980
3082
  const agents = path.join(repoRoot, "AGENTS.md");
2981
- add("AGENTS.md loop-agent block", (await exists(agents)) && (await readFile(agents, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
3083
+ add("AGENTS.md loop-agent block", (await exists(agents)) &&
3084
+ (await readFile(agents, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
2982
3085
  const readme = path.join(repoRoot, "README.md");
2983
- add("README loop-agent block", (await exists(readme)) && (await readFile(readme, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
3086
+ add("README loop-agent block", (await exists(readme)) &&
3087
+ (await readFile(readme, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
2984
3088
  add("repo-local skills", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
2985
3089
  const gitignorePath = path.join(repoRoot, ".gitignore");
2986
- const gitignoreContent = (await exists(gitignorePath)) ? await readFile(gitignorePath, "utf-8") : "";
2987
- add("gitignore loop-agent block", gitignoreContent.includes(GITIGNORE_BLOCK_START) && gitignoreContent.includes(".harness/tasks/*"), ".gitignore managed runtime ignores");
3090
+ const gitignoreContent = (await exists(gitignorePath))
3091
+ ? await readFile(gitignorePath, "utf-8")
3092
+ : "";
3093
+ add("gitignore loop-agent block", gitignoreContent.includes(GITIGNORE_BLOCK_START) &&
3094
+ gitignoreContent.includes(".harness/tasks/*"), ".gitignore managed runtime ignores");
2988
3095
  const requiredScripts = Object.keys(INIT_SCRIPT_FILES);
2989
3096
  const missingScripts = [];
2990
3097
  for (const script of requiredScripts) {
2991
3098
  if (!(await exists(path.join(repoRoot, script))))
2992
3099
  missingScripts.push(script);
2993
3100
  }
2994
- add("script matrix", missingScripts.length === 0, missingScripts.length === 0 ? `${requiredScripts.length} scripts` : `missing: ${missingScripts.join(", ")}`);
2995
- add("compat prompts", await exists(path.join(repoRoot, ".harness", "prompts", "analyze.md")) && await exists(path.join(repoRoot, ".harness", "prompts", "plan.md")), ".harness/prompts/analyze.md");
3101
+ add("script matrix", missingScripts.length === 0, missingScripts.length === 0
3102
+ ? `${requiredScripts.length} scripts`
3103
+ : `missing: ${missingScripts.join(", ")}`);
3104
+ add("compat prompts", (await exists(path.join(repoRoot, ".harness", "prompts", "analyze.md"))) &&
3105
+ (await exists(path.join(repoRoot, ".harness", "prompts", "plan.md"))), ".harness/prompts/analyze.md");
2996
3106
  add("harness runtime dirs", await exists(path.join(repoRoot, ".harness", "dag-runs", "active")), ".harness/dag-runs/active");
2997
3107
  return {
2998
3108
  ok: checks.every((check) => check.ok),
@@ -3032,7 +3142,12 @@ export async function runInitReconcile(input) {
3032
3142
  : update.report.modelMergeTasks.length > 0
3033
3143
  ? "needs-model-merge"
3034
3144
  : "needs-safe-update";
3035
- return { status, report: update.report, applied: update.applied, skipped: update.skipped };
3145
+ return {
3146
+ status,
3147
+ report: update.report,
3148
+ applied: update.applied,
3149
+ skipped: update.skipped,
3150
+ };
3036
3151
  }
3037
3152
  function parseInitArgs(repoRoot, args) {
3038
3153
  let subcommand;
@@ -3049,7 +3164,12 @@ function parseInitArgs(repoRoot, args) {
3049
3164
  let applySafe = false;
3050
3165
  for (let i = 0; i < args.length; i += 1) {
3051
3166
  const arg = args[i];
3052
- if ((arg === "instructions" || arg === "doctor" || arg === "check-update" || arg === "update" || arg === "reconcile") && !subcommand) {
3167
+ if ((arg === "instructions" ||
3168
+ arg === "doctor" ||
3169
+ arg === "check-update" ||
3170
+ arg === "update" ||
3171
+ arg === "reconcile") &&
3172
+ !subcommand) {
3053
3173
  subcommand = arg;
3054
3174
  continue;
3055
3175
  }
@@ -3140,7 +3260,9 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3140
3260
  throw new Error("usage: init update [--bootstrap-surface] [--apply-safe]");
3141
3261
  }
3142
3262
  const result = await applyInitUpdate(parsed);
3143
- console.log(parsed.json ? JSON.stringify(result, null, 2) : formatInitUpdateResult(result));
3263
+ console.log(parsed.json
3264
+ ? JSON.stringify(result, null, 2)
3265
+ : formatInitUpdateResult(result));
3144
3266
  return;
3145
3267
  }
3146
3268
  if (parsed.subcommand === "reconcile") {
@@ -3148,7 +3270,9 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3148
3270
  ...parsed,
3149
3271
  readRuntimeActivity: dependencies.readRuntimeActivity,
3150
3272
  });
3151
- console.log(parsed.json ? JSON.stringify(result, null, 2) : formatReconcileResult(result));
3273
+ console.log(parsed.json
3274
+ ? JSON.stringify(result, null, 2)
3275
+ : formatReconcileResult(result));
3152
3276
  return;
3153
3277
  }
3154
3278
  const result = await initializeLoopAgentProject(parsed);