@tea-agent/loop-agent 0.4.0 → 0.6.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 +48 -38
- package/README.md +3 -3
- package/dist/application/dag/args.js +9 -1
- package/dist/application/dag/run-dag.js +16 -2
- package/dist/application/dag/validate-dag.js +14 -1
- package/dist/cli/command-definitions.js +22 -4
- package/dist/cli/help.js +3 -2
- package/dist/cli/program.js +7 -5
- package/dist/commands/import-prd.js +76 -0
- package/dist/commands/init.js +230 -32
- package/dist/commands/instructions.js +90 -58
- package/dist/executors/config-core.js +3 -2
- package/dist/executors/dag-pi-executor.js +1 -0
- package/dist/executors/model-routing.js +43 -0
- package/dist/executors/pi-sdk-executor.js +63 -1
- package/dist/governance/manifest-types.js +9 -1
- package/dist/shared/preview.js +39 -0
- package/dist/task/source-references.js +221 -0
- package/dist/worker/cli.js +62 -1
- package/dist/worker/loop-agent/loop-agent-client.js +97 -5
- package/dist/worker/materialize/harness-task-materializer.js +162 -5
- package/dist/worker/observability/event-store.js +82 -0
- package/dist/worker/observability/events.js +79 -0
- package/dist/worker/observability/progress-composite.js +33 -0
- package/dist/worker/observability/read-model.js +1013 -0
- package/dist/worker/observability/snapshot-store.js +43 -0
- package/dist/worker/observability/types.js +1 -0
- package/dist/worker/observe/paths.js +64 -0
- package/dist/worker/observe/routes.js +423 -0
- package/dist/worker/observe/server.js +61 -0
- package/dist/worker/observe/static/app.js +1419 -0
- package/dist/worker/observe/static/index.html +63 -0
- package/dist/worker/observe/static/styles.css +613 -0
- package/dist/worker/pool/failure-routing.js +41 -6
- package/dist/worker/pool/run-store.js +59 -1
- package/dist/worker/progress-reporter.js +0 -18
- package/dist/worker/run-task/run-task.js +327 -92
- package/dist/worker/runner/run-ready.js +112 -4
- package/dist/workflows/dag/event-observer.js +132 -0
- package/dist/workflows/dag/init-hybrid.js +150 -26
- package/dist/workflows/dag/observer-compose.js +52 -0
- package/dist/workflows/dag/skill-instructions.js +4 -0
- package/dist/workflows/dag/types.js +1 -1
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/README.md +2 -0
- package/docs/architecture/runtime-boundaries.md +18 -3
- package/docs/design/README.md +22 -9
- package/docs/exec-plans/active/README.md +6 -1
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/init-surface.manifest.json +32 -2
- package/docs/loop-agent-harness.md +13 -0
- package/docs/reports/README.md +4 -0
- package/docs/templates/agent-dag.base.json +1 -1
- package/docs/templates/agent-dag.final-verification.json +1 -1
- package/docs/templates/agent-dag.supervised-implementation.json +1 -1
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/worker-dogfood-evidence.md +52 -0
- package/docs/templates/worker-dogfood-setup.md +48 -0
- package/examples/example-dag.json +1 -1
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/harness.json +5 -29
- package/package.json +6 -6
- package/skills/loop-agent/SKILL.md +5 -3
- package/skills/loop-agent/references/command-reference.md +12 -3
- package/skills/loop-agent/references/harness-policy.md +7 -3
- package/skills/loop-agent/references/task-workflow.md +8 -3
package/dist/commands/init.js
CHANGED
|
@@ -6,6 +6,8 @@ import { copyDir } from "../shared/copy-dir.js";
|
|
|
6
6
|
import { loadHarnessManifest } from "../governance/harness.js";
|
|
7
7
|
const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
|
|
8
8
|
const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
|
|
9
|
+
const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
|
|
10
|
+
const GITIGNORE_BLOCK_END = "# LOOP_AGENT_INIT_END";
|
|
9
11
|
const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
|
|
10
12
|
const CORE_DOC_FILES = [
|
|
11
13
|
"README.md",
|
|
@@ -720,17 +722,88 @@ function mergeManagedBlock(existing, block) {
|
|
|
720
722
|
}
|
|
721
723
|
return `${existing.trimEnd()}\n\n${block}\n`;
|
|
722
724
|
}
|
|
725
|
+
function mergeGitignoreManagedBlock(existing, block) {
|
|
726
|
+
const start = existing.indexOf(GITIGNORE_BLOCK_START);
|
|
727
|
+
const end = existing.indexOf(GITIGNORE_BLOCK_END);
|
|
728
|
+
if (start >= 0 && end > start) {
|
|
729
|
+
return `${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(end + GITIGNORE_BLOCK_END.length).trimStart()}`.trimEnd() + "\n";
|
|
730
|
+
}
|
|
731
|
+
if (!existing.trim())
|
|
732
|
+
return `${block}\n`;
|
|
733
|
+
return `${existing.trimEnd()}\n\n${block}\n`;
|
|
734
|
+
}
|
|
735
|
+
/** Shared ignore rules for loop-agent runtime facts; keep prompts/ and directory structure shareable. */
|
|
736
|
+
export function buildManagedGitignoreBlock() {
|
|
737
|
+
return [
|
|
738
|
+
GITIGNORE_BLOCK_START,
|
|
739
|
+
"# loop-agent runtime: ignore personal/session facts; keep prompts and directory placeholders shareable",
|
|
740
|
+
"",
|
|
741
|
+
"# tasks (source + runtime state per developer/session)",
|
|
742
|
+
".harness/tasks/*",
|
|
743
|
+
"!.harness/tasks/.gitkeep",
|
|
744
|
+
"",
|
|
745
|
+
"# DAG runs",
|
|
746
|
+
".harness/dag-runs/active/*",
|
|
747
|
+
"!.harness/dag-runs/active/.gitkeep",
|
|
748
|
+
".harness/dag-runs/completed/*",
|
|
749
|
+
"!.harness/dag-runs/completed/.gitkeep",
|
|
750
|
+
".harness/dag-runs/paused/*",
|
|
751
|
+
"!.harness/dag-runs/paused/.gitkeep",
|
|
752
|
+
"",
|
|
753
|
+
"# one-shot executor runs",
|
|
754
|
+
".harness/runs/*",
|
|
755
|
+
"!.harness/runs/.gitkeep",
|
|
756
|
+
"",
|
|
757
|
+
"# session / cache / logs / recomputable surface state",
|
|
758
|
+
".harness/live/",
|
|
759
|
+
".harness/cache/",
|
|
760
|
+
".harness/*.log",
|
|
761
|
+
".harness/init-surface.json",
|
|
762
|
+
"",
|
|
763
|
+
"# worker task pool",
|
|
764
|
+
".task-pool/",
|
|
765
|
+
"",
|
|
766
|
+
"# multi-worktree parallel mode",
|
|
767
|
+
".worktrees/",
|
|
768
|
+
GITIGNORE_BLOCK_END,
|
|
769
|
+
].join("\n");
|
|
770
|
+
}
|
|
771
|
+
function extractGitignoreManagedBlock(content) {
|
|
772
|
+
const start = content.indexOf(GITIGNORE_BLOCK_START);
|
|
773
|
+
const end = content.indexOf(GITIGNORE_BLOCK_END);
|
|
774
|
+
if (start < 0 || end <= start)
|
|
775
|
+
return undefined;
|
|
776
|
+
return content.slice(start, end + GITIGNORE_BLOCK_END.length);
|
|
777
|
+
}
|
|
723
778
|
function buildHarness(input) {
|
|
724
|
-
const
|
|
725
|
-
? {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
779
|
+
const templateExecutors = isRecord(input.template.executors)
|
|
780
|
+
? { ...input.template.executors }
|
|
781
|
+
: {};
|
|
782
|
+
const existingExecutors = isRecord(input.existing.executors)
|
|
783
|
+
? input.existing.executors
|
|
784
|
+
: {};
|
|
785
|
+
// Cursor executor is opt-in for package developers; do not project it to target apps.
|
|
786
|
+
// Keep it only when the target already configured executors.cursor.
|
|
787
|
+
if (!isRecord(existingExecutors.cursor)) {
|
|
788
|
+
delete templateExecutors.cursor;
|
|
789
|
+
}
|
|
790
|
+
const mergedExecutors = mergeRecord(templateExecutors, existingExecutors);
|
|
791
|
+
// requiresApiKey is Cursor-facing only; never project it on pi.
|
|
792
|
+
if (isRecord(mergedExecutors.pi) && "requiresApiKey" in mergedExecutors.pi) {
|
|
793
|
+
const pi = { ...mergedExecutors.pi };
|
|
794
|
+
delete pi.requiresApiKey;
|
|
795
|
+
mergedExecutors.pi = pi;
|
|
796
|
+
}
|
|
797
|
+
// Prefer DAG-facing executors.*.defaultModel over legacy top-level models.*.
|
|
798
|
+
// --provider/--model still apply as a convenience override for pi defaultModel.
|
|
799
|
+
if (input.provider || input.model) {
|
|
800
|
+
const piExisting = isRecord(mergedExecutors.pi) ? mergedExecutors.pi : {};
|
|
801
|
+
mergedExecutors.pi = {
|
|
802
|
+
...piExisting,
|
|
803
|
+
...(input.model ? { defaultModel: input.model } : {}),
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const harness = {
|
|
734
807
|
...input.existing,
|
|
735
808
|
version: 1,
|
|
736
809
|
project: input.projectName,
|
|
@@ -753,6 +826,7 @@ function buildHarness(input) {
|
|
|
753
826
|
progressDir: `${input.governanceRoot}/progress`,
|
|
754
827
|
reportsDir: `${input.governanceRoot}/reports`,
|
|
755
828
|
decisionsDir: `${input.governanceRoot}/decisions`,
|
|
829
|
+
taskPoolDir: ".task-pool",
|
|
756
830
|
}),
|
|
757
831
|
scripts: mergeRecord(input.existing.scripts, {
|
|
758
832
|
checkEngineeringStructure: "scripts/check-engineering-structure.sh",
|
|
@@ -771,11 +845,30 @@ function buildHarness(input) {
|
|
|
771
845
|
standardVerify: "bash scripts/ci-governance.sh",
|
|
772
846
|
fullVerify: "bash scripts/ci.sh",
|
|
773
847
|
}),
|
|
774
|
-
|
|
775
|
-
modelProfiles: input.existing.modelProfiles ?? input.template.modelProfiles ?? {},
|
|
776
|
-
modelRouting: input.existing.modelRouting ?? input.template.modelRouting ?? {},
|
|
777
|
-
executors: mergeRecord(input.template.executors, input.existing.executors),
|
|
848
|
+
executors: mergedExecutors,
|
|
778
849
|
};
|
|
850
|
+
// Do not project legacy step-routing models / modelProfiles / modelRouting into
|
|
851
|
+
// new or refreshed harness.json. Keep them only when the target already has them
|
|
852
|
+
// so old files remain loadable until the user migrates.
|
|
853
|
+
if (isRecord(input.existing.models) && Object.keys(input.existing.models).length > 0) {
|
|
854
|
+
harness.models = input.existing.models;
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
delete harness.models;
|
|
858
|
+
}
|
|
859
|
+
if (isRecord(input.existing.modelProfiles) && Object.keys(input.existing.modelProfiles).length > 0) {
|
|
860
|
+
harness.modelProfiles = input.existing.modelProfiles;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
delete harness.modelProfiles;
|
|
864
|
+
}
|
|
865
|
+
if (isRecord(input.existing.modelRouting) && Object.keys(input.existing.modelRouting).length > 0) {
|
|
866
|
+
harness.modelRouting = input.existing.modelRouting;
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
delete harness.modelRouting;
|
|
870
|
+
}
|
|
871
|
+
return harness;
|
|
779
872
|
}
|
|
780
873
|
async function writeTextIfMissing(input) {
|
|
781
874
|
const target = path.join(input.repoRoot, input.relativePath);
|
|
@@ -899,8 +992,11 @@ function inferInitSurfaceMode(relativePath) {
|
|
|
899
992
|
relativePath.endsWith("/")) {
|
|
900
993
|
return "directory";
|
|
901
994
|
}
|
|
902
|
-
if (relativePath === "README.md" ||
|
|
995
|
+
if (relativePath === "README.md" ||
|
|
996
|
+
relativePath === "AGENTS.md" ||
|
|
997
|
+
relativePath === ".gitignore") {
|
|
903
998
|
return "managed-block";
|
|
999
|
+
}
|
|
904
1000
|
if (relativePath === "harness.json" ||
|
|
905
1001
|
relativePath.startsWith("scripts/") ||
|
|
906
1002
|
relativePath.startsWith(".harness/prompts/") ||
|
|
@@ -934,6 +1030,9 @@ async function buildDesiredSurfaceContent(input) {
|
|
|
934
1030
|
}),
|
|
935
1031
|
};
|
|
936
1032
|
}
|
|
1033
|
+
if (manifestPath === ".gitignore") {
|
|
1034
|
+
return { content: buildManagedGitignoreBlock() };
|
|
1035
|
+
}
|
|
937
1036
|
if (manifestPath === "harness.json") {
|
|
938
1037
|
const template = await readJsonIfExists(path.join(input.assetRoot, "harness.json"));
|
|
939
1038
|
return {
|
|
@@ -953,6 +1052,16 @@ async function buildDesiredSurfaceContent(input) {
|
|
|
953
1052
|
const name = path.basename(manifestPath);
|
|
954
1053
|
return { content: COMPAT_PROMPTS[name] };
|
|
955
1054
|
}
|
|
1055
|
+
// `.agents/skills/...` is a target-only mirror of the bundled `skills/...`
|
|
1056
|
+
// directory; the package ships `skills/`, never `.agents/skills/`.
|
|
1057
|
+
if (manifestPath.startsWith(".agents/skills/")) {
|
|
1058
|
+
const sourceManifestPath = manifestPath.slice(".agents/".length);
|
|
1059
|
+
const mirrorSourcePath = path.join(input.assetRoot, sourceManifestPath);
|
|
1060
|
+
if (await exists(mirrorSourcePath)) {
|
|
1061
|
+
return { content: await readFile(mirrorSourcePath, "utf-8"), sourcePath: mirrorSourcePath };
|
|
1062
|
+
}
|
|
1063
|
+
return {};
|
|
1064
|
+
}
|
|
956
1065
|
if (manifestPath.startsWith("docs/")) {
|
|
957
1066
|
const doc = manifestPath.slice("docs/".length);
|
|
958
1067
|
const generated = buildGeneratedCoreDoc({
|
|
@@ -1031,7 +1140,10 @@ async function buildCurrentSurfaceState(input) {
|
|
|
1031
1140
|
const current = await readFile(targetPath);
|
|
1032
1141
|
const currentSha256 = sha256Buffer(current);
|
|
1033
1142
|
if (entry.mode === "managed-block") {
|
|
1034
|
-
const
|
|
1143
|
+
const text = current.toString("utf-8");
|
|
1144
|
+
const currentBlock = entry.path === ".gitignore" || targetRelativePath === ".gitignore"
|
|
1145
|
+
? extractGitignoreManagedBlock(text)
|
|
1146
|
+
: extractManagedBlock(text);
|
|
1035
1147
|
files[targetRelativePath] = {
|
|
1036
1148
|
...base,
|
|
1037
1149
|
currentSha256,
|
|
@@ -1194,6 +1306,8 @@ function buildTargetDocsReadme(input) {
|
|
|
1194
1306
|
"- `decisions/README.md` - 架构决策 / architecture decisions",
|
|
1195
1307
|
"- `templates/` - 可复用的计划、报告与 DAG 模板 / reusable planning, reporting, and DAG templates",
|
|
1196
1308
|
"- `templates/production-readiness-checklist.md` - 低/中风险单仓库 DAG 任务的 production readiness 检查清单 / production readiness checklist for low/medium-risk single-repo DAG work",
|
|
1309
|
+
"- `templates/worker-dogfood-setup.md` - 发布控制器下的真实 Worker sample setup / real Worker sample setup with a published controller",
|
|
1310
|
+
"- `templates/worker-dogfood-evidence.md` - Worker sample、Observe、morning report 与 QA coverage evidence / Worker evidence template",
|
|
1197
1311
|
"",
|
|
1198
1312
|
"## 验证 / Verification",
|
|
1199
1313
|
"",
|
|
@@ -1228,6 +1342,9 @@ function buildTargetRuntimeBoundaries(input) {
|
|
|
1228
1342
|
"Executors / Integrations layer",
|
|
1229
1343
|
" └─ 外部工具、SDK、数据库、消息队列、浏览器、模型或 shell 适配",
|
|
1230
1344
|
"",
|
|
1345
|
+
"Worker adapter layer (optional)",
|
|
1346
|
+
" └─ 产品线 TaskSpec / Task Pool / local Observe 适配;通过已发布 loop-agent CLI 执行,不在进程内耦合 target command 或 application 层",
|
|
1347
|
+
"",
|
|
1231
1348
|
"Infrastructure / Store layer",
|
|
1232
1349
|
" └─ 文件系统、数据库、缓存、运行事实、原子写入和生命周期副作用",
|
|
1233
1350
|
"",
|
|
@@ -1241,6 +1358,7 @@ function buildTargetRuntimeBoundaries(input) {
|
|
|
1241
1358
|
"- Application / Use-case layer 可以依赖 Domain / Workflow、Infrastructure 和 Integrations。",
|
|
1242
1359
|
"- Domain / Workflow layer 不应依赖 Entry layer 的格式化、argv、HTTP/UI 细节。",
|
|
1243
1360
|
"- Executors / Integrations 不应依赖 Entry layer 的输出格式。",
|
|
1361
|
+
"- Worker adapter 应把 `.task-pool/` 作为独立运行事实区;它通过 CLI/subprocess contract 调用 loop-agent,不能复制或直接耦合 target command/application 实现。",
|
|
1244
1362
|
"- Infrastructure / Store 应集中副作用,不把 raw path mutation 或持久化细节扩散给上层。",
|
|
1245
1363
|
"",
|
|
1246
1364
|
"## 目标项目适配",
|
|
@@ -1382,6 +1500,7 @@ function buildTargetLoopAgentHarness(input) {
|
|
|
1382
1500
|
"- `.harness/tasks/` stores task state and source materials.",
|
|
1383
1501
|
"- `.harness/dag-runs/` stores DAG run facts.",
|
|
1384
1502
|
"- `.harness/runs/` stores one-shot executor facts.",
|
|
1503
|
+
"- `.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.",
|
|
1385
1504
|
"- `docs/` stores durable governance, plans, reports, progress, and decisions.",
|
|
1386
1505
|
"- `skills/` stores repo-local skill instructions; the CLI can fall back to bundled skills when needed.",
|
|
1387
1506
|
"",
|
|
@@ -1395,6 +1514,8 @@ function buildTargetLoopAgentHarness(input) {
|
|
|
1395
1514
|
"",
|
|
1396
1515
|
"Use `docs/templates/production-readiness-checklist.md` when claiming Production Readiness v0.1 for low/medium-risk single-repo DAG work.",
|
|
1397
1516
|
"",
|
|
1517
|
+
"For real product-line Worker samples, use `docs/templates/worker-dogfood-setup.md` and `docs/templates/worker-dogfood-evidence.md`. Explicit failed-task retries must preserve prior evidence and use a new worker run id.",
|
|
1518
|
+
"",
|
|
1398
1519
|
"## Script Matrix",
|
|
1399
1520
|
"",
|
|
1400
1521
|
"`scripts/check-repo.sh` verifies loop-agent governance. `scripts/ci-tests.sh` handles target project verification through conservative language/toolchain detection and should be adapted after reading the project.",
|
|
@@ -1462,7 +1583,8 @@ export function buildInitInstructions(input) {
|
|
|
1462
1583
|
"- 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.",
|
|
1463
1584
|
"- 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.",
|
|
1464
1585
|
"- Populate `docs/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.",
|
|
1465
|
-
"- Copy repo-local skills by default so the target repo has auditable skill instructions.",
|
|
1586
|
+
"- Copy repo-local skills by default so the target repo has auditable skill instructions; full profile also mirrors skills into `.agents/skills/` for external agents.",
|
|
1587
|
+
"- Merge a loop-agent managed block into `.gitignore` that ignores harness runtime facts (tasks, dag-runs, runs, live, cache, init-surface.json, .task-pool) while keeping prompts and directory placeholders shareable.",
|
|
1466
1588
|
"- Do not copy examples by default; examples stay bundled in the tool and are available through `loop-agent examples`.",
|
|
1467
1589
|
"- Add or update a loop-agent managed block in AGENTS.md.",
|
|
1468
1590
|
"- The generated AGENTS.md must include documentation convergence and structured DAG write-boundary rules so target projects keep the same working discipline as this repository.",
|
|
@@ -1556,6 +1678,18 @@ export async function initializeLoopAgentProject(options) {
|
|
|
1556
1678
|
written,
|
|
1557
1679
|
skipped,
|
|
1558
1680
|
});
|
|
1681
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
1682
|
+
const existingGitignore = (await exists(gitignorePath))
|
|
1683
|
+
? await readFile(gitignorePath, "utf-8")
|
|
1684
|
+
: "";
|
|
1685
|
+
await writeText({
|
|
1686
|
+
repoRoot,
|
|
1687
|
+
relativePath: ".gitignore",
|
|
1688
|
+
content: mergeGitignoreManagedBlock(existingGitignore, buildManagedGitignoreBlock()),
|
|
1689
|
+
merge: true,
|
|
1690
|
+
written,
|
|
1691
|
+
skipped,
|
|
1692
|
+
});
|
|
1559
1693
|
for (const doc of CORE_DOC_FILES) {
|
|
1560
1694
|
const generated = buildGeneratedCoreDoc({ doc, projectName, governanceRoot });
|
|
1561
1695
|
if (generated) {
|
|
@@ -1604,9 +1738,21 @@ export async function initializeLoopAgentProject(options) {
|
|
|
1604
1738
|
targetRelativePath: "skills",
|
|
1605
1739
|
written,
|
|
1606
1740
|
});
|
|
1741
|
+
// Mirror bundled skills into the agent-compatible `.agents/skills/` path so
|
|
1742
|
+
// external agents (e.g. OpenCode) that auto-discover `.agents/skills/<name>/SKILL.md`
|
|
1743
|
+
// share the same skill content as loop-agent's primary `skills/` path.
|
|
1744
|
+
// The package still ships only `skills/`; `.agents/skills` is a target projection.
|
|
1745
|
+
await copyDirMerge({
|
|
1746
|
+
assetRoot,
|
|
1747
|
+
repoRoot,
|
|
1748
|
+
sourceRelativePath: "skills",
|
|
1749
|
+
targetRelativePath: ".agents/skills",
|
|
1750
|
+
written,
|
|
1751
|
+
});
|
|
1607
1752
|
}
|
|
1608
1753
|
else {
|
|
1609
1754
|
skipped.push("skills/");
|
|
1755
|
+
skipped.push(".agents/skills/");
|
|
1610
1756
|
}
|
|
1611
1757
|
skipped.push("examples/");
|
|
1612
1758
|
for (const [relativePath, content] of Object.entries(buildInitScriptFiles(governanceRoot))) {
|
|
@@ -1736,6 +1882,27 @@ export async function checkInitUpdate(input) {
|
|
|
1736
1882
|
}
|
|
1737
1883
|
}
|
|
1738
1884
|
}
|
|
1885
|
+
// Safe harness hygiene: strip obsolete pi.requiresApiKey without full harness rewrite.
|
|
1886
|
+
// Cursor requiresApiKey is intentionally kept when present.
|
|
1887
|
+
const harnessPath = path.join(repoRoot, "harness.json");
|
|
1888
|
+
if (await exists(harnessPath)) {
|
|
1889
|
+
try {
|
|
1890
|
+
const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
|
|
1891
|
+
if (isRecord(harness) &&
|
|
1892
|
+
isRecord(harness.executors) &&
|
|
1893
|
+
isRecord(harness.executors.pi) &&
|
|
1894
|
+
Object.prototype.hasOwnProperty.call(harness.executors.pi, "requiresApiKey")) {
|
|
1895
|
+
deterministicActions.push({
|
|
1896
|
+
type: "strip-pi-requires-api-key",
|
|
1897
|
+
path: "harness.json",
|
|
1898
|
+
reason: "executors.pi.requiresApiKey is unused; strip it while keeping user model config and cursor.requiresApiKey",
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
catch {
|
|
1903
|
+
// leave malformed harness for model merge / doctor
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1739
1906
|
const partial = {
|
|
1740
1907
|
repoRoot,
|
|
1741
1908
|
controllerVersion: currentState.controllerVersion,
|
|
@@ -1759,6 +1926,29 @@ export async function checkInitUpdate(input) {
|
|
|
1759
1926
|
};
|
|
1760
1927
|
}
|
|
1761
1928
|
async function applySafeAction(input) {
|
|
1929
|
+
if (input.action.type === "strip-pi-requires-api-key") {
|
|
1930
|
+
const target = path.join(input.repoRoot, "harness.json");
|
|
1931
|
+
if (!(await exists(target)))
|
|
1932
|
+
return false;
|
|
1933
|
+
const harness = JSON.parse(await readFile(target, "utf-8"));
|
|
1934
|
+
if (!isRecord(harness) || !isRecord(harness.executors) || !isRecord(harness.executors.pi)) {
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
if (!Object.prototype.hasOwnProperty.call(harness.executors.pi, "requiresApiKey")) {
|
|
1938
|
+
return false;
|
|
1939
|
+
}
|
|
1940
|
+
const nextPi = { ...harness.executors.pi };
|
|
1941
|
+
delete nextPi.requiresApiKey;
|
|
1942
|
+
const next = {
|
|
1943
|
+
...harness,
|
|
1944
|
+
executors: {
|
|
1945
|
+
...harness.executors,
|
|
1946
|
+
pi: nextPi,
|
|
1947
|
+
},
|
|
1948
|
+
};
|
|
1949
|
+
await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1762
1952
|
const assetRoot = await findPackageRoot();
|
|
1763
1953
|
const entry = {
|
|
1764
1954
|
path: input.action.path,
|
|
@@ -1789,11 +1979,13 @@ async function applySafeAction(input) {
|
|
|
1789
1979
|
await mkdir(path.dirname(target), { recursive: true });
|
|
1790
1980
|
if (input.action.type === "refresh-managed-block") {
|
|
1791
1981
|
const existing = (await exists(target)) ? await readFile(target, "utf-8") : "";
|
|
1792
|
-
const next = input.action.path === "
|
|
1793
|
-
?
|
|
1794
|
-
: input.action.path === "
|
|
1795
|
-
?
|
|
1796
|
-
:
|
|
1982
|
+
const next = input.action.path === ".gitignore"
|
|
1983
|
+
? mergeGitignoreManagedBlock(existing, desired.content)
|
|
1984
|
+
: input.action.path === "README.md" && existing.trim().length === 0
|
|
1985
|
+
? buildTargetReadme({ projectName: input.projectName, governanceRoot: input.governanceRoot })
|
|
1986
|
+
: input.action.path === "AGENTS.md" && existing.trim().length === 0
|
|
1987
|
+
? `# AGENTS.md\n\n${desired.content}\n`
|
|
1988
|
+
: mergeManagedBlock(existing, desired.content);
|
|
1797
1989
|
await writeFile(target, next, "utf-8");
|
|
1798
1990
|
return true;
|
|
1799
1991
|
}
|
|
@@ -1826,16 +2018,18 @@ export async function applyInitUpdate(input) {
|
|
|
1826
2018
|
skipped.push(action);
|
|
1827
2019
|
continue;
|
|
1828
2020
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2021
|
+
if (action.type !== "strip-pi-requires-api-key") {
|
|
2022
|
+
const currentState = await buildCurrentSurfaceState({
|
|
2023
|
+
repoRoot,
|
|
2024
|
+
projectName,
|
|
2025
|
+
governanceRoot,
|
|
2026
|
+
stateKind: "inferred-baseline",
|
|
2027
|
+
});
|
|
2028
|
+
const relationship = currentState.files[action.path]?.relationship;
|
|
2029
|
+
if (relationship === "local-existing-unknown") {
|
|
2030
|
+
skipped.push(action);
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
1839
2033
|
}
|
|
1840
2034
|
if (await applySafeAction({ repoRoot, projectName, governanceRoot, action }))
|
|
1841
2035
|
applied.push(action);
|
|
@@ -1909,6 +2103,10 @@ export async function runInitDoctor(input) {
|
|
|
1909
2103
|
const readme = path.join(repoRoot, "README.md");
|
|
1910
2104
|
add("README loop-agent block", (await exists(readme)) && (await readFile(readme, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
|
|
1911
2105
|
add("repo-local skills", await exists(path.join(repoRoot, "skills", "loop-agent", "SKILL.md")), "skills/loop-agent/SKILL.md");
|
|
2106
|
+
add("agent-compatible skills mirror", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
|
|
2107
|
+
const gitignorePath = path.join(repoRoot, ".gitignore");
|
|
2108
|
+
const gitignoreContent = (await exists(gitignorePath)) ? await readFile(gitignorePath, "utf-8") : "";
|
|
2109
|
+
add("gitignore loop-agent block", gitignoreContent.includes(GITIGNORE_BLOCK_START) && gitignoreContent.includes(".harness/tasks/*"), ".gitignore managed runtime ignores");
|
|
1912
2110
|
const requiredScripts = Object.keys(INIT_SCRIPT_FILES);
|
|
1913
2111
|
const missingScripts = [];
|
|
1914
2112
|
for (const script of requiredScripts) {
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { access, readdir } from
|
|
2
|
-
import path from
|
|
3
|
-
import { getArtifactPath, getArtifactRelativePath, } from
|
|
4
|
-
import { defaultHybridDagOutputPath } from
|
|
5
|
-
import { getTaskDir, getTaskPaths, loadTaskConfig } from
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getArtifactPath, getArtifactRelativePath, } from "../shared/artifacts-core.js";
|
|
4
|
+
import { defaultHybridDagOutputPath } from "../workflows/dag/init-hybrid.js";
|
|
5
|
+
import { getTaskDir, getTaskPaths, loadTaskConfig } from "../task/runtime.js";
|
|
6
6
|
const STAGES = new Set([
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
"source",
|
|
8
|
+
"dag-draft",
|
|
9
|
+
"task-artifacts",
|
|
10
|
+
"promotion",
|
|
11
|
+
"closeout",
|
|
12
12
|
]);
|
|
13
13
|
export function parseInstructionsArgs(args) {
|
|
14
14
|
const [stageRaw, ...rest] = args;
|
|
15
15
|
if (!stageRaw || !STAGES.has(stageRaw)) {
|
|
16
|
-
throw new Error(
|
|
16
|
+
throw new Error("usage: instructions <source|dag-draft|task-artifacts|promotion|closeout> --task <task-id> [--json]");
|
|
17
17
|
}
|
|
18
18
|
let taskId;
|
|
19
19
|
let json = false;
|
|
20
20
|
for (let i = 0; i < rest.length; i += 1) {
|
|
21
21
|
const arg = rest[i];
|
|
22
|
-
if (arg ===
|
|
22
|
+
if (arg === "--task") {
|
|
23
23
|
taskId = rest[++i];
|
|
24
24
|
if (!taskId)
|
|
25
|
-
throw new Error(
|
|
25
|
+
throw new Error("instructions --task requires a task id");
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
|
-
if (arg.startsWith(
|
|
29
|
-
taskId = arg.slice(
|
|
28
|
+
if (arg.startsWith("--task=")) {
|
|
29
|
+
taskId = arg.slice("--task=".length);
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
|
-
if (arg ===
|
|
32
|
+
if (arg === "--json") {
|
|
33
33
|
json = true;
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
36
|
throw new Error(`unknown instructions flag: ${arg}`);
|
|
37
37
|
}
|
|
38
38
|
if (!taskId)
|
|
39
|
-
throw new Error(
|
|
39
|
+
throw new Error("instructions requires --task <task-id>");
|
|
40
40
|
return { stage: stageRaw, taskId, json };
|
|
41
41
|
}
|
|
42
42
|
export async function runInstructions(repoRoot, args) {
|
|
@@ -54,13 +54,13 @@ export async function buildInstructions(repoRoot, stage, taskId) {
|
|
|
54
54
|
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
55
55
|
const allowedEditRoots = taskConfig.allowedPaths.length > 0
|
|
56
56
|
? taskConfig.allowedPaths
|
|
57
|
-
: [path.relative(repoRoot, taskDir).replace(/\\/g,
|
|
57
|
+
: [path.relative(repoRoot, taskDir).replace(/\\/g, "/")];
|
|
58
58
|
const writePolicy = {
|
|
59
|
-
mode: stage ===
|
|
60
|
-
?
|
|
61
|
-
: stage ===
|
|
62
|
-
?
|
|
63
|
-
:
|
|
59
|
+
mode: stage === "promotion"
|
|
60
|
+
? "run-evidence-read-only"
|
|
61
|
+
: stage === "closeout"
|
|
62
|
+
? "docs-progress"
|
|
63
|
+
: "task-owned",
|
|
64
64
|
allowedEditRoots,
|
|
65
65
|
forbiddenPaths: taskConfig.forbiddenPaths,
|
|
66
66
|
};
|
|
@@ -70,74 +70,106 @@ export async function buildInstructions(repoRoot, stage, taskId) {
|
|
|
70
70
|
existingFiles: await listExistingFiles(taskDir),
|
|
71
71
|
writePolicy,
|
|
72
72
|
};
|
|
73
|
-
if (stage ===
|
|
73
|
+
if (stage === "source") {
|
|
74
74
|
return refreshInstructionBlockers(withMissingDependencies({
|
|
75
75
|
...base,
|
|
76
76
|
outputPath: paths.sourceDir,
|
|
77
77
|
dependencies: [paths.taskConfigPath],
|
|
78
|
-
template: {
|
|
78
|
+
template: {
|
|
79
|
+
requiredHeadings: [
|
|
80
|
+
"# 需求",
|
|
81
|
+
"# 执行约束",
|
|
82
|
+
"## 目标",
|
|
83
|
+
"## 验收标准",
|
|
84
|
+
"## 非目标",
|
|
85
|
+
],
|
|
86
|
+
},
|
|
79
87
|
completionCriteria: [
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
"source/references/ holds the immutable original PRD (via import-prd or materialize); do not let AI rewrite it",
|
|
89
|
+
"source/需求.md is a derived execution contract with objective, scope, non-goals, and acceptance criteria mapped back to the original PRD",
|
|
90
|
+
"source/需求.md uses stable REQ/AC ids or explicit source anchors when the original PRD is long",
|
|
91
|
+
"source/执行约束.md exists when path or behavioral constraints matter",
|
|
92
|
+
"AI assumptions that are not in the original PRD stay under open questions, never as fake requirements",
|
|
82
93
|
],
|
|
83
94
|
}));
|
|
84
95
|
}
|
|
85
|
-
if (stage ===
|
|
96
|
+
if (stage === "dag-draft") {
|
|
86
97
|
return refreshInstructionBlockers(withMissingDependencies({
|
|
87
98
|
...base,
|
|
88
99
|
outputPath: defaultHybridDagOutputPath(taskId),
|
|
89
100
|
dependencies: [
|
|
90
101
|
paths.taskConfigPath,
|
|
91
|
-
path.join(paths.sourceDir,
|
|
102
|
+
path.join(paths.sourceDir, "需求.md"),
|
|
92
103
|
],
|
|
93
|
-
template: {
|
|
104
|
+
template: {
|
|
105
|
+
requiredHeadings: [
|
|
106
|
+
"version",
|
|
107
|
+
"title",
|
|
108
|
+
"objective",
|
|
109
|
+
"tasks",
|
|
110
|
+
"executorModels",
|
|
111
|
+
],
|
|
112
|
+
},
|
|
94
113
|
completionCriteria: [
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
"dag draft validates with dag validate --strict-models --strict-governance",
|
|
115
|
+
"writeSet is narrow and matches task constraints",
|
|
97
116
|
],
|
|
98
117
|
}));
|
|
99
118
|
}
|
|
100
|
-
if (stage ===
|
|
119
|
+
if (stage === "promotion") {
|
|
101
120
|
return refreshInstructionBlockers(withMissingDependencies({
|
|
102
121
|
...base,
|
|
103
|
-
outputPath: path.join(taskDir,
|
|
122
|
+
outputPath: path.join(taskDir, "artifacts"),
|
|
104
123
|
dependencies: [
|
|
105
|
-
path.join(repoRoot,
|
|
106
|
-
path.join(repoRoot,
|
|
124
|
+
path.join(repoRoot, ".harness", "dag-runs", "completed"),
|
|
125
|
+
path.join(repoRoot, ".harness", "runs", "completed"),
|
|
107
126
|
],
|
|
108
|
-
template: { requiredHeadings: [
|
|
127
|
+
template: { requiredHeadings: ["# 修改记录", "# 验证结果"] },
|
|
109
128
|
completionCriteria: [
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
"completed run facts remain read-only",
|
|
130
|
+
"task artifacts summarize real run evidence without inventing verification",
|
|
112
131
|
],
|
|
113
132
|
}));
|
|
114
133
|
}
|
|
115
|
-
if (stage ===
|
|
134
|
+
if (stage === "closeout") {
|
|
116
135
|
return refreshInstructionBlockers(withMissingDependencies({
|
|
117
136
|
...base,
|
|
118
|
-
outputPath: path.join(repoRoot,
|
|
137
|
+
outputPath: path.join(repoRoot, "docs", "progress", `${taskId}.md`),
|
|
119
138
|
dependencies: [
|
|
120
|
-
getArtifactPath(taskDir,
|
|
121
|
-
getArtifactPath(taskDir,
|
|
139
|
+
getArtifactPath(taskDir, "implement"),
|
|
140
|
+
getArtifactPath(taskDir, "verify"),
|
|
122
141
|
],
|
|
123
|
-
template: {
|
|
142
|
+
template: {
|
|
143
|
+
requiredHeadings: [
|
|
144
|
+
"# Progress",
|
|
145
|
+
"## Summary",
|
|
146
|
+
"## Verification",
|
|
147
|
+
"## Remaining Risk",
|
|
148
|
+
],
|
|
149
|
+
},
|
|
124
150
|
completionCriteria: [
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
"records commands actually run and their outcome",
|
|
152
|
+
"links remaining risks or follow-up tasks",
|
|
127
153
|
],
|
|
128
154
|
}));
|
|
129
155
|
}
|
|
130
|
-
const artifactSteps = [
|
|
156
|
+
const artifactSteps = [
|
|
157
|
+
"analyze",
|
|
158
|
+
"plan",
|
|
159
|
+
"implement",
|
|
160
|
+
"verify",
|
|
161
|
+
"retrospective",
|
|
162
|
+
];
|
|
131
163
|
return refreshInstructionBlockers(withMissingDependencies({
|
|
132
164
|
...base,
|
|
133
|
-
outputPath: path.join(taskDir,
|
|
165
|
+
outputPath: path.join(taskDir, "artifacts"),
|
|
134
166
|
dependencies: [paths.taskConfigPath, paths.statePath],
|
|
135
167
|
template: {
|
|
136
168
|
requiredHeadings: artifactSteps.map((step) => getArtifactRelativePath(step)),
|
|
137
169
|
},
|
|
138
170
|
completionCriteria: [
|
|
139
|
-
|
|
140
|
-
|
|
171
|
+
"artifact content is substantive and not the new-task template",
|
|
172
|
+
"verification artifact names exact commands and pass/fail results",
|
|
141
173
|
],
|
|
142
174
|
}));
|
|
143
175
|
}
|
|
@@ -153,7 +185,7 @@ async function listExistingFiles(root) {
|
|
|
153
185
|
const names = await readdir(root, { recursive: true });
|
|
154
186
|
return names
|
|
155
187
|
.map((name) => String(name))
|
|
156
|
-
.filter((name) => !name.includes(
|
|
188
|
+
.filter((name) => !name.includes("/logs/"))
|
|
157
189
|
.sort();
|
|
158
190
|
}
|
|
159
191
|
catch {
|
|
@@ -179,17 +211,17 @@ export async function refreshInstructionBlockers(doc) {
|
|
|
179
211
|
function formatInstructionsMarkdown(doc) {
|
|
180
212
|
const lines = [
|
|
181
213
|
`# Instructions: ${doc.artifactId}`,
|
|
182
|
-
|
|
214
|
+
"",
|
|
183
215
|
`- task: \`${doc.taskId}\``,
|
|
184
216
|
`- output: \`${doc.outputPath}\``,
|
|
185
217
|
`- write policy: \`${doc.writePolicy.mode}\``,
|
|
186
|
-
`- blocked: ${doc.blocked ?
|
|
187
|
-
|
|
188
|
-
|
|
218
|
+
`- blocked: ${doc.blocked ? "yes" : "no"}`,
|
|
219
|
+
"",
|
|
220
|
+
"## Dependencies",
|
|
189
221
|
...doc.dependencies.map((dep) => `- \`${dep}\``),
|
|
190
|
-
|
|
191
|
-
|
|
222
|
+
"",
|
|
223
|
+
"## Completion Criteria",
|
|
192
224
|
...doc.completionCriteria.map((criterion) => `- ${criterion}`),
|
|
193
225
|
];
|
|
194
|
-
return `${lines.join(
|
|
226
|
+
return `${lines.join("\n")}\n`;
|
|
195
227
|
}
|