@tea-agent/loop-agent 0.25.3 → 0.25.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/CHANGELOG.md +15 -0
- package/dist/application/dag/args.js +21 -1
- package/dist/application/dag/run-dag.js +1 -0
- package/dist/cli/command-definitions.js +1 -1
- package/dist/cli/program.js +82 -63
- package/dist/commands/init.js +206 -82
- package/dist/commands/run-dag-progress.js +109 -0
- package/dist/commands/run-dag.js +16 -5
- package/dist/executors/shell-executor.js +32 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +88 -15
- package/dist/workflows/dag/backend-test-result-contract.js +35 -9
- package/dist/workflows/dag/frontend-test-case-checklist.js +71 -0
- package/dist/workflows/dag/frontend-test-html-report.js +77 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +44 -1
- package/dist/workflows/dag/init-hybrid.js +9 -9
- package/dist/workflows/dag/types.js +4 -0
- package/dist/workflows/dag/validate.js +3 -1
- package/docs/templates/agent-dag.schema.json +6 -0
- package/docs/templates/backend-test-dag.json +3 -3
- package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.json +31 -4
- package/harness.json +3 -2
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -1
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
{
|
|
20
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() +
|
|
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() +
|
|
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) &&
|
|
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) &&
|
|
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) &&
|
|
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("/")
|
|
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" &&
|
|
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"
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 = {
|
|
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
|
|
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()
|
|
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 &&
|
|
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" &&
|
|
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
|
|
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/") &&
|
|
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" ||
|
|
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 ||
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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))
|
|
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))
|
|
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({
|
|
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({
|
|
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 {
|
|
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 {
|
|
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 {
|
|
2566
|
+
return {
|
|
2567
|
+
type: "write-generated-missing",
|
|
2568
|
+
path: pathName,
|
|
2569
|
+
reason: "required generated init file is missing",
|
|
2570
|
+
};
|
|
2491
2571
|
}
|
|
2492
|
-
return {
|
|
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" ||
|
|
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) ||
|
|
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" &&
|
|
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))
|
|
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({
|
|
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({
|
|
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"
|
|
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
|
|
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)) &&
|
|
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)) &&
|
|
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))
|
|
2987
|
-
|
|
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
|
|
2995
|
-
|
|
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 {
|
|
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" ||
|
|
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
|
|
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
|
|
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);
|