@tea-agent/loop-agent 0.25.2 → 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/AGENTS.md +1 -0
- package/CHANGELOG.md +37 -0
- package/dist/application/dag/args.js +21 -1
- package/dist/application/dag/run-dag.js +1 -0
- package/dist/cli/command-definitions.js +2 -2
- package/dist/cli/program.js +84 -64
- package/dist/commands/client-recovery.js +657 -0
- package/dist/commands/init.js +283 -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/architecture/runtime-boundaries.md +13 -0
- package/docs/init-surface.manifest.json +6 -2
- 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/SKILL.md +2 -1
- package/skills/loop-agent/references/command-reference.md +2 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { isDeepStrictEqual } from "node:util";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { copyDir } from "../shared/copy-dir.js";
|
|
7
8
|
import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
|
|
8
9
|
import { loadHarnessManifest } from "../governance/harness.js";
|
|
10
|
+
import { OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, applyPiRetryMerge, buildOpenCodeTransientRetryPluginSource, inspectPiRetryConfig, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
|
|
9
11
|
const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
|
|
10
12
|
const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
|
|
11
13
|
const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
|
|
@@ -14,18 +16,36 @@ const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
|
|
|
14
16
|
const DEFAULT_GOVERNANCE_ROOT = "ai_workspace/loop-agent";
|
|
15
17
|
const CORE_DOC_FILES = [
|
|
16
18
|
{ source: "README.md", target: "README.md" },
|
|
17
|
-
{
|
|
18
|
-
|
|
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
|
+
},
|
|
19
27
|
{ source: "governance/feature-workflow.md", target: "feature-workflow.md" },
|
|
20
|
-
{
|
|
28
|
+
{
|
|
29
|
+
source: "governance/verification-matrix.md",
|
|
30
|
+
target: "verification-matrix.md",
|
|
31
|
+
},
|
|
21
32
|
{ source: "runtime/loop-agent-harness.md", target: "loop-agent-harness.md" },
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
},
|
|
25
45
|
{ source: "templates/README.md", target: "templates/README.md" },
|
|
26
46
|
];
|
|
27
47
|
function coreDocSourceForTarget(target) {
|
|
28
|
-
return CORE_DOC_FILES.find((entry) => entry.target === target)?.source ?? target;
|
|
48
|
+
return (CORE_DOC_FILES.find((entry) => entry.target === target)?.source ?? target);
|
|
29
49
|
}
|
|
30
50
|
const GOVERNANCE_README_DIRS = [
|
|
31
51
|
"decisions",
|
|
@@ -854,7 +874,7 @@ function buildManagedAgentsBlock(input) {
|
|
|
854
874
|
"### Agent DAG 路径",
|
|
855
875
|
"",
|
|
856
876
|
"```bash",
|
|
857
|
-
|
|
877
|
+
'loop-agent new-task <task-id> "任务标题"',
|
|
858
878
|
"# write .harness/tasks/<task-id>/source/需求.md",
|
|
859
879
|
"# write .harness/tasks/<task-id>/source/执行约束.md",
|
|
860
880
|
"loop-agent dag run-task <task-id> --profile auto --strict-models",
|
|
@@ -868,9 +888,12 @@ function buildManagedAgentsBlock(input) {
|
|
|
868
888
|
"",
|
|
869
889
|
"### 任务类型路由(taskKind)",
|
|
870
890
|
"",
|
|
871
|
-
|
|
891
|
+
'- 用户明确提出后端测试、接口/API 测试、pytest,或语境明确为后端的自动化测试时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。',
|
|
872
892
|
"- `backend-test` 是 `taskKind`,不是 `--profile` 的可选值;运行 `dag run-task` 时继续使用 `--profile auto`。",
|
|
873
893
|
"- 仅出现“自动化测试”且无法判断前后端时,先阅读任务源与目标项目技术栈再决定,禁止无条件路由到 `backend-test`。",
|
|
894
|
+
'- 用户提示词明确是前端实现需求(例如前端页面、UI、组件或交互开发)时,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"frontend-implementation"`,不得保留默认 `standard`。',
|
|
895
|
+
"- `frontend-implementation` 是 `taskKind`,不是 `--profile` 的可选值;运行 `dag run-task` 时继续使用 `--profile auto`,也可按需显式选择 `minimal` / `standard` / `reviewed` / `supervised`,不要把业务模板名当作 profile。",
|
|
896
|
+
'- 前端自动化测试(浏览器/UI 自动化、Playwright、E2E)继续使用 `taskKind: "frontend-test"`,不得设置为 `frontend-implementation`。',
|
|
874
897
|
"",
|
|
875
898
|
"### 运行看板(只读)",
|
|
876
899
|
"",
|
|
@@ -904,7 +927,8 @@ function mergeManagedBlock(existing, block) {
|
|
|
904
927
|
const start = existing.indexOf(MANAGED_BLOCK_START);
|
|
905
928
|
const end = existing.indexOf(MANAGED_BLOCK_END);
|
|
906
929
|
if (start >= 0 && end > start) {
|
|
907
|
-
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");
|
|
908
932
|
}
|
|
909
933
|
return `${existing.trimEnd()}\n\n${block}\n`;
|
|
910
934
|
}
|
|
@@ -912,7 +936,8 @@ function mergeGitignoreManagedBlock(existing, block) {
|
|
|
912
936
|
const start = existing.indexOf(GITIGNORE_BLOCK_START);
|
|
913
937
|
const end = existing.indexOf(GITIGNORE_BLOCK_END);
|
|
914
938
|
if (start >= 0 && end > start) {
|
|
915
|
-
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");
|
|
916
941
|
}
|
|
917
942
|
if (!existing.trim())
|
|
918
943
|
return `${block}\n`;
|
|
@@ -1051,19 +1076,22 @@ function buildHarness(input) {
|
|
|
1051
1076
|
// Do not project legacy step-routing models / modelProfiles / modelRouting into
|
|
1052
1077
|
// new or refreshed harness.json. Keep them only when the target already has them
|
|
1053
1078
|
// so old files remain loadable until the user migrates.
|
|
1054
|
-
if (isRecord(input.existing.models) &&
|
|
1079
|
+
if (isRecord(input.existing.models) &&
|
|
1080
|
+
Object.keys(input.existing.models).length > 0) {
|
|
1055
1081
|
harness.models = input.existing.models;
|
|
1056
1082
|
}
|
|
1057
1083
|
else {
|
|
1058
1084
|
delete harness.models;
|
|
1059
1085
|
}
|
|
1060
|
-
if (isRecord(input.existing.modelProfiles) &&
|
|
1086
|
+
if (isRecord(input.existing.modelProfiles) &&
|
|
1087
|
+
Object.keys(input.existing.modelProfiles).length > 0) {
|
|
1061
1088
|
harness.modelProfiles = input.existing.modelProfiles;
|
|
1062
1089
|
}
|
|
1063
1090
|
else {
|
|
1064
1091
|
delete harness.modelProfiles;
|
|
1065
1092
|
}
|
|
1066
|
-
if (isRecord(input.existing.modelRouting) &&
|
|
1093
|
+
if (isRecord(input.existing.modelRouting) &&
|
|
1094
|
+
Object.keys(input.existing.modelRouting).length > 0) {
|
|
1067
1095
|
harness.modelRouting = input.existing.modelRouting;
|
|
1068
1096
|
}
|
|
1069
1097
|
else {
|
|
@@ -1110,7 +1138,9 @@ async function copyDirMerge(input) {
|
|
|
1110
1138
|
const source = path.join(input.assetRoot, input.sourceRelativePath);
|
|
1111
1139
|
const target = path.join(input.repoRoot, input.targetRelativePath);
|
|
1112
1140
|
await copyDir(source, target);
|
|
1113
|
-
input.written.push(input.targetRelativePath.endsWith("/")
|
|
1141
|
+
input.written.push(input.targetRelativePath.endsWith("/")
|
|
1142
|
+
? input.targetRelativePath
|
|
1143
|
+
: `${input.targetRelativePath}/`);
|
|
1114
1144
|
}
|
|
1115
1145
|
async function ensureHarnessDirs(repoRoot, written) {
|
|
1116
1146
|
const dirs = [
|
|
@@ -1187,7 +1217,8 @@ function manifestPathToTargetPath(relativePath, governanceRoot) {
|
|
|
1187
1217
|
return relativePath;
|
|
1188
1218
|
}
|
|
1189
1219
|
function targetPathToManifestPath(relativePath, governanceRoot) {
|
|
1190
|
-
if (governanceRoot !== "docs" &&
|
|
1220
|
+
if (governanceRoot !== "docs" &&
|
|
1221
|
+
relativePath.startsWith(`${governanceRoot}/`)) {
|
|
1191
1222
|
return `docs/${relativePath.slice(`${governanceRoot}/`.length)}`;
|
|
1192
1223
|
}
|
|
1193
1224
|
if (relativePath.startsWith(".agents/skills/")) {
|
|
@@ -1197,7 +1228,9 @@ function targetPathToManifestPath(relativePath, governanceRoot) {
|
|
|
1197
1228
|
}
|
|
1198
1229
|
async function readPackageVersion(assetRoot) {
|
|
1199
1230
|
const packageJson = JSON.parse(await readFile(path.join(assetRoot, "package.json"), "utf-8"));
|
|
1200
|
-
return typeof packageJson.version === "string"
|
|
1231
|
+
return typeof packageJson.version === "string"
|
|
1232
|
+
? packageJson.version
|
|
1233
|
+
: "0.0.0";
|
|
1201
1234
|
}
|
|
1202
1235
|
async function readInitSurfaceManifest(assetRoot) {
|
|
1203
1236
|
const manifestPath = path.join(assetRoot, "docs", "init-surface.manifest.json");
|
|
@@ -1275,6 +1308,7 @@ function inferInitSurfaceMode(relativePath) {
|
|
|
1275
1308
|
return "managed-block";
|
|
1276
1309
|
}
|
|
1277
1310
|
if (relativePath === "harness.json" ||
|
|
1311
|
+
relativePath === OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH ||
|
|
1278
1312
|
relativePath.startsWith("scripts/") ||
|
|
1279
1313
|
relativePath.startsWith(".harness/prompts/") ||
|
|
1280
1314
|
relativePath.startsWith("docs/README.md") ||
|
|
@@ -1321,6 +1355,9 @@ async function buildDesiredSurfaceContent(input) {
|
|
|
1321
1355
|
}), null, 2)}\n`,
|
|
1322
1356
|
};
|
|
1323
1357
|
}
|
|
1358
|
+
if (manifestPath === OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH) {
|
|
1359
|
+
return { content: buildOpenCodeTransientRetryPluginSource() };
|
|
1360
|
+
}
|
|
1324
1361
|
if (manifestPath.startsWith("scripts/")) {
|
|
1325
1362
|
const scripts = buildInitScriptFiles(input.governanceRoot);
|
|
1326
1363
|
return { content: scripts[manifestPath] };
|
|
@@ -1335,7 +1372,10 @@ async function buildDesiredSurfaceContent(input) {
|
|
|
1335
1372
|
const sourceManifestPath = manifestPath.slice(".agents/".length);
|
|
1336
1373
|
const mirrorSourcePath = path.join(input.assetRoot, sourceManifestPath);
|
|
1337
1374
|
if (await exists(mirrorSourcePath)) {
|
|
1338
|
-
return {
|
|
1375
|
+
return {
|
|
1376
|
+
content: await readFile(mirrorSourcePath, "utf-8"),
|
|
1377
|
+
sourcePath: mirrorSourcePath,
|
|
1378
|
+
};
|
|
1339
1379
|
}
|
|
1340
1380
|
return {};
|
|
1341
1381
|
}
|
|
@@ -1350,7 +1390,10 @@ async function buildDesiredSurfaceContent(input) {
|
|
|
1350
1390
|
return { content: generated };
|
|
1351
1391
|
const mappedSourcePath = path.join(input.assetRoot, "docs", coreDocSourceForTarget(doc));
|
|
1352
1392
|
if (await exists(mappedSourcePath)) {
|
|
1353
|
-
return {
|
|
1393
|
+
return {
|
|
1394
|
+
content: await readFile(mappedSourcePath, "utf-8"),
|
|
1395
|
+
sourcePath: mappedSourcePath,
|
|
1396
|
+
};
|
|
1354
1397
|
}
|
|
1355
1398
|
}
|
|
1356
1399
|
const sourcePath = path.join(input.assetRoot, manifestPath);
|
|
@@ -1392,7 +1435,10 @@ export async function readRecordedSurfaceControllerVersion(repoRoot) {
|
|
|
1392
1435
|
}
|
|
1393
1436
|
if (!state || typeof state.controllerVersion !== "string")
|
|
1394
1437
|
return undefined;
|
|
1395
|
-
return {
|
|
1438
|
+
return {
|
|
1439
|
+
controllerVersion: state.controllerVersion,
|
|
1440
|
+
stateKind: state.stateKind,
|
|
1441
|
+
};
|
|
1396
1442
|
}
|
|
1397
1443
|
async function buildCurrentSurfaceState(input) {
|
|
1398
1444
|
const assetRoot = await findPackageRoot();
|
|
@@ -1401,7 +1447,10 @@ async function buildCurrentSurfaceState(input) {
|
|
|
1401
1447
|
const files = {};
|
|
1402
1448
|
for (const manifestEntry of manifest.entries) {
|
|
1403
1449
|
const targetRelativePath = normalizeRelativePath(manifestPathToTargetPath(manifestEntry.path, input.governanceRoot));
|
|
1404
|
-
const entry = {
|
|
1450
|
+
const entry = {
|
|
1451
|
+
...manifestEntry,
|
|
1452
|
+
path: targetRelativePath,
|
|
1453
|
+
};
|
|
1405
1454
|
const targetPath = path.join(input.repoRoot, targetRelativePath);
|
|
1406
1455
|
const targetStat = await stat(targetPath).catch(() => null);
|
|
1407
1456
|
const desired = await buildDesiredSurfaceContent({
|
|
@@ -1416,7 +1465,9 @@ async function buildCurrentSurfaceState(input) {
|
|
|
1416
1465
|
status: targetStat ? "present" : "missing",
|
|
1417
1466
|
relationship: "missing-from-target",
|
|
1418
1467
|
sourceSha256,
|
|
1419
|
-
sourcePath: desired.sourcePath
|
|
1468
|
+
sourcePath: desired.sourcePath
|
|
1469
|
+
? repoRelative(assetRoot, desired.sourcePath)
|
|
1470
|
+
: undefined,
|
|
1420
1471
|
mode: entry.mode,
|
|
1421
1472
|
};
|
|
1422
1473
|
if (entry.mode === "state") {
|
|
@@ -1434,7 +1485,9 @@ async function buildCurrentSurfaceState(input) {
|
|
|
1434
1485
|
if (entry.mode === "directory") {
|
|
1435
1486
|
files[targetRelativePath] = {
|
|
1436
1487
|
...base,
|
|
1437
|
-
relationship: targetStat.isDirectory()
|
|
1488
|
+
relationship: targetStat.isDirectory()
|
|
1489
|
+
? "directory-present"
|
|
1490
|
+
: "local-existing-unknown",
|
|
1438
1491
|
};
|
|
1439
1492
|
continue;
|
|
1440
1493
|
}
|
|
@@ -1448,14 +1501,17 @@ async function buildCurrentSurfaceState(input) {
|
|
|
1448
1501
|
files[targetRelativePath] = {
|
|
1449
1502
|
...base,
|
|
1450
1503
|
currentSha256,
|
|
1451
|
-
relationship: currentBlock &&
|
|
1504
|
+
relationship: currentBlock &&
|
|
1505
|
+
sourceSha256 &&
|
|
1506
|
+
sha256Text(currentBlock) === sourceSha256
|
|
1452
1507
|
? "managed-block-current"
|
|
1453
1508
|
: "managed-block-present",
|
|
1454
1509
|
};
|
|
1455
1510
|
continue;
|
|
1456
1511
|
}
|
|
1457
1512
|
let semanticallyMatchesGeneratedJson = false;
|
|
1458
|
-
if (targetRelativePath === "harness.json" &&
|
|
1513
|
+
if (targetRelativePath === "harness.json" &&
|
|
1514
|
+
desired.content !== undefined) {
|
|
1459
1515
|
try {
|
|
1460
1516
|
semanticallyMatchesGeneratedJson = isDeepStrictEqual(JSON.parse(current.toString("utf-8")), JSON.parse(desired.content));
|
|
1461
1517
|
}
|
|
@@ -1529,7 +1585,9 @@ async function listRelativeFiles(rootPath) {
|
|
|
1529
1585
|
const files = [];
|
|
1530
1586
|
async function walk(currentPath, relativeDir) {
|
|
1531
1587
|
for (const entry of await readdir(currentPath, { withFileTypes: true })) {
|
|
1532
|
-
const relativePath = relativeDir
|
|
1588
|
+
const relativePath = relativeDir
|
|
1589
|
+
? `${relativeDir}/${entry.name}`
|
|
1590
|
+
: entry.name;
|
|
1533
1591
|
const absolutePath = path.join(currentPath, entry.name);
|
|
1534
1592
|
if (entry.isDirectory())
|
|
1535
1593
|
await walk(absolutePath, relativePath);
|
|
@@ -1558,7 +1616,8 @@ async function expectedLegacyInitFileSha(input) {
|
|
|
1558
1616
|
}));
|
|
1559
1617
|
}
|
|
1560
1618
|
}
|
|
1561
|
-
if (!input.legacyPath.startsWith("docs/") &&
|
|
1619
|
+
if (!input.legacyPath.startsWith("docs/") &&
|
|
1620
|
+
!input.legacyPath.startsWith("skills/")) {
|
|
1562
1621
|
return undefined;
|
|
1563
1622
|
}
|
|
1564
1623
|
if (input.legacyPath.startsWith("docs/")) {
|
|
@@ -1572,7 +1631,10 @@ function collectSafeRetiredDirectories(paths) {
|
|
|
1572
1631
|
for (const retiredPath of paths) {
|
|
1573
1632
|
let dir = path.posix.dirname(retiredPath);
|
|
1574
1633
|
while (dir !== "." && dir !== "/") {
|
|
1575
|
-
if (dir === "skills" ||
|
|
1634
|
+
if (dir === "skills" ||
|
|
1635
|
+
dir.startsWith("skills/") ||
|
|
1636
|
+
dir === "docs" ||
|
|
1637
|
+
dir.startsWith("docs/")) {
|
|
1576
1638
|
dirs.add(dir);
|
|
1577
1639
|
}
|
|
1578
1640
|
dir = path.posix.dirname(dir);
|
|
@@ -1583,28 +1645,24 @@ function collectSafeRetiredDirectories(paths) {
|
|
|
1583
1645
|
return [...dirs].sort((left, right) => right.split("/").length - left.split("/").length);
|
|
1584
1646
|
}
|
|
1585
1647
|
function needsHarnessModelMigration(harness) {
|
|
1586
|
-
if ("model" in harness ||
|
|
1648
|
+
if ("model" in harness ||
|
|
1649
|
+
"models" in harness ||
|
|
1650
|
+
"modelProfiles" in harness ||
|
|
1651
|
+
"modelRouting" in harness) {
|
|
1587
1652
|
return true;
|
|
1588
1653
|
}
|
|
1589
1654
|
if (isRecord(harness.executors)) {
|
|
1590
1655
|
if ("cursor" in harness.executors)
|
|
1591
1656
|
return true;
|
|
1592
1657
|
if (isRecord(harness.executors.pi)) {
|
|
1593
|
-
return
|
|
1594
|
-
"defaultModel" in harness.executors.pi);
|
|
1658
|
+
return "requiresApiKey" in harness.executors.pi;
|
|
1595
1659
|
}
|
|
1596
1660
|
}
|
|
1597
1661
|
return false;
|
|
1598
1662
|
}
|
|
1599
1663
|
function migrateHarnessModelFields(harness) {
|
|
1600
1664
|
const next = { ...harness };
|
|
1601
|
-
const legacyModel = typeof next.model === "string"
|
|
1602
|
-
? next.model
|
|
1603
|
-
: isRecord(next.executors) &&
|
|
1604
|
-
isRecord(next.executors.pi) &&
|
|
1605
|
-
typeof next.executors.pi.defaultModel === "string"
|
|
1606
|
-
? next.executors.pi.defaultModel
|
|
1607
|
-
: undefined;
|
|
1665
|
+
const legacyModel = typeof next.model === "string" ? next.model : undefined;
|
|
1608
1666
|
delete next.model;
|
|
1609
1667
|
delete next.models;
|
|
1610
1668
|
delete next.modelProfiles;
|
|
@@ -1613,7 +1671,6 @@ function migrateHarnessModelFields(harness) {
|
|
|
1613
1671
|
delete executors.cursor;
|
|
1614
1672
|
const pi = isRecord(executors.pi) ? { ...executors.pi } : {};
|
|
1615
1673
|
delete pi.requiresApiKey;
|
|
1616
|
-
delete pi.defaultModel;
|
|
1617
1674
|
if (legacyModel) {
|
|
1618
1675
|
for (const level of ["LOW", "MED", "HIGH"]) {
|
|
1619
1676
|
if (typeof pi[level] !== "string")
|
|
@@ -1769,7 +1826,9 @@ async function collectRetiredLayoutActions(input) {
|
|
|
1769
1826
|
}
|
|
1770
1827
|
async function resolveInitProjectContext(input) {
|
|
1771
1828
|
const harness = await readJsonIfExists(path.join(input.repoRoot, "harness.json"));
|
|
1772
|
-
const recordedGovernanceRoot = typeof harness.governanceRoot === "string"
|
|
1829
|
+
const recordedGovernanceRoot = typeof harness.governanceRoot === "string"
|
|
1830
|
+
? harness.governanceRoot
|
|
1831
|
+
: undefined;
|
|
1773
1832
|
return {
|
|
1774
1833
|
projectName: input.projectName ??
|
|
1775
1834
|
(typeof harness.project === "string" ? harness.project : undefined) ??
|
|
@@ -1816,7 +1875,7 @@ function buildManagedReadmeBlock(input) {
|
|
|
1816
1875
|
"默认使用 Agent DAG 作为实现工作流:",
|
|
1817
1876
|
"",
|
|
1818
1877
|
"```bash",
|
|
1819
|
-
|
|
1878
|
+
'loop-agent new-task <task-id> "任务标题"',
|
|
1820
1879
|
"# write .harness/tasks/<task-id>/source/需求.md",
|
|
1821
1880
|
"# write .harness/tasks/<task-id>/source/执行约束.md",
|
|
1822
1881
|
"loop-agent dag run-task <task-id> --profile auto --strict-models",
|
|
@@ -2005,7 +2064,7 @@ function buildTargetDevelopmentPrinciples(input) {
|
|
|
2005
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.",
|
|
2006
2065
|
"",
|
|
2007
2066
|
"- Every slice needs its own acceptance criteria, verification commands, and failure conditions.",
|
|
2008
|
-
|
|
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.',
|
|
2009
2068
|
"- Horizontal anti-patterns: finish all of one layer before the next; or write every test first, then implement everything.",
|
|
2010
2069
|
"- For behavior changes, use one failing test → minimal implementation → green → next behavior. Do not batch all RED then all GREEN.",
|
|
2011
2070
|
"- Split large features into multiple independently runnable tasks/DAGs instead of one oversized writer across every layer.",
|
|
@@ -2048,7 +2107,7 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
2048
2107
|
"## Agent DAG Path",
|
|
2049
2108
|
"",
|
|
2050
2109
|
"```bash",
|
|
2051
|
-
|
|
2110
|
+
'loop-agent new-task <task-id> "Task title"',
|
|
2052
2111
|
"# write .harness/tasks/<task-id>/source/需求.md",
|
|
2053
2112
|
"# write .harness/tasks/<task-id>/source/执行约束.md",
|
|
2054
2113
|
"loop-agent dag run-task <task-id> --profile auto --strict-models",
|
|
@@ -2074,11 +2133,11 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
2074
2133
|
"",
|
|
2075
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:",
|
|
2076
2135
|
"",
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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.',
|
|
2082
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`.",
|
|
2083
2142
|
"",
|
|
2084
2143
|
"Use the package-backed public knowledge CLI for graph operations. Do not require target projects to run package-only kb runtime scripts:",
|
|
@@ -2089,14 +2148,14 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
2089
2148
|
"loop-agent dag run-task <task-id> # taskKind: knowledge-graph-bootstrap",
|
|
2090
2149
|
"loop-agent knowledge query --mode by_feature --feature F-2026-004 --json",
|
|
2091
2150
|
"loop-agent knowledge query --mode by_id --id SVC-order --json",
|
|
2092
|
-
|
|
2151
|
+
'loop-agent knowledge query --mode search --text "keyword" --json',
|
|
2093
2152
|
"loop-agent knowledge graph-incremental-prepare --feature F-2026-004 --service <service>",
|
|
2094
2153
|
"# review the prepared scope/staging; for a manual reviewed promotion:",
|
|
2095
2154
|
"loop-agent knowledge graph-promote",
|
|
2096
2155
|
"loop-agent knowledge graph-materialize",
|
|
2097
2156
|
"```",
|
|
2098
2157
|
"",
|
|
2099
|
-
|
|
2158
|
+
'Daily Feature test-knowledge write-back still uses `taskKind: "knowledge-sync"` with a bound `featureId`, separate from graph bootstrap/incremental entry points.',
|
|
2100
2159
|
"",
|
|
2101
2160
|
"## Verification",
|
|
2102
2161
|
"",
|
|
@@ -2196,11 +2255,16 @@ function buildGeneratedCoreDoc(input) {
|
|
|
2196
2255
|
governanceRoot: input.governanceRoot,
|
|
2197
2256
|
});
|
|
2198
2257
|
case "feature-workflow.md":
|
|
2199
|
-
return buildTargetFeatureWorkflow({
|
|
2258
|
+
return buildTargetFeatureWorkflow({
|
|
2259
|
+
governanceRoot: input.governanceRoot,
|
|
2260
|
+
});
|
|
2200
2261
|
case "verification-matrix.md":
|
|
2201
2262
|
return buildTargetVerificationMatrix();
|
|
2202
2263
|
case "loop-agent-harness.md":
|
|
2203
|
-
return buildTargetLoopAgentHarness({
|
|
2264
|
+
return buildTargetLoopAgentHarness({
|
|
2265
|
+
projectName: input.projectName,
|
|
2266
|
+
governanceRoot: input.governanceRoot,
|
|
2267
|
+
});
|
|
2204
2268
|
default:
|
|
2205
2269
|
return undefined;
|
|
2206
2270
|
}
|
|
@@ -2258,7 +2322,7 @@ export function buildInitInstructions(input) {
|
|
|
2258
2322
|
"",
|
|
2259
2323
|
"## Apply Defaults",
|
|
2260
2324
|
"",
|
|
2261
|
-
|
|
2325
|
+
'- Use the current loop-agent harness.json as the default template, but write target project name plus `adapter: "loop-agent"`.',
|
|
2262
2326
|
`- ${DAG_HARD_GATE_TRIGGER}`,
|
|
2263
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.",
|
|
2264
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.",
|
|
@@ -2326,7 +2390,9 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2326
2390
|
const existingHarness = await readJsonIfExists(path.join(repoRoot, "harness.json"));
|
|
2327
2391
|
await mkdir(repoRoot, { recursive: true });
|
|
2328
2392
|
const readmePath = path.join(repoRoot, "README.md");
|
|
2329
|
-
const existingReadme = (await exists(readmePath))
|
|
2393
|
+
const existingReadme = (await exists(readmePath))
|
|
2394
|
+
? await readFile(readmePath, "utf-8")
|
|
2395
|
+
: undefined;
|
|
2330
2396
|
const readmeContent = existingReadme
|
|
2331
2397
|
? mergeManagedBlock(existingReadme, buildManagedReadmeBlock({ projectName, governanceRoot }))
|
|
2332
2398
|
: buildTargetReadme({ projectName, governanceRoot });
|
|
@@ -2355,7 +2421,9 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2355
2421
|
skipped,
|
|
2356
2422
|
});
|
|
2357
2423
|
const agentsPath = path.join(repoRoot, "AGENTS.md");
|
|
2358
|
-
const existingAgents = (await exists(agentsPath))
|
|
2424
|
+
const existingAgents = (await exists(agentsPath))
|
|
2425
|
+
? await readFile(agentsPath, "utf-8")
|
|
2426
|
+
: `# AGENTS.md\n`;
|
|
2359
2427
|
await writeText({
|
|
2360
2428
|
repoRoot,
|
|
2361
2429
|
relativePath: "AGENTS.md",
|
|
@@ -2377,7 +2445,11 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2377
2445
|
skipped,
|
|
2378
2446
|
});
|
|
2379
2447
|
for (const doc of CORE_DOC_FILES) {
|
|
2380
|
-
const generated = buildGeneratedCoreDoc({
|
|
2448
|
+
const generated = buildGeneratedCoreDoc({
|
|
2449
|
+
doc: doc.target,
|
|
2450
|
+
projectName,
|
|
2451
|
+
governanceRoot,
|
|
2452
|
+
});
|
|
2381
2453
|
if (generated) {
|
|
2382
2454
|
await writeTextIfMissing({
|
|
2383
2455
|
repoRoot,
|
|
@@ -2443,23 +2515,65 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2443
2515
|
}
|
|
2444
2516
|
await ensureHarnessDirs(repoRoot, written);
|
|
2445
2517
|
await writeCompatPrompts({ repoRoot, merge, written, skipped });
|
|
2446
|
-
|
|
2518
|
+
const clientRecoveryMode = options.clientRecovery ?? "auto";
|
|
2519
|
+
const clientRecovery = clientRecoveryMode === "off"
|
|
2520
|
+
? undefined
|
|
2521
|
+
: await runClientRecovery({
|
|
2522
|
+
repoRoot,
|
|
2523
|
+
mode: clientRecoveryMode,
|
|
2524
|
+
});
|
|
2525
|
+
if (clientRecovery?.plugin.written) {
|
|
2526
|
+
written.push(OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH);
|
|
2527
|
+
}
|
|
2528
|
+
else if (clientRecoveryMode === "off") {
|
|
2529
|
+
skipped.push(OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH);
|
|
2530
|
+
}
|
|
2531
|
+
await writeInitSurfaceState({
|
|
2532
|
+
repoRoot,
|
|
2533
|
+
projectName,
|
|
2534
|
+
governanceRoot,
|
|
2535
|
+
stateKind: "recorded",
|
|
2536
|
+
});
|
|
2447
2537
|
written.push(INIT_SURFACE_STATE_PATH);
|
|
2448
|
-
return {
|
|
2538
|
+
return {
|
|
2539
|
+
repoRoot,
|
|
2540
|
+
projectName,
|
|
2541
|
+
governanceRoot,
|
|
2542
|
+
profile,
|
|
2543
|
+
written,
|
|
2544
|
+
skipped,
|
|
2545
|
+
clientRecovery,
|
|
2546
|
+
};
|
|
2449
2547
|
}
|
|
2450
2548
|
function actionForMissing(pathName, state) {
|
|
2451
2549
|
if (state.mode === "state")
|
|
2452
2550
|
return undefined;
|
|
2453
2551
|
if (state.mode === "directory") {
|
|
2454
|
-
return {
|
|
2552
|
+
return {
|
|
2553
|
+
type: "create-directory",
|
|
2554
|
+
path: pathName,
|
|
2555
|
+
reason: "required init directory is missing",
|
|
2556
|
+
};
|
|
2455
2557
|
}
|
|
2456
2558
|
if (state.mode === "copied") {
|
|
2457
|
-
return {
|
|
2559
|
+
return {
|
|
2560
|
+
type: "copy-missing",
|
|
2561
|
+
path: pathName,
|
|
2562
|
+
reason: "required bundled init file is missing",
|
|
2563
|
+
};
|
|
2458
2564
|
}
|
|
2459
2565
|
if (state.mode === "generated") {
|
|
2460
|
-
return {
|
|
2566
|
+
return {
|
|
2567
|
+
type: "write-generated-missing",
|
|
2568
|
+
path: pathName,
|
|
2569
|
+
reason: "required generated init file is missing",
|
|
2570
|
+
};
|
|
2461
2571
|
}
|
|
2462
|
-
return {
|
|
2572
|
+
return {
|
|
2573
|
+
type: "refresh-managed-block",
|
|
2574
|
+
path: pathName,
|
|
2575
|
+
reason: "managed block file is missing or stale",
|
|
2576
|
+
};
|
|
2463
2577
|
}
|
|
2464
2578
|
function modelMergeTaskFor(pathName, state, allPaths) {
|
|
2465
2579
|
return {
|
|
@@ -2609,7 +2723,8 @@ export async function checkInitUpdate(input) {
|
|
|
2609
2723
|
if (recordedState &&
|
|
2610
2724
|
isRecord(harness) &&
|
|
2611
2725
|
governanceRoot !== "docs" &&
|
|
2612
|
-
(harness.governanceRoot === "docs" ||
|
|
2726
|
+
(harness.governanceRoot === "docs" ||
|
|
2727
|
+
hasLegacyHarnessGovernancePaths(harness))) {
|
|
2613
2728
|
deterministicActions.push({
|
|
2614
2729
|
type: "migrate-harness-governance-root",
|
|
2615
2730
|
path: "harness.json",
|
|
@@ -2642,12 +2757,19 @@ export async function checkInitUpdate(input) {
|
|
|
2642
2757
|
},
|
|
2643
2758
|
};
|
|
2644
2759
|
const recommendedNext = recommendedNextFor(partial);
|
|
2760
|
+
// Pi user config is never part of project surface hash; only report semantics.
|
|
2761
|
+
const piInspection = await inspectPiRetryConfig({
|
|
2762
|
+
homeDir: input.homeDir,
|
|
2763
|
+
});
|
|
2645
2764
|
return {
|
|
2646
2765
|
...partial,
|
|
2647
2766
|
ok: deterministicActions.length === 0 &&
|
|
2648
2767
|
modelMergeTasks.length === 0 &&
|
|
2649
2768
|
humanDecisions.length === 0,
|
|
2650
2769
|
recommendedNext,
|
|
2770
|
+
clientRecovery: {
|
|
2771
|
+
pi: piInspection,
|
|
2772
|
+
},
|
|
2651
2773
|
};
|
|
2652
2774
|
}
|
|
2653
2775
|
async function applySafeAction(input) {
|
|
@@ -2672,7 +2794,9 @@ async function applySafeAction(input) {
|
|
|
2672
2794
|
if (!(await exists(target)))
|
|
2673
2795
|
return false;
|
|
2674
2796
|
const harness = JSON.parse(await readFile(target, "utf-8"));
|
|
2675
|
-
if (!isRecord(harness) ||
|
|
2797
|
+
if (!isRecord(harness) ||
|
|
2798
|
+
!isRecord(harness.executors) ||
|
|
2799
|
+
!isRecord(harness.executors.pi)) {
|
|
2676
2800
|
return false;
|
|
2677
2801
|
}
|
|
2678
2802
|
if (!Object.hasOwn(harness.executors.pi, "requiresApiKey")) {
|
|
@@ -2706,7 +2830,8 @@ async function applySafeAction(input) {
|
|
|
2706
2830
|
return false;
|
|
2707
2831
|
const harness = JSON.parse(await readFile(target, "utf-8"));
|
|
2708
2832
|
if (!isRecord(harness) ||
|
|
2709
|
-
(harness.governanceRoot !== "docs" &&
|
|
2833
|
+
(harness.governanceRoot !== "docs" &&
|
|
2834
|
+
!hasLegacyHarnessGovernancePaths(harness))) {
|
|
2710
2835
|
return false;
|
|
2711
2836
|
}
|
|
2712
2837
|
const next = {
|
|
@@ -2782,11 +2907,16 @@ async function applySafeAction(input) {
|
|
|
2782
2907
|
return false;
|
|
2783
2908
|
await mkdir(path.dirname(target), { recursive: true });
|
|
2784
2909
|
if (input.action.type === "refresh-managed-block") {
|
|
2785
|
-
const existing = (await exists(target))
|
|
2910
|
+
const existing = (await exists(target))
|
|
2911
|
+
? await readFile(target, "utf-8")
|
|
2912
|
+
: "";
|
|
2786
2913
|
const next = input.action.path === ".gitignore"
|
|
2787
2914
|
? mergeGitignoreManagedBlock(existing, desired.content)
|
|
2788
2915
|
: input.action.path === "README.md" && existing.trim().length === 0
|
|
2789
|
-
? buildTargetReadme({
|
|
2916
|
+
? buildTargetReadme({
|
|
2917
|
+
projectName: input.projectName,
|
|
2918
|
+
governanceRoot: input.governanceRoot,
|
|
2919
|
+
})
|
|
2790
2920
|
: input.action.path === "AGENTS.md" && existing.trim().length === 0
|
|
2791
2921
|
? `# AGENTS.md\n\n${desired.content}\n`
|
|
2792
2922
|
: mergeManagedBlock(existing, desired.content);
|
|
@@ -2811,8 +2941,14 @@ export async function applyInitUpdate(input) {
|
|
|
2811
2941
|
});
|
|
2812
2942
|
const applied = [];
|
|
2813
2943
|
const skipped = [];
|
|
2944
|
+
const clientRecoveryMode = input.clientRecovery ?? "auto";
|
|
2814
2945
|
if (input.bootstrapSurface) {
|
|
2815
|
-
await writeInitSurfaceState({
|
|
2946
|
+
await writeInitSurfaceState({
|
|
2947
|
+
repoRoot,
|
|
2948
|
+
projectName,
|
|
2949
|
+
governanceRoot,
|
|
2950
|
+
stateKind: "inferred-baseline",
|
|
2951
|
+
});
|
|
2816
2952
|
applied.push({
|
|
2817
2953
|
type: "bootstrap-surface",
|
|
2818
2954
|
path: INIT_SURFACE_STATE_PATH,
|
|
@@ -2823,8 +2959,16 @@ export async function applyInitUpdate(input) {
|
|
|
2823
2959
|
const existingSurface = await readExistingSurfaceState(repoRoot);
|
|
2824
2960
|
// Preserve source strength: recorded stays recorded so unchanged owned files
|
|
2825
2961
|
// remain deterministic refresh candidates; bootstrap/inferred stays inferred.
|
|
2826
|
-
const preservedStateKind = existingSurface?.stateKind === "recorded"
|
|
2827
|
-
|
|
2962
|
+
const preservedStateKind = existingSurface?.stateKind === "recorded"
|
|
2963
|
+
? "recorded"
|
|
2964
|
+
: "inferred-baseline";
|
|
2965
|
+
const report = await checkInitUpdate({
|
|
2966
|
+
repoRoot,
|
|
2967
|
+
projectName,
|
|
2968
|
+
governanceRoot,
|
|
2969
|
+
clientRecovery: clientRecoveryMode,
|
|
2970
|
+
homeDir: input.homeDir,
|
|
2971
|
+
});
|
|
2828
2972
|
for (const action of report.deterministicActions) {
|
|
2829
2973
|
if (action.type === "bootstrap-surface") {
|
|
2830
2974
|
skipped.push(action);
|
|
@@ -2846,12 +2990,36 @@ export async function applyInitUpdate(input) {
|
|
|
2846
2990
|
preserveOwnershipFrom: existingSurface,
|
|
2847
2991
|
});
|
|
2848
2992
|
}
|
|
2993
|
+
// Pi user config is only mutated with explicit --client-recovery=user.
|
|
2994
|
+
// Do not reinstall the project plugin here — that would bypass ownership and
|
|
2995
|
+
// clobber user-modified plugins; plugin updates stay on deterministic actions.
|
|
2996
|
+
if (clientRecoveryMode === "user") {
|
|
2997
|
+
await applyPiRetryMerge({
|
|
2998
|
+
homeDir: input.homeDir ?? os.homedir(),
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
2849
3001
|
return {
|
|
2850
3002
|
applied,
|
|
2851
3003
|
skipped,
|
|
2852
|
-
report: await checkInitUpdate({
|
|
3004
|
+
report: await checkInitUpdate({
|
|
3005
|
+
repoRoot,
|
|
3006
|
+
projectName,
|
|
3007
|
+
governanceRoot,
|
|
3008
|
+
clientRecovery: clientRecoveryMode,
|
|
3009
|
+
homeDir: input.homeDir,
|
|
3010
|
+
}),
|
|
2853
3011
|
};
|
|
2854
3012
|
}
|
|
3013
|
+
function formatClientRecoverySummary(report) {
|
|
3014
|
+
if (!report.clientRecovery?.pi)
|
|
3015
|
+
return [];
|
|
3016
|
+
const pi = report.clientRecovery.pi;
|
|
3017
|
+
return [
|
|
3018
|
+
`clientRecovery.pi.reason: ${pi.reason}`,
|
|
3019
|
+
`clientRecovery.pi.action: ${pi.action}`,
|
|
3020
|
+
`clientRecovery.pi.path: ${pi.path}`,
|
|
3021
|
+
];
|
|
3022
|
+
}
|
|
2855
3023
|
function formatCheckUpdateText(report) {
|
|
2856
3024
|
return [
|
|
2857
3025
|
`loop-agent init update check for ${report.repoRoot}`,
|
|
@@ -2860,17 +3028,22 @@ function formatCheckUpdateText(report) {
|
|
|
2860
3028
|
`deterministicActions: ${report.deterministicActions.length}`,
|
|
2861
3029
|
`modelMergeTasks: ${report.modelMergeTasks.length}`,
|
|
2862
3030
|
`humanDecisions: ${report.humanDecisions.length}`,
|
|
3031
|
+
...formatClientRecoverySummary(report),
|
|
2863
3032
|
"recommendedNext:",
|
|
2864
3033
|
...report.recommendedNext.map((item) => `- ${item}`),
|
|
2865
3034
|
].join("\n");
|
|
2866
3035
|
}
|
|
2867
3036
|
function formatCheckUpdateMarkdown(report) {
|
|
3037
|
+
const piLines = formatClientRecoverySummary(report).map((line) => `- ${line}`);
|
|
2868
3038
|
const lines = [
|
|
2869
3039
|
"# loop-agent init check-update",
|
|
2870
3040
|
"",
|
|
2871
3041
|
`- repoRoot: \`${report.repoRoot}\``,
|
|
2872
3042
|
`- controllerVersion: \`${report.controllerVersion}\``,
|
|
2873
3043
|
`- surfaceState: \`${report.surfaceState}\``,
|
|
3044
|
+
...(piLines.length > 0
|
|
3045
|
+
? ["", "## Client Recovery (Pi user config, read-only)", "", ...piLines]
|
|
3046
|
+
: []),
|
|
2874
3047
|
"",
|
|
2875
3048
|
"## Deterministic Actions",
|
|
2876
3049
|
"",
|
|
@@ -2907,21 +3080,29 @@ export async function runInitDoctor(input) {
|
|
|
2907
3080
|
add("harness.json", false, error instanceof Error ? error.message : String(error));
|
|
2908
3081
|
}
|
|
2909
3082
|
const agents = path.join(repoRoot, "AGENTS.md");
|
|
2910
|
-
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");
|
|
2911
3085
|
const readme = path.join(repoRoot, "README.md");
|
|
2912
|
-
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");
|
|
2913
3088
|
add("repo-local skills", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
|
|
2914
3089
|
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
2915
|
-
const gitignoreContent = (await exists(gitignorePath))
|
|
2916
|
-
|
|
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");
|
|
2917
3095
|
const requiredScripts = Object.keys(INIT_SCRIPT_FILES);
|
|
2918
3096
|
const missingScripts = [];
|
|
2919
3097
|
for (const script of requiredScripts) {
|
|
2920
3098
|
if (!(await exists(path.join(repoRoot, script))))
|
|
2921
3099
|
missingScripts.push(script);
|
|
2922
3100
|
}
|
|
2923
|
-
add("script matrix", missingScripts.length === 0, missingScripts.length === 0
|
|
2924
|
-
|
|
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");
|
|
2925
3106
|
add("harness runtime dirs", await exists(path.join(repoRoot, ".harness", "dag-runs", "active")), ".harness/dag-runs/active");
|
|
2926
3107
|
return {
|
|
2927
3108
|
ok: checks.every((check) => check.ok),
|
|
@@ -2961,7 +3142,12 @@ export async function runInitReconcile(input) {
|
|
|
2961
3142
|
: update.report.modelMergeTasks.length > 0
|
|
2962
3143
|
? "needs-model-merge"
|
|
2963
3144
|
: "needs-safe-update";
|
|
2964
|
-
return {
|
|
3145
|
+
return {
|
|
3146
|
+
status,
|
|
3147
|
+
report: update.report,
|
|
3148
|
+
applied: update.applied,
|
|
3149
|
+
skipped: update.skipped,
|
|
3150
|
+
};
|
|
2965
3151
|
}
|
|
2966
3152
|
function parseInitArgs(repoRoot, args) {
|
|
2967
3153
|
let subcommand;
|
|
@@ -2971,13 +3157,19 @@ function parseInitArgs(repoRoot, args) {
|
|
|
2971
3157
|
let merge = true;
|
|
2972
3158
|
let provider;
|
|
2973
3159
|
let model;
|
|
3160
|
+
let clientRecovery = "auto";
|
|
2974
3161
|
let json = false;
|
|
2975
3162
|
let markdown = false;
|
|
2976
3163
|
let bootstrapSurface = false;
|
|
2977
3164
|
let applySafe = false;
|
|
2978
3165
|
for (let i = 0; i < args.length; i += 1) {
|
|
2979
3166
|
const arg = args[i];
|
|
2980
|
-
if ((arg === "instructions" ||
|
|
3167
|
+
if ((arg === "instructions" ||
|
|
3168
|
+
arg === "doctor" ||
|
|
3169
|
+
arg === "check-update" ||
|
|
3170
|
+
arg === "update" ||
|
|
3171
|
+
arg === "reconcile") &&
|
|
3172
|
+
!subcommand) {
|
|
2981
3173
|
subcommand = arg;
|
|
2982
3174
|
continue;
|
|
2983
3175
|
}
|
|
@@ -3005,6 +3197,10 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3005
3197
|
model = args[++i];
|
|
3006
3198
|
else if (arg.startsWith("--model="))
|
|
3007
3199
|
model = arg.slice("--model=".length);
|
|
3200
|
+
else if (arg === "--client-recovery")
|
|
3201
|
+
clientRecovery = parseClientRecoveryMode(args[++i]);
|
|
3202
|
+
else if (arg.startsWith("--client-recovery="))
|
|
3203
|
+
clientRecovery = parseClientRecoveryMode(arg.slice("--client-recovery=".length));
|
|
3008
3204
|
else if (arg === "--json")
|
|
3009
3205
|
json = true;
|
|
3010
3206
|
else if (arg === "--markdown")
|
|
@@ -3028,6 +3224,7 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3028
3224
|
merge,
|
|
3029
3225
|
provider,
|
|
3030
3226
|
model,
|
|
3227
|
+
clientRecovery,
|
|
3031
3228
|
subcommand,
|
|
3032
3229
|
json,
|
|
3033
3230
|
markdown,
|
|
@@ -3063,7 +3260,9 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
|
|
|
3063
3260
|
throw new Error("usage: init update [--bootstrap-surface] [--apply-safe]");
|
|
3064
3261
|
}
|
|
3065
3262
|
const result = await applyInitUpdate(parsed);
|
|
3066
|
-
console.log(parsed.json
|
|
3263
|
+
console.log(parsed.json
|
|
3264
|
+
? JSON.stringify(result, null, 2)
|
|
3265
|
+
: formatInitUpdateResult(result));
|
|
3067
3266
|
return;
|
|
3068
3267
|
}
|
|
3069
3268
|
if (parsed.subcommand === "reconcile") {
|
|
@@ -3071,7 +3270,9 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
|
|
|
3071
3270
|
...parsed,
|
|
3072
3271
|
readRuntimeActivity: dependencies.readRuntimeActivity,
|
|
3073
3272
|
});
|
|
3074
|
-
console.log(parsed.json
|
|
3273
|
+
console.log(parsed.json
|
|
3274
|
+
? JSON.stringify(result, null, 2)
|
|
3275
|
+
: formatReconcileResult(result));
|
|
3075
3276
|
return;
|
|
3076
3277
|
}
|
|
3077
3278
|
const result = await initializeLoopAgentProject(parsed);
|