@tea-agent/loop-agent 0.10.0-alpha.0 → 0.11.0
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 +2 -2
- package/CHANGELOG.md +39 -47
- package/README.md +33 -6
- package/dist/application/dag/args.js +2 -3
- package/dist/application/dag/generate-task-dag.js +5 -14
- package/dist/cli/command-definitions.js +44 -5
- package/dist/cli/program.js +37 -3
- package/dist/cli/update/notifier.js +117 -0
- package/dist/cli/update/npm-client.js +151 -0
- package/dist/cli/update/policy.js +58 -0
- package/dist/cli/update/state.js +68 -0
- package/dist/cli.js +33 -0
- package/dist/commands/init.js +432 -58
- package/dist/commands/plan.js +50 -0
- package/dist/governance/exec-plans.js +545 -0
- package/dist/governance/manifest-types.js +0 -5
- package/dist/task/config-types.js +0 -1
- package/dist/worker/observe/static/app.js +326 -45
- package/dist/worker/observe/static/styles.css +1 -0
- package/dist/workflows/dag/governance-profile.js +0 -10
- package/dist/workflows/dag/init-hybrid.js +5 -201
- package/dist/workflows/dag/sdd-embedded.js +128 -0
- package/dist/workflows/dag/skill-instructions.js +5 -4
- package/docs/README.md +1 -0
- package/docs/agent-dag-runner.md +2 -2
- package/docs/architecture/runtime-boundaries.md +3 -0
- package/docs/design/README.md +1 -0
- package/docs/development-principles.md +1 -1
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +6 -0
- package/docs/feature-workflow.md +27 -21
- package/docs/harness-methodology-debugging.md +1 -1
- package/docs/harness-methodology-tdd.md +3 -3
- package/docs/init-surface.manifest.json +23 -50
- package/docs/loop-agent-harness.md +8 -3
- package/docs/progress/README.md +6 -0
- package/docs/reports/README.md +10 -0
- package/docs/templates/project-start-checklist.md +2 -2
- package/harness.json +1 -3
- package/package.json +3 -3
- package/skills/frontend-implementation/SKILL.md +3 -0
- package/skills/loop-agent/references/command-reference.md +6 -0
- package/skills/loop-agent/references/docs-converge.md +5 -5
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +0 -131
- package/docs/templates/backend-test-dag.json +0 -213
- package/docs/templates/backend-test-dag.retrospect.prompt.md +0 -128
- package/docs/templates/backend-test-dag.review-cases.prompt.md +0 -85
package/dist/commands/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { access, copyFile, mkdir, readdir, readFile, 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 path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { copyDir } from "../shared/copy-dir.js";
|
|
6
7
|
import { loadHarnessManifest } from "../governance/harness.js";
|
|
@@ -9,6 +10,7 @@ const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
|
|
|
9
10
|
const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
|
|
10
11
|
const GITIGNORE_BLOCK_END = "# LOOP_AGENT_INIT_END";
|
|
11
12
|
const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
|
|
13
|
+
const DEFAULT_GOVERNANCE_ROOT = "ai_workspace/loop-agent";
|
|
12
14
|
const CORE_DOC_FILES = [
|
|
13
15
|
"README.md",
|
|
14
16
|
"architecture/runtime-boundaries.md",
|
|
@@ -108,7 +110,7 @@ required_paths=(
|
|
|
108
110
|
"${GOVERNANCE_ROOT_TOKEN}/verification-matrix.md"
|
|
109
111
|
"${GOVERNANCE_ROOT_TOKEN}/architecture/runtime-boundaries.md"
|
|
110
112
|
"${GOVERNANCE_ROOT_TOKEN}/templates"
|
|
111
|
-
"skills/loop-agent/SKILL.md"
|
|
113
|
+
".agents/skills/loop-agent/SKILL.md"
|
|
112
114
|
".harness/prompts/analyze.md"
|
|
113
115
|
".harness/prompts/plan.md"
|
|
114
116
|
".harness/tasks"
|
|
@@ -305,7 +307,7 @@ fi
|
|
|
305
307
|
|
|
306
308
|
if [[ -d artifacts ]]; then
|
|
307
309
|
artifact_count="$(find artifacts -mindepth 1 -maxdepth 1 ! -name .gitkeep | wc -l | tr -d ' ')"
|
|
308
|
-
[[ "\${artifact_count}" == "0" ]] || failures+=("root artifacts/ contains \${artifact_count} entries; use
|
|
310
|
+
[[ "\${artifact_count}" == "0" ]] || failures+=("root artifacts/ contains \${artifact_count} entries; use ${GOVERNANCE_ROOT_TOKEN}/reports or .harness run artifacts")
|
|
309
311
|
fi
|
|
310
312
|
|
|
311
313
|
if (( \${#failures[@]} > 0 )); then
|
|
@@ -370,7 +372,7 @@ cd "\${ROOT_DIR}"
|
|
|
370
372
|
check_skill() {
|
|
371
373
|
local skill_name="$1"
|
|
372
374
|
shift
|
|
373
|
-
local skill_dir="skills/\${skill_name}"
|
|
375
|
+
local skill_dir=".agents/skills/\${skill_name}"
|
|
374
376
|
local skill_md="\${skill_dir}/SKILL.md"
|
|
375
377
|
local missing=()
|
|
376
378
|
|
|
@@ -408,7 +410,7 @@ check_skill "loop-agent" \\
|
|
|
408
410
|
"references/command-reference.md"
|
|
409
411
|
check_skill "agent-worker" "references/agent-worker-operator.md"
|
|
410
412
|
|
|
411
|
-
description_line="$(grep -E '^description:' skills/agent-worker/SKILL.md | head -n 1 | tr '[:upper:]' '[:lower:]')"
|
|
413
|
+
description_line="$(grep -E '^description:' .agents/skills/agent-worker/SKILL.md | head -n 1 | tr '[:upper:]' '[:lower:]')"
|
|
412
414
|
for term in "agent-worker" "feature packet" "taskspec" "task pool" "self-host" "candidate" "loop-agent"; do
|
|
413
415
|
if [[ "\${description_line}" != *"\${term}"* ]]; then
|
|
414
416
|
echo "skill entry 检查失败(agent-worker):description 缺少触发/路由词 \${term}" >&2
|
|
@@ -591,7 +593,7 @@ fi
|
|
|
591
593
|
|
|
592
594
|
if (( ran == 0 )); then
|
|
593
595
|
echo "未检测到可安全自动运行的项目测试入口。"
|
|
594
|
-
echo "loop-agent 治理检查仍可运行;请让初始化模型根据目标项目技术栈补充 scripts/ci-tests.sh 与
|
|
596
|
+
echo "loop-agent 治理检查仍可运行;请让初始化模型根据目标项目技术栈补充 scripts/ci-tests.sh 与 ${GOVERNANCE_ROOT_TOKEN}/verification-matrix.md。"
|
|
595
597
|
fi
|
|
596
598
|
|
|
597
599
|
if (( \${#notes[@]} > 0 )); then
|
|
@@ -679,14 +681,14 @@ function buildManagedAgentsBlock(input) {
|
|
|
679
681
|
"",
|
|
680
682
|
"### 初始化闭环",
|
|
681
683
|
"",
|
|
682
|
-
|
|
684
|
+
`当用户要求在本仓库初始化 loop-agent,或发现 \`README.md\` / \`${input.governanceRoot}/verification-matrix.md\` 仍停留在通用初始化状态时,模型应在同一轮工作中完成初始化闭环,不要把补全文档和验证矩阵留给用户交互。`,
|
|
683
685
|
"",
|
|
684
686
|
"标准流程:",
|
|
685
687
|
"",
|
|
686
688
|
"1. 运行 `loop-agent init instructions --repo-root .` 获取完整初始化指引。",
|
|
687
689
|
"2. 运行 `loop-agent init --repo-root . --profile full --merge` 完成确定性脚手架。",
|
|
688
690
|
"3. 立即读取目标项目的 README、manifest/build/config 文件和顶层源码/模块目录,识别项目类型、模块、运行入口和验证命令。",
|
|
689
|
-
|
|
691
|
+
`4. 基于真实项目文件补全或修正根 \`README.md\`、\`${input.governanceRoot}/verification-matrix.md\`,必要时适配 \`scripts/ci-tests.sh\`。`,
|
|
690
692
|
"5. 运行 `loop-agent init doctor --repo-root .`、`loop-agent inspect --repo-root .`、`loop-agent docs audit --repo-root .` 和 quick verification。",
|
|
691
693
|
"",
|
|
692
694
|
"如果仓库信息不足,写明“尚未从仓库文件中识别到 ...”以及下一步需要的事实;不要留下泛化占位符,也不要只提示用户稍后手填。",
|
|
@@ -737,7 +739,7 @@ function buildManagedAgentsBlock(input) {
|
|
|
737
739
|
"",
|
|
738
740
|
"- `.harness/tasks/` 保存任务状态和 source 材料。",
|
|
739
741
|
"- `.harness/dag-runs/` 与 `.harness/runs/` 保存运行事实;已完成事实只读。",
|
|
740
|
-
"-
|
|
742
|
+
"- `.agents/skills/` 保存目标项目本地 skill 指令;本地缺失时 runtime 可回退到 npm 包内置 skills。",
|
|
741
743
|
"- 示例内置在工具中;优先使用 `loop-agent examples list` 和 `loop-agent examples show <name>` 查看,不默认提交到目标仓库。",
|
|
742
744
|
"",
|
|
743
745
|
"### 验证",
|
|
@@ -760,7 +762,7 @@ function buildManagedAgentsBlock(input) {
|
|
|
760
762
|
"",
|
|
761
763
|
"### 交接",
|
|
762
764
|
"",
|
|
763
|
-
|
|
765
|
+
`较大的交接应说明改了什么、为什么这样改、验证命令和结果、影响到的 docs/tests/scripts/contracts、剩余风险和下一步。长期结论应进入 \`${input.governanceRoot}/progress\`、\`${input.governanceRoot}/reports\`、\`${input.governanceRoot}/exec-plans\`、\`${input.governanceRoot}/decisions\`、测试、脚本或模板。`,
|
|
764
766
|
MANAGED_BLOCK_END,
|
|
765
767
|
].join("\n");
|
|
766
768
|
}
|
|
@@ -1010,12 +1012,18 @@ function manifestPathToTargetPath(relativePath, governanceRoot) {
|
|
|
1010
1012
|
if (governanceRoot !== "docs" && relativePath.startsWith("docs/")) {
|
|
1011
1013
|
return `${governanceRoot}/${relativePath.slice("docs/".length)}`;
|
|
1012
1014
|
}
|
|
1015
|
+
if (relativePath.startsWith("skills/")) {
|
|
1016
|
+
return `.agents/${relativePath}`;
|
|
1017
|
+
}
|
|
1013
1018
|
return relativePath;
|
|
1014
1019
|
}
|
|
1015
1020
|
function targetPathToManifestPath(relativePath, governanceRoot) {
|
|
1016
1021
|
if (governanceRoot !== "docs" && relativePath.startsWith(`${governanceRoot}/`)) {
|
|
1017
1022
|
return `docs/${relativePath.slice(`${governanceRoot}/`.length)}`;
|
|
1018
1023
|
}
|
|
1024
|
+
if (relativePath.startsWith(".agents/skills/")) {
|
|
1025
|
+
return relativePath.slice(".agents/".length);
|
|
1026
|
+
}
|
|
1019
1027
|
return relativePath;
|
|
1020
1028
|
}
|
|
1021
1029
|
async function readPackageVersion(assetRoot) {
|
|
@@ -1206,10 +1214,20 @@ async function buildCurrentSurfaceState(input) {
|
|
|
1206
1214
|
};
|
|
1207
1215
|
continue;
|
|
1208
1216
|
}
|
|
1217
|
+
let semanticallyMatchesGeneratedJson = false;
|
|
1218
|
+
if (targetRelativePath === "harness.json" && desired.content !== undefined) {
|
|
1219
|
+
try {
|
|
1220
|
+
semanticallyMatchesGeneratedJson = isDeepStrictEqual(JSON.parse(current.toString("utf-8")), JSON.parse(desired.content));
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
// Invalid JSON remains local-existing-unknown for model merge / doctor.
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1209
1226
|
files[targetRelativePath] = {
|
|
1210
1227
|
...base,
|
|
1211
1228
|
currentSha256,
|
|
1212
|
-
relationship:
|
|
1229
|
+
relationship: semanticallyMatchesGeneratedJson ||
|
|
1230
|
+
(sourceSha256 !== undefined && currentSha256 === sourceSha256)
|
|
1213
1231
|
? entry.mode === "generated"
|
|
1214
1232
|
? "matches-current-generated"
|
|
1215
1233
|
: "matches-current-package"
|
|
@@ -1232,15 +1250,271 @@ async function writeInitSurfaceState(input) {
|
|
|
1232
1250
|
await writeFile(target, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
1233
1251
|
return state;
|
|
1234
1252
|
}
|
|
1253
|
+
function isCurrentInitRelationship(relationship) {
|
|
1254
|
+
return (relationship === "matches-current-package" ||
|
|
1255
|
+
relationship === "matches-current-generated" ||
|
|
1256
|
+
relationship === "managed-block-current" ||
|
|
1257
|
+
relationship === "directory-present" ||
|
|
1258
|
+
relationship === "state-file");
|
|
1259
|
+
}
|
|
1260
|
+
async function fileSha256IfExists(filePath) {
|
|
1261
|
+
const info = await stat(filePath).catch(() => null);
|
|
1262
|
+
if (!info || !info.isFile())
|
|
1263
|
+
return undefined;
|
|
1264
|
+
return sha256Buffer(await readFile(filePath));
|
|
1265
|
+
}
|
|
1266
|
+
async function isDirectoryEmpty(dirPath) {
|
|
1267
|
+
const info = await stat(dirPath).catch(() => null);
|
|
1268
|
+
if (!info || !info.isDirectory())
|
|
1269
|
+
return false;
|
|
1270
|
+
return (await readdir(dirPath)).length === 0;
|
|
1271
|
+
}
|
|
1272
|
+
async function listRelativeFiles(rootPath) {
|
|
1273
|
+
const rootInfo = await stat(rootPath).catch(() => null);
|
|
1274
|
+
if (!rootInfo?.isDirectory())
|
|
1275
|
+
return [];
|
|
1276
|
+
const files = [];
|
|
1277
|
+
async function walk(currentPath, relativeDir) {
|
|
1278
|
+
for (const entry of await readdir(currentPath, { withFileTypes: true })) {
|
|
1279
|
+
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
1280
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
1281
|
+
if (entry.isDirectory())
|
|
1282
|
+
await walk(absolutePath, relativePath);
|
|
1283
|
+
else if (entry.isFile())
|
|
1284
|
+
files.push(relativePath);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
await walk(rootPath, "");
|
|
1288
|
+
return files.sort();
|
|
1289
|
+
}
|
|
1290
|
+
async function expectedLegacyInitFileSha(input) {
|
|
1291
|
+
if (input.legacyPath.startsWith("docs/")) {
|
|
1292
|
+
const doc = input.legacyPath.slice("docs/".length);
|
|
1293
|
+
const generated = buildGeneratedCoreDoc({
|
|
1294
|
+
doc,
|
|
1295
|
+
projectName: input.projectName,
|
|
1296
|
+
governanceRoot: "docs",
|
|
1297
|
+
});
|
|
1298
|
+
if (generated !== undefined)
|
|
1299
|
+
return sha256Text(generated);
|
|
1300
|
+
const generatedDir = GOVERNANCE_README_DIRS.find((dir) => input.legacyPath === `docs/${dir}/README.md`);
|
|
1301
|
+
if (generatedDir) {
|
|
1302
|
+
return sha256Text(buildGovernanceDirectoryReadme({
|
|
1303
|
+
dir: generatedDir,
|
|
1304
|
+
projectName: input.projectName,
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (!input.legacyPath.startsWith("docs/") && !input.legacyPath.startsWith("skills/")) {
|
|
1309
|
+
return undefined;
|
|
1310
|
+
}
|
|
1311
|
+
return fileSha256IfExists(path.join(input.assetRoot, input.legacyPath));
|
|
1312
|
+
}
|
|
1313
|
+
function collectSafeRetiredDirectories(paths) {
|
|
1314
|
+
const dirs = new Set();
|
|
1315
|
+
for (const retiredPath of paths) {
|
|
1316
|
+
let dir = path.posix.dirname(retiredPath);
|
|
1317
|
+
while (dir !== "." && dir !== "/") {
|
|
1318
|
+
if (dir === "skills" || dir.startsWith("skills/") || dir === "docs" || dir.startsWith("docs/")) {
|
|
1319
|
+
dirs.add(dir);
|
|
1320
|
+
}
|
|
1321
|
+
dir = path.posix.dirname(dir);
|
|
1322
|
+
}
|
|
1323
|
+
if (retiredPath.startsWith("skills/"))
|
|
1324
|
+
dirs.add("skills");
|
|
1325
|
+
}
|
|
1326
|
+
return [...dirs].sort((left, right) => right.split("/").length - left.split("/").length);
|
|
1327
|
+
}
|
|
1328
|
+
function needsHarnessModelMigration(harness) {
|
|
1329
|
+
if ("model" in harness || "models" in harness || "modelProfiles" in harness || "modelRouting" in harness) {
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
if (isRecord(harness.executors)) {
|
|
1333
|
+
if ("cursor" in harness.executors)
|
|
1334
|
+
return true;
|
|
1335
|
+
if (isRecord(harness.executors.pi)) {
|
|
1336
|
+
return ("requiresApiKey" in harness.executors.pi ||
|
|
1337
|
+
"defaultModel" in harness.executors.pi);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
function migrateHarnessModelFields(harness) {
|
|
1343
|
+
const next = { ...harness };
|
|
1344
|
+
const legacyModel = typeof next.model === "string"
|
|
1345
|
+
? next.model
|
|
1346
|
+
: isRecord(next.executors) &&
|
|
1347
|
+
isRecord(next.executors.pi) &&
|
|
1348
|
+
typeof next.executors.pi.defaultModel === "string"
|
|
1349
|
+
? next.executors.pi.defaultModel
|
|
1350
|
+
: undefined;
|
|
1351
|
+
delete next.model;
|
|
1352
|
+
delete next.models;
|
|
1353
|
+
delete next.modelProfiles;
|
|
1354
|
+
delete next.modelRouting;
|
|
1355
|
+
const executors = isRecord(next.executors) ? { ...next.executors } : {};
|
|
1356
|
+
delete executors.cursor;
|
|
1357
|
+
const pi = isRecord(executors.pi) ? { ...executors.pi } : {};
|
|
1358
|
+
delete pi.requiresApiKey;
|
|
1359
|
+
delete pi.defaultModel;
|
|
1360
|
+
if (legacyModel) {
|
|
1361
|
+
for (const level of ["LOW", "MED", "HIGH"]) {
|
|
1362
|
+
if (typeof pi[level] !== "string")
|
|
1363
|
+
pi[level] = legacyModel;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
executors.pi = pi;
|
|
1367
|
+
next.executors = executors;
|
|
1368
|
+
return next;
|
|
1369
|
+
}
|
|
1370
|
+
function hasLegacyHarnessGovernancePaths(harness) {
|
|
1371
|
+
for (const sectionName of ["entrypoints", "artifacts"]) {
|
|
1372
|
+
const section = harness[sectionName];
|
|
1373
|
+
if (isRecord(section) &&
|
|
1374
|
+
Object.values(section).some((value) => typeof value === "string" && value.startsWith("docs/"))) {
|
|
1375
|
+
return true;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
async function collectRetiredLayoutActions(input) {
|
|
1381
|
+
if (!input.recordedState)
|
|
1382
|
+
return [];
|
|
1383
|
+
const actions = [];
|
|
1384
|
+
const retiredOwnedPaths = [];
|
|
1385
|
+
const currentPaths = new Set(Object.keys(input.currentState.files));
|
|
1386
|
+
const recordedPaths = new Set(Object.keys(input.recordedState.files));
|
|
1387
|
+
for (const [oldPath, oldState] of Object.entries(input.recordedState.files)) {
|
|
1388
|
+
const targetPath = normalizeRelativePath(manifestPathToTargetPath(oldPath, input.governanceRoot));
|
|
1389
|
+
if (targetPath === oldPath || !currentPaths.has(targetPath))
|
|
1390
|
+
continue;
|
|
1391
|
+
if (oldState.mode === "directory" || oldState.mode === "state")
|
|
1392
|
+
continue;
|
|
1393
|
+
const oldAbsolute = path.join(input.repoRoot, oldPath);
|
|
1394
|
+
const oldHash = await fileSha256IfExists(oldAbsolute);
|
|
1395
|
+
if (!oldHash)
|
|
1396
|
+
continue;
|
|
1397
|
+
if (!oldState.currentSha256 || oldHash !== oldState.currentSha256) {
|
|
1398
|
+
input.humanDecisions.push({
|
|
1399
|
+
path: oldPath,
|
|
1400
|
+
reason: `legacy init file may have local edits; review before migrating to ${targetPath}`,
|
|
1401
|
+
});
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
const targetAbsolute = path.join(input.repoRoot, targetPath);
|
|
1405
|
+
const targetHash = await fileSha256IfExists(targetAbsolute);
|
|
1406
|
+
const targetState = input.currentState.files[targetPath];
|
|
1407
|
+
if (!targetHash) {
|
|
1408
|
+
if (targetState?.sourceSha256 !== undefined &&
|
|
1409
|
+
targetState.sourceSha256 !== oldHash) {
|
|
1410
|
+
actions.push({
|
|
1411
|
+
type: "remove-owned-file",
|
|
1412
|
+
path: oldPath,
|
|
1413
|
+
reason: `remove unchanged legacy init file so the current ${targetPath} can be installed`,
|
|
1414
|
+
});
|
|
1415
|
+
retiredOwnedPaths.push(oldPath);
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
actions.push({
|
|
1419
|
+
type: "migrate-owned-file",
|
|
1420
|
+
path: oldPath,
|
|
1421
|
+
targetPath,
|
|
1422
|
+
reason: `move loop-agent-owned legacy init file to ${targetPath}`,
|
|
1423
|
+
});
|
|
1424
|
+
retiredOwnedPaths.push(oldPath);
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
const recordedTargetState = input.recordedState.files[targetPath];
|
|
1428
|
+
if (targetHash === oldHash ||
|
|
1429
|
+
(input.recordedState.stateKind === "recorded" &&
|
|
1430
|
+
recordedTargetState?.currentSha256 !== undefined &&
|
|
1431
|
+
recordedTargetState.currentSha256 === targetHash) ||
|
|
1432
|
+
isCurrentInitRelationship(targetState?.relationship)) {
|
|
1433
|
+
actions.push({
|
|
1434
|
+
type: "remove-owned-file",
|
|
1435
|
+
path: oldPath,
|
|
1436
|
+
reason: `remove duplicate loop-agent-owned legacy init file; current path is ${targetPath}`,
|
|
1437
|
+
});
|
|
1438
|
+
retiredOwnedPaths.push(oldPath);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
input.humanDecisions.push({
|
|
1442
|
+
path: oldPath,
|
|
1443
|
+
reason: `legacy init file is owned, but target path ${targetPath} already exists with different content`,
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
for (const legacyRoot of ["docs", "skills"]) {
|
|
1447
|
+
for (const relativePath of await listRelativeFiles(path.join(input.repoRoot, legacyRoot))) {
|
|
1448
|
+
const oldPath = `${legacyRoot}/${relativePath}`;
|
|
1449
|
+
if (recordedPaths.has(oldPath))
|
|
1450
|
+
continue;
|
|
1451
|
+
const targetPath = normalizeRelativePath(manifestPathToTargetPath(oldPath, input.governanceRoot));
|
|
1452
|
+
if (targetPath === oldPath)
|
|
1453
|
+
continue;
|
|
1454
|
+
const expectedSha = await expectedLegacyInitFileSha({
|
|
1455
|
+
assetRoot: input.assetRoot,
|
|
1456
|
+
projectName: input.projectName,
|
|
1457
|
+
legacyPath: oldPath,
|
|
1458
|
+
});
|
|
1459
|
+
if (!expectedSha)
|
|
1460
|
+
continue;
|
|
1461
|
+
const oldHash = await fileSha256IfExists(path.join(input.repoRoot, oldPath));
|
|
1462
|
+
if (!oldHash)
|
|
1463
|
+
continue;
|
|
1464
|
+
const targetHash = await fileSha256IfExists(path.join(input.repoRoot, targetPath));
|
|
1465
|
+
if (targetHash === oldHash) {
|
|
1466
|
+
actions.push({
|
|
1467
|
+
type: "remove-owned-file",
|
|
1468
|
+
path: oldPath,
|
|
1469
|
+
reason: `remove duplicate legacy init file; current path is ${targetPath}`,
|
|
1470
|
+
});
|
|
1471
|
+
retiredOwnedPaths.push(oldPath);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (oldHash !== expectedSha) {
|
|
1475
|
+
input.humanDecisions.push({
|
|
1476
|
+
path: oldPath,
|
|
1477
|
+
reason: `legacy init-shaped file is not recorded and differs from the current bundled/generated asset; review before migrating to ${targetPath}`,
|
|
1478
|
+
});
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
if (!targetHash) {
|
|
1482
|
+
actions.push({
|
|
1483
|
+
type: "migrate-owned-file",
|
|
1484
|
+
path: oldPath,
|
|
1485
|
+
targetPath,
|
|
1486
|
+
reason: `move package-matching legacy init file to ${targetPath}`,
|
|
1487
|
+
});
|
|
1488
|
+
retiredOwnedPaths.push(oldPath);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
input.humanDecisions.push({
|
|
1492
|
+
path: oldPath,
|
|
1493
|
+
reason: `legacy init-shaped file matches the package, but target path ${targetPath} already exists with different content`,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
for (const dir of collectSafeRetiredDirectories(retiredOwnedPaths)) {
|
|
1498
|
+
actions.push({
|
|
1499
|
+
type: "remove-empty-directory",
|
|
1500
|
+
path: dir,
|
|
1501
|
+
reason: "remove directory if it is empty after legacy init files are migrated",
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
return actions;
|
|
1505
|
+
}
|
|
1235
1506
|
async function resolveInitProjectContext(input) {
|
|
1236
1507
|
const harness = await readJsonIfExists(path.join(input.repoRoot, "harness.json"));
|
|
1508
|
+
const recordedGovernanceRoot = typeof harness.governanceRoot === "string" ? harness.governanceRoot : undefined;
|
|
1237
1509
|
return {
|
|
1238
1510
|
projectName: input.projectName ??
|
|
1239
1511
|
(typeof harness.project === "string" ? harness.project : undefined) ??
|
|
1240
1512
|
path.basename(input.repoRoot),
|
|
1241
1513
|
governanceRoot: input.governanceRoot ??
|
|
1242
|
-
(
|
|
1243
|
-
|
|
1514
|
+
(recordedGovernanceRoot === "docs"
|
|
1515
|
+
? DEFAULT_GOVERNANCE_ROOT
|
|
1516
|
+
: recordedGovernanceRoot) ??
|
|
1517
|
+
DEFAULT_GOVERNANCE_ROOT,
|
|
1244
1518
|
};
|
|
1245
1519
|
}
|
|
1246
1520
|
function buildManagedReadmeBlock(input) {
|
|
@@ -1453,7 +1727,7 @@ function buildTargetDevelopmentPrinciples(input) {
|
|
|
1453
1727
|
"1. One task advances one bounded work block.",
|
|
1454
1728
|
"2. Search existing code, docs, scripts, and tests before designing new behavior.",
|
|
1455
1729
|
"3. Shell verification is the completion authority.",
|
|
1456
|
-
|
|
1730
|
+
`4. Runtime state belongs in \`.harness/\`; durable decisions belong in \`${input.governanceRoot}/\`.`,
|
|
1457
1731
|
"5. Model writer nodes must be bounded by explicit allowed and forbidden paths.",
|
|
1458
1732
|
"6. Advisory model output must be followed by deterministic verification.",
|
|
1459
1733
|
"7. Repeated constraints should become docs, tests, scripts, checks, or templates.",
|
|
@@ -1480,7 +1754,7 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
1480
1754
|
"3. Contract: state deliverables, non-goals, completion criteria, verification commands, and failure conditions.",
|
|
1481
1755
|
"4. Implement: make the smallest coherent change and update required docs, scripts, and tests.",
|
|
1482
1756
|
"5. Verify: run governance checks plus target project verification.",
|
|
1483
|
-
|
|
1757
|
+
`6. Handoff: record evidence in ${input.governanceRoot}/progress, ${input.governanceRoot}/reports, an exec plan, or an ADR when useful.`,
|
|
1484
1758
|
"",
|
|
1485
1759
|
"## Agent DAG Path",
|
|
1486
1760
|
"",
|
|
@@ -1554,8 +1828,8 @@ function buildTargetLoopAgentHarness(input) {
|
|
|
1554
1828
|
"- `.harness/dag-runs/` stores DAG run facts.",
|
|
1555
1829
|
"- `.harness/runs/` stores one-shot executor facts.",
|
|
1556
1830
|
"- `.harness/task-pool/` stores optional agent-worker Task Pool state, batch artifacts, failure handoffs, and local Observe events; it is runtime state and normally ignored by Git.",
|
|
1557
|
-
|
|
1558
|
-
"-
|
|
1831
|
+
`- \`${input.governanceRoot}/\` stores loop-agent generated governance, plans, reports, progress, and decisions.`,
|
|
1832
|
+
"- `.agents/skills/` stores project repo-local skill instructions; the CLI can fall back to bundled package skills when needed.",
|
|
1559
1833
|
"",
|
|
1560
1834
|
"## Default Workflow",
|
|
1561
1835
|
"",
|
|
@@ -1565,9 +1839,9 @@ function buildTargetLoopAgentHarness(input) {
|
|
|
1565
1839
|
"",
|
|
1566
1840
|
"After a run, use `loop-agent dag report --run-id <run-id> --markdown` to read canonical run facts and `loop-agent dag doctor --run-id <run-id> --markdown` to diagnose failed or paused runs. Failed DAG runs should produce a failure handoff via `loop-agent dag closeout-draft --run-id <run-id>` instead of a successful closeout.",
|
|
1567
1841
|
"",
|
|
1568
|
-
|
|
1842
|
+
`Use \`${input.governanceRoot}/templates/production-readiness-checklist.md\` when claiming Production Readiness v0.1 for low/medium-risk single-repo DAG work.`,
|
|
1569
1843
|
"",
|
|
1570
|
-
|
|
1844
|
+
`For real product-line Worker samples, use \`${input.governanceRoot}/templates/worker-dogfood-setup.md\` and \`${input.governanceRoot}/templates/worker-dogfood-evidence.md\`. Explicit failed-task retries must preserve prior evidence and use a new worker run id.`,
|
|
1571
1845
|
"",
|
|
1572
1846
|
"## Script Matrix",
|
|
1573
1847
|
"",
|
|
@@ -1583,13 +1857,16 @@ function buildGeneratedCoreDoc(input) {
|
|
|
1583
1857
|
case "architecture/runtime-boundaries.md":
|
|
1584
1858
|
return buildTargetRuntimeBoundaries({ projectName: input.projectName });
|
|
1585
1859
|
case "development-principles.md":
|
|
1586
|
-
return buildTargetDevelopmentPrinciples({
|
|
1860
|
+
return buildTargetDevelopmentPrinciples({
|
|
1861
|
+
projectName: input.projectName,
|
|
1862
|
+
governanceRoot: input.governanceRoot,
|
|
1863
|
+
});
|
|
1587
1864
|
case "feature-workflow.md":
|
|
1588
1865
|
return buildTargetFeatureWorkflow({ governanceRoot: input.governanceRoot });
|
|
1589
1866
|
case "verification-matrix.md":
|
|
1590
1867
|
return buildTargetVerificationMatrix();
|
|
1591
1868
|
case "loop-agent-harness.md":
|
|
1592
|
-
return buildTargetLoopAgentHarness({ projectName: input.projectName });
|
|
1869
|
+
return buildTargetLoopAgentHarness({ projectName: input.projectName, governanceRoot: input.governanceRoot });
|
|
1593
1870
|
default:
|
|
1594
1871
|
return undefined;
|
|
1595
1872
|
}
|
|
@@ -1610,7 +1887,7 @@ function buildGovernanceDirectoryReadme(input) {
|
|
|
1610
1887
|
}
|
|
1611
1888
|
export function buildInitInstructions(input) {
|
|
1612
1889
|
const projectName = input.projectName ?? path.basename(path.resolve(input.repoRoot));
|
|
1613
|
-
const governanceRoot = input.governanceRoot ??
|
|
1890
|
+
const governanceRoot = input.governanceRoot ?? DEFAULT_GOVERNANCE_ROOT;
|
|
1614
1891
|
return [
|
|
1615
1892
|
"# loop-agent Initialization Instructions",
|
|
1616
1893
|
"",
|
|
@@ -1621,7 +1898,7 @@ export function buildInitInstructions(input) {
|
|
|
1621
1898
|
`- Project name: default \`${projectName}\`.`,
|
|
1622
1899
|
`- Governance root: default \`${governanceRoot}\`.`,
|
|
1623
1900
|
"- Confirm provider/model. Defaults may be used, but the target project may need a different provider/model matrix.",
|
|
1624
|
-
"- Default: merge existing AGENTS.md, harness.json, and docs
|
|
1901
|
+
"- Default: merge existing AGENTS.md, harness.json, and loop-agent governance docs instead of overwriting user content.",
|
|
1625
1902
|
"",
|
|
1626
1903
|
"## Apply Defaults",
|
|
1627
1904
|
"",
|
|
@@ -1635,8 +1912,8 @@ export function buildInitInstructions(input) {
|
|
|
1635
1912
|
`- Generate target-project versions of \`${governanceRoot}/README.md\`, \`${governanceRoot}/development-principles.md\`, \`${governanceRoot}/architecture/runtime-boundaries.md\`, \`${governanceRoot}/feature-workflow.md\`, \`${governanceRoot}/verification-matrix.md\`, and \`${governanceRoot}/loop-agent-harness.md\`; copy only generic methodology/template material as-is.`,
|
|
1636
1913
|
"- After deterministic initialization, inspect README/config/build files (for example package.json, pyproject.toml, go.mod, Cargo.toml, pom.xml, Gradle files, Makefile, .sln/.csproj, or project-specific scripts) and adapt `scripts/ci-tests.sh` plus the verification matrix to the real project.",
|
|
1637
1914
|
"- Enrich the root `README.md`: keep the deterministic project title and the loop-agent managed block intact, and fill the human-authored sections (项目概览, 技术栈与目录结构, 开发与验证) from the target project's actual files. The root README must serve both as a human-first project entry and as an agent work entry; replace the initialization-model supplement comments when the project files provide the information.",
|
|
1638
|
-
|
|
1639
|
-
"-
|
|
1915
|
+
`- Populate \`${governanceRoot}/verification-matrix.md\` with the target project's actual quick, standard, and full verification commands derived from its real language and toolchain, keeping the governance rows intact.`,
|
|
1916
|
+
"- Project repo-local skills live in `.agents/skills/`. Do not create a root `skills/` directory in the target project; the package's bundled `skills/` remains the built-in fallback.",
|
|
1640
1917
|
"- Merge a loop-agent managed block into `.gitignore` that ignores harness runtime facts (tasks, dag-runs, runs, live, cache, init-surface.json, .harness/task-pool, legacy .task-pool residue) while keeping prompts and directory placeholders shareable.",
|
|
1641
1918
|
"- Do not copy examples by default; examples stay bundled in the tool and are available through `loop-agent examples`.",
|
|
1642
1919
|
"- Add or update a loop-agent managed block in AGENTS.md.",
|
|
@@ -1663,8 +1940,8 @@ export function buildInitInstructions(input) {
|
|
|
1663
1940
|
"",
|
|
1664
1941
|
"1. Read the target project's existing README, manifest/build/config files, and top-level source/module directories.",
|
|
1665
1942
|
"2. Replace the root README's `初始化模型补充` comments when the information can be inferred from files. At minimum, fill project overview, technology stack/directory structure, and development/verification commands.",
|
|
1666
|
-
|
|
1667
|
-
|
|
1943
|
+
`3. Update \`${governanceRoot}/verification-matrix.md\` with the target project's actual quick, standard, and full verification commands.`,
|
|
1944
|
+
`4. Confirm the generated docs expose \`${governanceRoot}/templates/production-readiness-checklist.md\` plus the operator recovery path: \`dag report\`, \`dag doctor --run-id --markdown\`, and failed-run \`dag closeout-draft\` failure handoff.`,
|
|
1668
1945
|
"5. Update `scripts/ci-tests.sh` only when the conservative generated detector is insufficient for the target project.",
|
|
1669
1946
|
"6. Run `loop-agent init doctor`, `loop-agent inspect`, `loop-agent docs audit`, and the quick verification command.",
|
|
1670
1947
|
"",
|
|
@@ -1683,7 +1960,7 @@ export function buildInitInstructions(input) {
|
|
|
1683
1960
|
export async function initializeLoopAgentProject(options) {
|
|
1684
1961
|
const repoRoot = path.resolve(options.repoRoot);
|
|
1685
1962
|
const projectName = options.projectName ?? path.basename(repoRoot);
|
|
1686
|
-
const governanceRoot = options.governanceRoot ??
|
|
1963
|
+
const governanceRoot = options.governanceRoot ?? DEFAULT_GOVERNANCE_ROOT;
|
|
1687
1964
|
const profile = options.profile ?? "full";
|
|
1688
1965
|
const merge = options.merge ?? true;
|
|
1689
1966
|
const written = [];
|
|
@@ -1784,17 +2061,8 @@ export async function initializeLoopAgentProject(options) {
|
|
|
1784
2061
|
written,
|
|
1785
2062
|
});
|
|
1786
2063
|
if (profile === "full") {
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
repoRoot,
|
|
1790
|
-
sourceRelativePath: "skills",
|
|
1791
|
-
targetRelativePath: "skills",
|
|
1792
|
-
written,
|
|
1793
|
-
});
|
|
1794
|
-
// Mirror bundled skills into the agent-compatible `.agents/skills/` path so
|
|
1795
|
-
// external agents (e.g. OpenCode) that auto-discover `.agents/skills/<name>/SKILL.md`
|
|
1796
|
-
// share the same skill content as loop-agent's primary `skills/` path.
|
|
1797
|
-
// The package still ships only `skills/`; `.agents/skills` is a target projection.
|
|
2064
|
+
// Project repo-local skills live only under `.agents/skills/`.
|
|
2065
|
+
// The package still ships `skills/` as the built-in fallback source.
|
|
1798
2066
|
await copyDirMerge({
|
|
1799
2067
|
assetRoot,
|
|
1800
2068
|
repoRoot,
|
|
@@ -1804,7 +2072,6 @@ export async function initializeLoopAgentProject(options) {
|
|
|
1804
2072
|
});
|
|
1805
2073
|
}
|
|
1806
2074
|
else {
|
|
1807
|
-
skipped.push("skills/");
|
|
1808
2075
|
skipped.push(".agents/skills/");
|
|
1809
2076
|
}
|
|
1810
2077
|
skipped.push("examples/");
|
|
@@ -1890,6 +2157,7 @@ export async function checkInitUpdate(input) {
|
|
|
1890
2157
|
governanceRoot: input.governanceRoot,
|
|
1891
2158
|
});
|
|
1892
2159
|
const recordedState = await readExistingSurfaceState(repoRoot);
|
|
2160
|
+
const assetRoot = await findPackageRoot();
|
|
1893
2161
|
const surfaceState = recordedState?.stateKind ?? "missing";
|
|
1894
2162
|
const currentState = await buildCurrentSurfaceState({
|
|
1895
2163
|
repoRoot,
|
|
@@ -1901,6 +2169,18 @@ export async function checkInitUpdate(input) {
|
|
|
1901
2169
|
const deterministicActions = [];
|
|
1902
2170
|
const modelMergeTasks = [];
|
|
1903
2171
|
const humanDecisions = [];
|
|
2172
|
+
const retiredLayoutActions = await collectRetiredLayoutActions({
|
|
2173
|
+
assetRoot,
|
|
2174
|
+
repoRoot,
|
|
2175
|
+
projectName,
|
|
2176
|
+
governanceRoot,
|
|
2177
|
+
recordedState,
|
|
2178
|
+
currentState,
|
|
2179
|
+
humanDecisions,
|
|
2180
|
+
});
|
|
2181
|
+
const retiredTargetPaths = new Set(retiredLayoutActions
|
|
2182
|
+
.filter((action) => action.type === "migrate-owned-file" && action.targetPath)
|
|
2183
|
+
.map((action) => action.targetPath));
|
|
1904
2184
|
if (!recordedState) {
|
|
1905
2185
|
deterministicActions.push({
|
|
1906
2186
|
type: "bootstrap-surface",
|
|
@@ -1910,6 +2190,8 @@ export async function checkInitUpdate(input) {
|
|
|
1910
2190
|
}
|
|
1911
2191
|
for (const [pathName, state] of Object.entries(currentState.files)) {
|
|
1912
2192
|
if (state.relationship === "missing-from-target") {
|
|
2193
|
+
if (retiredTargetPaths.has(pathName))
|
|
2194
|
+
continue;
|
|
1913
2195
|
const action = actionForMissing(pathName, state);
|
|
1914
2196
|
if (action)
|
|
1915
2197
|
deterministicActions.push(action);
|
|
@@ -1930,11 +2212,23 @@ export async function checkInitUpdate(input) {
|
|
|
1930
2212
|
reason: "required init directory path exists but is not a directory",
|
|
1931
2213
|
});
|
|
1932
2214
|
}
|
|
2215
|
+
else if (recordedState?.stateKind === "recorded" &&
|
|
2216
|
+
(state.mode === "copied" || state.mode === "generated") &&
|
|
2217
|
+
state.currentSha256 !== undefined &&
|
|
2218
|
+
recordedState.files[pathName]?.currentSha256 === state.currentSha256) {
|
|
2219
|
+
deterministicActions.push({
|
|
2220
|
+
type: "refresh-owned-file",
|
|
2221
|
+
path: pathName,
|
|
2222
|
+
mode: state.mode,
|
|
2223
|
+
reason: "refresh an unchanged recorded init file to the current generated or bundled content",
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
1933
2226
|
else {
|
|
1934
2227
|
modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths));
|
|
1935
2228
|
}
|
|
1936
2229
|
}
|
|
1937
2230
|
}
|
|
2231
|
+
deterministicActions.unshift(...retiredLayoutActions);
|
|
1938
2232
|
// Safe harness hygiene: strip obsolete pi.requiresApiKey without full harness rewrite.
|
|
1939
2233
|
// Cursor requiresApiKey is intentionally kept when present.
|
|
1940
2234
|
const harnessPath = path.join(repoRoot, "harness.json");
|
|
@@ -1950,14 +2244,21 @@ export async function checkInitUpdate(input) {
|
|
|
1950
2244
|
reason: "harness.json is missing or has a stale IDE JSON Schema reference",
|
|
1951
2245
|
});
|
|
1952
2246
|
}
|
|
1953
|
-
if (
|
|
1954
|
-
isRecord(harness
|
|
1955
|
-
|
|
1956
|
-
|
|
2247
|
+
if (recordedState &&
|
|
2248
|
+
isRecord(harness) &&
|
|
2249
|
+
governanceRoot !== "docs" &&
|
|
2250
|
+
(harness.governanceRoot === "docs" || hasLegacyHarnessGovernancePaths(harness))) {
|
|
2251
|
+
deterministicActions.push({
|
|
2252
|
+
type: "migrate-harness-governance-root",
|
|
2253
|
+
path: "harness.json",
|
|
2254
|
+
reason: `replace the legacy default governanceRoot docs with ${governanceRoot}`,
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
if (isRecord(harness) && needsHarnessModelMigration(harness)) {
|
|
1957
2258
|
deterministicActions.push({
|
|
1958
|
-
type: "
|
|
2259
|
+
type: "migrate-harness-model-fields",
|
|
1959
2260
|
path: "harness.json",
|
|
1960
|
-
reason: "
|
|
2261
|
+
reason: "harness.json contains legacy model fields or executor fields that can be safely normalized",
|
|
1961
2262
|
});
|
|
1962
2263
|
}
|
|
1963
2264
|
}
|
|
@@ -2027,16 +2328,79 @@ async function applySafeAction(input) {
|
|
|
2027
2328
|
await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
|
|
2028
2329
|
return true;
|
|
2029
2330
|
}
|
|
2331
|
+
if (input.action.type === "migrate-harness-model-fields") {
|
|
2332
|
+
const target = path.join(input.repoRoot, "harness.json");
|
|
2333
|
+
if (!(await exists(target)))
|
|
2334
|
+
return false;
|
|
2335
|
+
const harness = JSON.parse(await readFile(target, "utf-8"));
|
|
2336
|
+
if (!isRecord(harness) || !needsHarnessModelMigration(harness))
|
|
2337
|
+
return false;
|
|
2338
|
+
await writeFile(target, `${JSON.stringify(migrateHarnessModelFields(harness), null, 2)}\n`, "utf-8");
|
|
2339
|
+
return true;
|
|
2340
|
+
}
|
|
2341
|
+
if (input.action.type === "migrate-harness-governance-root") {
|
|
2342
|
+
const target = path.join(input.repoRoot, "harness.json");
|
|
2343
|
+
if (!(await exists(target)))
|
|
2344
|
+
return false;
|
|
2345
|
+
const harness = JSON.parse(await readFile(target, "utf-8"));
|
|
2346
|
+
if (!isRecord(harness) ||
|
|
2347
|
+
(harness.governanceRoot !== "docs" && !hasLegacyHarnessGovernancePaths(harness))) {
|
|
2348
|
+
return false;
|
|
2349
|
+
}
|
|
2350
|
+
const next = {
|
|
2351
|
+
...harness,
|
|
2352
|
+
governanceRoot: input.governanceRoot,
|
|
2353
|
+
};
|
|
2354
|
+
for (const sectionName of ["entrypoints", "artifacts"]) {
|
|
2355
|
+
const section = harness[sectionName];
|
|
2356
|
+
if (!isRecord(section))
|
|
2357
|
+
continue;
|
|
2358
|
+
next[sectionName] = Object.fromEntries(Object.entries(section).map(([key, value]) => [
|
|
2359
|
+
key,
|
|
2360
|
+
typeof value === "string" && value.startsWith("docs/")
|
|
2361
|
+
? `${input.governanceRoot}/${value.slice("docs/".length)}`
|
|
2362
|
+
: value,
|
|
2363
|
+
]));
|
|
2364
|
+
}
|
|
2365
|
+
await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
|
|
2366
|
+
return true;
|
|
2367
|
+
}
|
|
2368
|
+
if (input.action.type === "migrate-owned-file") {
|
|
2369
|
+
if (!input.action.targetPath)
|
|
2370
|
+
return false;
|
|
2371
|
+
const source = path.join(input.repoRoot, input.action.path);
|
|
2372
|
+
const target = path.join(input.repoRoot, input.action.targetPath);
|
|
2373
|
+
if (!(await exists(source)) || (await exists(target)))
|
|
2374
|
+
return false;
|
|
2375
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
2376
|
+
await rename(source, target);
|
|
2377
|
+
return true;
|
|
2378
|
+
}
|
|
2379
|
+
if (input.action.type === "remove-owned-file") {
|
|
2380
|
+
const target = path.join(input.repoRoot, input.action.path);
|
|
2381
|
+
if (!(await exists(target)))
|
|
2382
|
+
return false;
|
|
2383
|
+
await rm(target, { force: true });
|
|
2384
|
+
return true;
|
|
2385
|
+
}
|
|
2386
|
+
if (input.action.type === "remove-empty-directory") {
|
|
2387
|
+
const target = path.join(input.repoRoot, input.action.path);
|
|
2388
|
+
if (!(await isDirectoryEmpty(target)))
|
|
2389
|
+
return false;
|
|
2390
|
+
await rmdir(target);
|
|
2391
|
+
return true;
|
|
2392
|
+
}
|
|
2030
2393
|
const assetRoot = await findPackageRoot();
|
|
2031
2394
|
const entry = {
|
|
2032
2395
|
path: input.action.path,
|
|
2033
|
-
mode: input.action.
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2396
|
+
mode: input.action.mode ??
|
|
2397
|
+
(input.action.type === "create-directory"
|
|
2398
|
+
? "directory"
|
|
2399
|
+
: input.action.type === "copy-missing"
|
|
2400
|
+
? "copied"
|
|
2401
|
+
: input.action.type === "refresh-managed-block"
|
|
2402
|
+
? "managed-block"
|
|
2403
|
+
: "generated"),
|
|
2040
2404
|
};
|
|
2041
2405
|
const target = path.join(input.repoRoot, input.action.path);
|
|
2042
2406
|
if (input.action.type === "create-directory") {
|
|
@@ -2067,6 +2431,10 @@ async function applySafeAction(input) {
|
|
|
2067
2431
|
await writeFile(target, next, "utf-8");
|
|
2068
2432
|
return true;
|
|
2069
2433
|
}
|
|
2434
|
+
if (input.action.type === "refresh-owned-file") {
|
|
2435
|
+
await writeFile(target, desired.content, "utf-8");
|
|
2436
|
+
return true;
|
|
2437
|
+
}
|
|
2070
2438
|
if (await exists(target))
|
|
2071
2439
|
return false;
|
|
2072
2440
|
await writeFile(target, desired.content, "utf-8");
|
|
@@ -2096,7 +2464,14 @@ export async function applyInitUpdate(input) {
|
|
|
2096
2464
|
skipped.push(action);
|
|
2097
2465
|
continue;
|
|
2098
2466
|
}
|
|
2099
|
-
if (action.type !== "strip-pi-requires-api-key" &&
|
|
2467
|
+
if (action.type !== "strip-pi-requires-api-key" &&
|
|
2468
|
+
action.type !== "add-harness-schema-ref" &&
|
|
2469
|
+
action.type !== "refresh-owned-file" &&
|
|
2470
|
+
action.type !== "migrate-harness-governance-root" &&
|
|
2471
|
+
action.type !== "migrate-harness-model-fields" &&
|
|
2472
|
+
action.type !== "migrate-owned-file" &&
|
|
2473
|
+
action.type !== "remove-owned-file" &&
|
|
2474
|
+
action.type !== "remove-empty-directory") {
|
|
2100
2475
|
const currentState = await buildCurrentSurfaceState({
|
|
2101
2476
|
repoRoot,
|
|
2102
2477
|
projectName,
|
|
@@ -2180,8 +2555,7 @@ export async function runInitDoctor(input) {
|
|
|
2180
2555
|
add("AGENTS.md loop-agent block", (await exists(agents)) && (await readFile(agents, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
|
|
2181
2556
|
const readme = path.join(repoRoot, "README.md");
|
|
2182
2557
|
add("README loop-agent block", (await exists(readme)) && (await readFile(readme, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
|
|
2183
|
-
add("repo-local skills", await exists(path.join(repoRoot, "skills", "loop-agent", "SKILL.md")), "skills/loop-agent/SKILL.md");
|
|
2184
|
-
add("agent-compatible skills mirror", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
|
|
2558
|
+
add("repo-local skills", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
|
|
2185
2559
|
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
2186
2560
|
const gitignoreContent = (await exists(gitignorePath)) ? await readFile(gitignorePath, "utf-8") : "";
|
|
2187
2561
|
add("gitignore loop-agent block", gitignoreContent.includes(GITIGNORE_BLOCK_START) && gitignoreContent.includes(".harness/tasks/*"), ".gitignore managed runtime ignores");
|
|
@@ -2203,7 +2577,7 @@ export async function runInitDoctor(input) {
|
|
|
2203
2577
|
function parseInitArgs(repoRoot, args) {
|
|
2204
2578
|
let subcommand;
|
|
2205
2579
|
let projectName;
|
|
2206
|
-
let governanceRoot
|
|
2580
|
+
let governanceRoot;
|
|
2207
2581
|
let profile = "full";
|
|
2208
2582
|
let merge = true;
|
|
2209
2583
|
let provider;
|
|
@@ -2326,7 +2700,7 @@ function formatInitResult(result) {
|
|
|
2326
2700
|
`written: ${result.written.length}`,
|
|
2327
2701
|
`skipped: ${result.skipped.length}`,
|
|
2328
2702
|
"next: loop-agent init doctor --repo-root <target>",
|
|
2329
|
-
|
|
2703
|
+
`model-executed init: continue automatically by inspecting the target project, adapting README.md, ${result.governanceRoot}/verification-matrix.md, and scripts/ci-tests.sh when needed, then run inspect/docs audit/quick verification.`,
|
|
2330
2704
|
].join("\n");
|
|
2331
2705
|
}
|
|
2332
2706
|
export async function listInitializedFiles(repoRoot) {
|