@tea-agent/loop-agent 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +9 -0
- package/dist/commands/init.js +28 -0
- package/dist/task/config-types.js +7 -0
- package/dist/task/runtime.js +1 -0
- package/dist/worker/cli.js +11 -0
- package/dist/worker/materialize/harness-task-materializer.js +7 -0
- package/dist/worker/observability/read-model.js +38 -2
- package/dist/worker/observe/static/app.js +63 -2
- package/dist/worker/observe/static/dag-layout.d.ts +31 -0
- package/dist/worker/observe/static/dag-layout.js +83 -0
- package/dist/worker/observe/static/styles.css +111 -2
- package/dist/worker/task-graph/validate.js +68 -3
- package/dist/workflows/dag/init-hybrid.js +18 -2
- package/docs/README.md +4 -0
- package/docs/exec-plans/active/README.md +1 -2
- package/docs/exec-plans/completed/README.md +2 -0
- package/docs/init-surface.manifest.json +33 -0
- package/docs/templates/product-line/AGENTS.md +8 -0
- package/docs/templates/product-line/README.md +9 -0
- package/docs/templates/product-line/acceptance.yaml +14 -0
- package/docs/templates/product-line/closeout.yaml +9 -0
- package/docs/templates/product-line/design.md +13 -0
- package/docs/templates/product-line/links.md +10 -0
- package/docs/templates/product-line/requirement.md +17 -0
- package/docs/templates/product-line/task-graph.yaml +15 -0
- package/docs/templates/product-line/task.yaml +65 -0
- package/docs/templates/product-line/test-plan.md +7 -0
- package/docs/verification-matrix.md +8 -0
- package/harness.json +1 -0
- package/package.json +2 -1
- package/scripts/check-product-line-docs.sh +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.5] - 2026-07-11
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
10
|
+
|
|
11
|
+
- Observe DAG Detail 新增离线 SVG 依赖图:read-model 从 canonical `run.json` 投影稳定 edges,支持并行/汇合/失败/跳过状态、键盘节点选择、上下游高亮、滚动与重置视图,并保留节点表格和过程时间线降级路径。
|
|
12
|
+
- 新增 repository-owned Worker nightly wrapper 与 GitHub Actions 手动/定时适配面:按 feature 互斥、保留超时/批次退出码,并收集 controller、morning report、Observe snapshot 等 artifacts。
|
|
13
|
+
- 新增 `agent-worker task validate-feature` 和可初始化投影的语言无关产品线模板,校验 AC 唯一性、任务依赖、TaskSpec 路径/验证命令及 QA verdict → success closeout 顺序,并接入仓库治理门禁。
|
|
14
|
+
|
|
15
|
+
## [0.7.4] - 2026-07-11
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- Worker materialize 现在把 TaskSpec 中所有 required `verify.commands` 写入 task config,并强制合并到 DAG 最终 shell verification;review 不再只能看到仓库级治理命令而缺少任务契约要求的 typecheck/focused tests。
|
|
20
|
+
|
|
7
21
|
## [0.7.3] - 2026-07-11
|
|
8
22
|
|
|
9
23
|
### 修复
|
package/README.md
CHANGED
|
@@ -88,6 +88,15 @@ loop-agent pi-prompt --cwd . --tools read,grep,find,ls "只读评审这个任务
|
|
|
88
88
|
loop-agent pi-prompt --cwd . --tools read,bash,edit,write,grep,find,ls "<包含 allowedPaths 和 forbiddenPaths 的有边界任务说明>"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
产品线 feature packet 可用伴生 Worker CLI 做 docs CI;仓库也提供外部 CI/cron wrapper,调度仍由外部系统负责:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
agent-worker task validate-feature <feature-dir>
|
|
95
|
+
bash scripts/worker-nightly.sh <feature-dir> <target-repo> <batch-run-id>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
nightly wrapper 按 feature 互斥,保留批次/超时退出码,并输出 morning report、Observe snapshot 和 controller 版本 artifacts。
|
|
99
|
+
|
|
91
100
|
## 核心概念
|
|
92
101
|
|
|
93
102
|
- **Agent DAG**:把一次任务拆成 contract、scout、plan、implement、verify、closeout 等可审查节点。
|
package/dist/commands/init.js
CHANGED
|
@@ -418,6 +418,7 @@ checks=(
|
|
|
418
418
|
"scripts/check-harness-runtime-clean.sh"
|
|
419
419
|
"scripts/check-architecture-boundaries.sh"
|
|
420
420
|
"scripts/check-skill-entry.sh"
|
|
421
|
+
"scripts/check-product-line-docs.sh"
|
|
421
422
|
)
|
|
422
423
|
|
|
423
424
|
for check in "\${checks[@]}"; do
|
|
@@ -428,6 +429,32 @@ done
|
|
|
428
429
|
|
|
429
430
|
echo "仓库治理检查全部通过"
|
|
430
431
|
`;
|
|
432
|
+
const INIT_CHECK_PRODUCT_LINE_DOCS_SH = `#!/usr/bin/env bash
|
|
433
|
+
set -euo pipefail
|
|
434
|
+
|
|
435
|
+
ROOT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
436
|
+
cd "\${ROOT_DIR}"
|
|
437
|
+
|
|
438
|
+
if ! command -v agent-worker >/dev/null 2>&1; then
|
|
439
|
+
echo "product-line docs check skipped: agent-worker is not installed"
|
|
440
|
+
exit 0
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
feature_dirs=()
|
|
444
|
+
for root in features product/features dogfood/features; do
|
|
445
|
+
[[ -d "\${root}" ]] || continue
|
|
446
|
+
while IFS= read -r acceptance; do
|
|
447
|
+
feature_dirs+=("$(dirname "\${acceptance}")")
|
|
448
|
+
done < <(find "\${root}" -mindepth 2 -maxdepth 2 -name acceptance.yaml -type f | sort)
|
|
449
|
+
done
|
|
450
|
+
|
|
451
|
+
for feature_dir in "\${feature_dirs[@]}"; do
|
|
452
|
+
echo "==> validate product-line feature: \${feature_dir}"
|
|
453
|
+
agent-worker task validate-feature "\${feature_dir}"
|
|
454
|
+
done
|
|
455
|
+
|
|
456
|
+
echo "product-line docs checks passed: \${#feature_dirs[@]} feature packet(s)"
|
|
457
|
+
`;
|
|
431
458
|
const INIT_CI_GOVERNANCE_SH = `#!/usr/bin/env bash
|
|
432
459
|
set -euo pipefail
|
|
433
460
|
|
|
@@ -570,6 +597,7 @@ const INIT_SCRIPT_FILES = {
|
|
|
570
597
|
"scripts/check-harness-runtime-clean.sh": INIT_CHECK_HARNESS_RUNTIME_CLEAN_SH,
|
|
571
598
|
"scripts/check-architecture-boundaries.sh": INIT_CHECK_ARCHITECTURE_BOUNDARIES_SH,
|
|
572
599
|
"scripts/check-skill-entry.sh": INIT_CHECK_SKILL_ENTRY_SH,
|
|
600
|
+
"scripts/check-product-line-docs.sh": INIT_CHECK_PRODUCT_LINE_DOCS_SH,
|
|
573
601
|
"scripts/check-repo.sh": INIT_CHECK_REPO_SH,
|
|
574
602
|
"scripts/ci-governance.sh": INIT_CI_GOVERNANCE_SH,
|
|
575
603
|
"scripts/ci-tests.sh": INIT_CI_TESTS_SH,
|
|
@@ -25,6 +25,11 @@ export const piSubagentModeSchema = z.enum(["off", "analyze-plan", "full"]);
|
|
|
25
25
|
export const verifyModeSchema = z.enum(["parallel", "serial"]);
|
|
26
26
|
export const verifyPresetSchema = z.enum(["auto", "quick", "standard", "full"]);
|
|
27
27
|
export const verifyQuotaSchema = z.enum(["1", "3", "full"]);
|
|
28
|
+
export const taskVerifyCommandSchema = z.object({
|
|
29
|
+
label: z.string().min(1),
|
|
30
|
+
command: z.string().min(1),
|
|
31
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
32
|
+
});
|
|
28
33
|
export const dagVerifyStrategySchema = z
|
|
29
34
|
.object({
|
|
30
35
|
intermediateQuota: verifyQuotaSchema.optional(),
|
|
@@ -73,6 +78,8 @@ export const taskConfigSchema = z.object({
|
|
|
73
78
|
capabilities: z.array(taskCapabilitySchema).optional(),
|
|
74
79
|
verifyMode: verifyModeSchema.optional().default("parallel"),
|
|
75
80
|
verifyPreset: verifyPresetSchema.optional().default("auto"),
|
|
81
|
+
/** Task-contract commands that must be included in final DAG shell verification. */
|
|
82
|
+
verifyCommands: z.array(taskVerifyCommandSchema).optional().default([]),
|
|
76
83
|
/** Verification selection policy. Intermediate loops may use quota; final gates still run full required verification. */
|
|
77
84
|
verifyQuota: verifyQuotaSchema.optional().default("full"),
|
|
78
85
|
/** DAG supervised verify strategy. Final quota is fixed to full; intermediate quota may reduce cost. */
|
package/dist/task/runtime.js
CHANGED
package/dist/worker/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createObserveServer } from "./observe/server.js";
|
|
|
16
16
|
import { prepareTaskPoolRetry } from "./pool/run-store.js";
|
|
17
17
|
import { taskSpecSchema } from "./task-spec/schema.js";
|
|
18
18
|
import { validateTaskSpec } from "./task-spec/validate.js";
|
|
19
|
+
import { validateFeatureTaskGraph } from "./task-graph/validate.js";
|
|
19
20
|
export function buildAgentWorkerProgram() {
|
|
20
21
|
const program = new Command();
|
|
21
22
|
program
|
|
@@ -49,6 +50,16 @@ export function buildAgentWorkerProgram() {
|
|
|
49
50
|
const result = resolveLoopAgentProfile(parsed);
|
|
50
51
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
51
52
|
});
|
|
53
|
+
task
|
|
54
|
+
.command("validate-feature")
|
|
55
|
+
.argument("<feature-dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
56
|
+
.description("Validate acceptance ids, task graph, TaskSpecs, paths, and verification commands")
|
|
57
|
+
.action(async (featureDir) => {
|
|
58
|
+
const result = await validateFeatureTaskGraph(path.resolve(featureDir));
|
|
59
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
60
|
+
if (!result.ok)
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
});
|
|
52
63
|
task
|
|
53
64
|
.command("retry")
|
|
54
65
|
.argument("<task-id>", "Failed Task Pool task to requeue")
|
|
@@ -44,6 +44,13 @@ export async function materializeTaskSpec(options) {
|
|
|
44
44
|
verifyMode: options.taskSpec.verify.mode,
|
|
45
45
|
verifyPreset: options.taskSpec.verify.preset,
|
|
46
46
|
verifyQuota: options.taskSpec.verify.quota,
|
|
47
|
+
verifyCommands: options.taskSpec.verify.commands
|
|
48
|
+
.filter((command) => command.required)
|
|
49
|
+
.map((command) => ({
|
|
50
|
+
label: command.label,
|
|
51
|
+
command: command.command,
|
|
52
|
+
timeoutMs: command.timeout_ms,
|
|
53
|
+
})),
|
|
47
54
|
autoCommitAfterVerify: false,
|
|
48
55
|
};
|
|
49
56
|
await writeTaskConfig(options.repoRoot, harnessTaskId, taskConfig);
|
|
@@ -645,7 +645,7 @@ async function loadDagRuns(repoRoot) {
|
|
|
645
645
|
const byId = new Map();
|
|
646
646
|
const statePaths = await findStateJsonFiles(path.join(repoRoot, ".harness", "dag-runs"));
|
|
647
647
|
for (const statePath of statePaths) {
|
|
648
|
-
const summary = parseDagStateFile(statePath);
|
|
648
|
+
const summary = await parseDagStateFile(statePath);
|
|
649
649
|
if (summary)
|
|
650
650
|
byId.set(summary.dagRunId, mergeDagRun(byId.get(summary.dagRunId), summary));
|
|
651
651
|
}
|
|
@@ -674,6 +674,7 @@ function mergeDagRun(existing, incoming) {
|
|
|
674
674
|
...(durationMs !== undefined ? { durationMs } : {}),
|
|
675
675
|
...(ranks && ranks.length > 0 ? { ranks } : {}),
|
|
676
676
|
nodes: mergeDagNodes(existing.nodes, incoming.nodes),
|
|
677
|
+
edges: incoming.edges.length > 0 ? incoming.edges : existing.edges,
|
|
677
678
|
...(incoming.dagPath ?? existing.dagPath ? { dagPath: incoming.dagPath ?? existing.dagPath } : {}),
|
|
678
679
|
};
|
|
679
680
|
}
|
|
@@ -819,6 +820,7 @@ async function loadDagEventFiles(repoRoot) {
|
|
|
819
820
|
...(finishedAt ? { finishedAt } : {}),
|
|
820
821
|
...(durationMs !== undefined ? { durationMs } : {}),
|
|
821
822
|
nodes: [...nodeMap.values()].sort((a, b) => a.nodeId.localeCompare(b.nodeId)),
|
|
823
|
+
edges: [],
|
|
822
824
|
});
|
|
823
825
|
}
|
|
824
826
|
return result;
|
|
@@ -845,7 +847,7 @@ async function walkForStateJson(dir, results) {
|
|
|
845
847
|
// skip
|
|
846
848
|
}
|
|
847
849
|
}
|
|
848
|
-
function parseDagStateFile(statePath) {
|
|
850
|
+
async function parseDagStateFile(statePath) {
|
|
849
851
|
try {
|
|
850
852
|
if (!existsSync(statePath))
|
|
851
853
|
return undefined;
|
|
@@ -863,6 +865,7 @@ function parseDagStateFile(statePath) {
|
|
|
863
865
|
const ranks = readStringMatrix(parsed, "ranks");
|
|
864
866
|
const rankByNode = buildRankIndex(ranks);
|
|
865
867
|
const nodes = parseDagNodes(parsed, rankByNode);
|
|
868
|
+
const edges = await parseDagEdges(path.join(runDir, "run.json"), nodes);
|
|
866
869
|
return {
|
|
867
870
|
dagRunId,
|
|
868
871
|
status,
|
|
@@ -872,6 +875,7 @@ function parseDagStateFile(statePath) {
|
|
|
872
875
|
...(durationMs !== undefined ? { durationMs } : {}),
|
|
873
876
|
...(ranks.length > 0 ? { ranks } : {}),
|
|
874
877
|
nodes,
|
|
878
|
+
edges,
|
|
875
879
|
dagPath: runDir,
|
|
876
880
|
};
|
|
877
881
|
}
|
|
@@ -879,6 +883,30 @@ function parseDagStateFile(statePath) {
|
|
|
879
883
|
return undefined;
|
|
880
884
|
}
|
|
881
885
|
}
|
|
886
|
+
async function parseDagEdges(runPath, nodes) {
|
|
887
|
+
const parsed = await safeReadJson(runPath);
|
|
888
|
+
if (!parsed)
|
|
889
|
+
return [];
|
|
890
|
+
const tasks = readObjectArray(parsed, "tasks");
|
|
891
|
+
if (tasks.length === 0)
|
|
892
|
+
return [];
|
|
893
|
+
const knownNodeIds = new Set(nodes.map((node) => node.nodeId));
|
|
894
|
+
const edges = new Map();
|
|
895
|
+
for (const task of tasks) {
|
|
896
|
+
const taskId = readString(task, "id") ?? readString(task, "nodeId");
|
|
897
|
+
if (!taskId || !knownNodeIds.has(taskId))
|
|
898
|
+
continue;
|
|
899
|
+
const dependencies = readStringArray(task, "depends_on");
|
|
900
|
+
const compatibleDependencies = dependencies.length > 0 ? dependencies : readStringArray(task, "dependsOn");
|
|
901
|
+
for (const dependencyId of compatibleDependencies) {
|
|
902
|
+
if (dependencyId === taskId || !knownNodeIds.has(dependencyId))
|
|
903
|
+
continue;
|
|
904
|
+
const edge = { from: dependencyId, to: taskId };
|
|
905
|
+
edges.set(`${edge.from}\u0000${edge.to}`, edge);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
909
|
+
}
|
|
882
910
|
function buildRankIndex(ranks) {
|
|
883
911
|
const index = new Map();
|
|
884
912
|
for (let i = 0; i < ranks.length; i++) {
|
|
@@ -995,6 +1023,14 @@ function readString(value, key) {
|
|
|
995
1023
|
const child = value[key];
|
|
996
1024
|
return typeof child === "string" ? child : undefined;
|
|
997
1025
|
}
|
|
1026
|
+
function readStringArray(value, key) {
|
|
1027
|
+
if (!value || typeof value !== "object")
|
|
1028
|
+
return [];
|
|
1029
|
+
const child = value[key];
|
|
1030
|
+
if (!Array.isArray(child))
|
|
1031
|
+
return [];
|
|
1032
|
+
return child.filter((item) => typeof item === "string");
|
|
1033
|
+
}
|
|
998
1034
|
function readNumber(value, key) {
|
|
999
1035
|
if (!value || typeof value !== "object")
|
|
1000
1036
|
return undefined;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { layoutDag } from "./dag-layout.js";
|
|
2
|
+
|
|
1
3
|
const POLL_MS = 2000;
|
|
2
4
|
const DASHBOARD_POLL_MS = 5000;
|
|
3
5
|
|
|
@@ -194,6 +196,16 @@ function statusLabel(status) {
|
|
|
194
196
|
return status;
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
function statusSymbol(status) {
|
|
200
|
+
const key = (status ?? "unknown").toLowerCase();
|
|
201
|
+
if (["finished", "completed", "succeeded", "done"].includes(key)) return "✓";
|
|
202
|
+
if (["failed", "error"].includes(key)) return "✕";
|
|
203
|
+
if (key === "skipped") return "↷";
|
|
204
|
+
if (["running", "started"].includes(key)) return "●";
|
|
205
|
+
if (["pending", "queued", "blocked"].includes(key)) return "○";
|
|
206
|
+
return "◇";
|
|
207
|
+
}
|
|
208
|
+
|
|
197
209
|
function badgeClass(status) {
|
|
198
210
|
if (!status) return "unknown";
|
|
199
211
|
const key = status.toLowerCase().replace(/_/g, "-");
|
|
@@ -704,6 +716,55 @@ function renderRankLanes(dag) {
|
|
|
704
716
|
return container;
|
|
705
717
|
}
|
|
706
718
|
|
|
719
|
+
function svgEl(tag, attrs = {}) {
|
|
720
|
+
const node = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
721
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value));
|
|
722
|
+
return node;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function renderDagGraph(dag, dagRunId) {
|
|
726
|
+
const nodes = dag.nodes ?? [];
|
|
727
|
+
const edges = dag.edges ?? [];
|
|
728
|
+
const layout = layoutDag(nodes, edges, dag.ranks ?? []);
|
|
729
|
+
const wrap = el("div", "dag-graph-wrap");
|
|
730
|
+
const toolbar = el("div", "dag-graph-toolbar");
|
|
731
|
+
toolbar.appendChild(el("span", "dag-graph-summary", `${nodes.length} 节点 · ${layout.edges.length} 条依赖`));
|
|
732
|
+
const reset = el("button", "dag-graph-reset", "重置视图");
|
|
733
|
+
toolbar.appendChild(reset);
|
|
734
|
+
wrap.appendChild(toolbar);
|
|
735
|
+
if (edges.length === 0) wrap.appendChild(el("p", "dag-graph-fallback", "该运行没有可用依赖数据;图按 Rank 展示,节点表格仍保留完整数据。"));
|
|
736
|
+
const viewport = el("div", "dag-graph-viewport");
|
|
737
|
+
const svg = svgEl("svg", { class: "dag-graph", viewBox: `0 0 ${layout.width} ${layout.height}`, width: layout.width, height: layout.height, role: "img", "aria-label": `DAG 依赖图,共 ${nodes.length} 个节点` });
|
|
738
|
+
const defs = svgEl("defs");
|
|
739
|
+
const marker = svgEl("marker", { id: "dag-arrow", viewBox: "0 0 10 10", refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: "auto-start-reverse" });
|
|
740
|
+
marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
741
|
+
defs.appendChild(marker); svg.appendChild(defs);
|
|
742
|
+
const related = new Set([selectedDagNodeId]);
|
|
743
|
+
for (const edge of layout.edges) if (edge.from === selectedDagNodeId || edge.to === selectedDagNodeId) { related.add(edge.from); related.add(edge.to); }
|
|
744
|
+
for (const edge of layout.edges) {
|
|
745
|
+
const path = svgEl("path", { d: edge.path, class: `dag-edge${selectedDagNodeId && (!related.has(edge.from) || !related.has(edge.to)) ? " dag-dimmed" : ""}`, "marker-end": "url(#dag-arrow)" });
|
|
746
|
+
path.appendChild(svgEl("title")); path.firstChild.textContent = `${edge.from} → ${edge.to}`; svg.appendChild(path);
|
|
747
|
+
}
|
|
748
|
+
const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
|
|
749
|
+
for (const position of layout.nodes) {
|
|
750
|
+
const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
|
|
751
|
+
const status = (node.status ?? "unknown").toLowerCase();
|
|
752
|
+
const group = svgEl("g", { transform: `translate(${position.x} ${position.y})`, class: `dag-graph-node status-${status}${node.nodeId === selectedDagNodeId ? " selected" : ""}${selectedDagNodeId && !related.has(node.nodeId) ? " dag-dimmed" : ""}`, tabindex: 0, role: "button", "aria-label": `${node.nodeId},状态 ${status}` });
|
|
753
|
+
group.appendChild(svgEl("rect", { width: position.width, height: position.height, rx: 10 }));
|
|
754
|
+
const title = svgEl("title"); title.textContent = `${node.nodeId}\n${node.executor ?? "未知执行器"}\n${status}`; group.appendChild(title);
|
|
755
|
+
const label = svgEl("text", { x: 14, y: 26, class: "dag-node-label" }); label.textContent = truncateText(node.label ?? node.nodeId, 26); group.appendChild(label);
|
|
756
|
+
const executor = svgEl("text", { x: 14, y: 50, class: "dag-node-meta" }); executor.textContent = `◈ ${node.executor ?? "unknown"}`; group.appendChild(executor);
|
|
757
|
+
const state = svgEl("text", { x: 14, y: 74, class: "dag-node-status" }); state.textContent = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`; group.appendChild(state);
|
|
758
|
+
const activate = () => selectDagNode(dagRunId, node.nodeId);
|
|
759
|
+
group.addEventListener("click", activate);
|
|
760
|
+
group.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(); } });
|
|
761
|
+
svg.appendChild(group);
|
|
762
|
+
}
|
|
763
|
+
viewport.appendChild(svg); wrap.appendChild(viewport);
|
|
764
|
+
reset.addEventListener("click", () => { viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" }); });
|
|
765
|
+
return wrap;
|
|
766
|
+
}
|
|
767
|
+
|
|
707
768
|
function expandablePreview(title, content, previewClass) {
|
|
708
769
|
const wrap = el("details", previewClass ?? "output-preview");
|
|
709
770
|
const summary = el("summary", null, title);
|
|
@@ -1307,11 +1368,11 @@ async function renderDagDetail(dagRunId, initial = true) {
|
|
|
1307
1368
|
);
|
|
1308
1369
|
|
|
1309
1370
|
clearNode(ranksEl);
|
|
1310
|
-
|
|
1371
|
+
ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
|
|
1311
1372
|
if (nodes.length === 0) {
|
|
1312
1373
|
ranksEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
|
|
1313
1374
|
} else {
|
|
1314
|
-
|
|
1375
|
+
ranksEl.appendChild(renderDagGraph(dag, dagRunId));
|
|
1315
1376
|
}
|
|
1316
1377
|
|
|
1317
1378
|
clearNode(nodesEl);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type DagLayoutNodeInput = { nodeId: string };
|
|
2
|
+
export type DagLayoutEdgeInput = { from: string; to: string };
|
|
3
|
+
export type DagLayoutNode = {
|
|
4
|
+
nodeId: string;
|
|
5
|
+
rank: number;
|
|
6
|
+
index: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
export type DagLayoutEdge = DagLayoutEdgeInput & { path: string };
|
|
13
|
+
export type DagLayout = {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
nodes: DagLayoutNode[];
|
|
17
|
+
edges: DagLayoutEdge[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function layoutDag(
|
|
21
|
+
nodes?: DagLayoutNodeInput[],
|
|
22
|
+
edges?: DagLayoutEdgeInput[],
|
|
23
|
+
ranks?: string[][],
|
|
24
|
+
options?: Partial<{
|
|
25
|
+
nodeWidth: number;
|
|
26
|
+
nodeHeight: number;
|
|
27
|
+
rankGap: number;
|
|
28
|
+
nodeGap: number;
|
|
29
|
+
padding: number;
|
|
30
|
+
}>,
|
|
31
|
+
): DagLayout;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
nodeWidth: 216,
|
|
3
|
+
nodeHeight: 92,
|
|
4
|
+
rankGap: 104,
|
|
5
|
+
nodeGap: 28,
|
|
6
|
+
padding: 32,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function layoutDag(nodes = [], edges = [], ranks = [], options = {}) {
|
|
10
|
+
const config = { ...DEFAULTS, ...options };
|
|
11
|
+
const nodeIds = [...new Set(nodes.map((node) => node.nodeId).filter(Boolean))].sort();
|
|
12
|
+
if (nodeIds.length === 0) return { width: 320, height: 160, nodes: [], edges: [] };
|
|
13
|
+
const known = new Set(nodeIds);
|
|
14
|
+
const validEdges = edges
|
|
15
|
+
.filter((edge) => known.has(edge.from) && known.has(edge.to) && edge.from !== edge.to)
|
|
16
|
+
.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
17
|
+
const layers = normalizeLayers(nodeIds, validEdges, ranks);
|
|
18
|
+
const positioned = [];
|
|
19
|
+
for (let rank = 0; rank < layers.length; rank++) {
|
|
20
|
+
const ids = layers[rank];
|
|
21
|
+
for (let index = 0; index < ids.length; index++) {
|
|
22
|
+
positioned.push({
|
|
23
|
+
nodeId: ids[index], rank, index,
|
|
24
|
+
x: config.padding + rank * (config.nodeWidth + config.rankGap),
|
|
25
|
+
y: config.padding + index * (config.nodeHeight + config.nodeGap),
|
|
26
|
+
width: config.nodeWidth, height: config.nodeHeight,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const byId = new Map(positioned.map((node) => [node.nodeId, node]));
|
|
31
|
+
const routed = validEdges.map((edge) => {
|
|
32
|
+
const from = byId.get(edge.from);
|
|
33
|
+
const to = byId.get(edge.to);
|
|
34
|
+
const x1 = from.x + from.width;
|
|
35
|
+
const y1 = from.y + from.height / 2;
|
|
36
|
+
const x2 = to.x;
|
|
37
|
+
const y2 = to.y + to.height / 2;
|
|
38
|
+
const bend = Math.max(36, (x2 - x1) / 2);
|
|
39
|
+
return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
|
|
40
|
+
});
|
|
41
|
+
const maxRows = Math.max(...layers.map((layer) => layer.length), 1);
|
|
42
|
+
return {
|
|
43
|
+
width: config.padding * 2 + layers.length * config.nodeWidth + (layers.length - 1) * config.rankGap,
|
|
44
|
+
height: config.padding * 2 + maxRows * config.nodeHeight + (maxRows - 1) * config.nodeGap,
|
|
45
|
+
nodes: positioned,
|
|
46
|
+
edges: routed,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeLayers(nodeIds, edges, ranks) {
|
|
51
|
+
const known = new Set(nodeIds);
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const layers = [];
|
|
54
|
+
if (Array.isArray(ranks)) {
|
|
55
|
+
for (const rank of ranks) {
|
|
56
|
+
if (!Array.isArray(rank)) continue;
|
|
57
|
+
const ids = [...new Set(rank.filter((id) => known.has(id) && !seen.has(id)))].sort();
|
|
58
|
+
ids.forEach((id) => seen.add(id));
|
|
59
|
+
if (ids.length > 0) layers.push(ids);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (layers.length === 0 && edges.length > 0) {
|
|
63
|
+
const level = new Map(nodeIds.map((id) => [id, 0]));
|
|
64
|
+
for (let pass = 0; pass < nodeIds.length; pass++) {
|
|
65
|
+
let changed = false;
|
|
66
|
+
for (const edge of edges) {
|
|
67
|
+
const next = Math.min(nodeIds.length - 1, level.get(edge.from) + 1);
|
|
68
|
+
if (next > level.get(edge.to)) { level.set(edge.to, next); changed = true; }
|
|
69
|
+
}
|
|
70
|
+
if (!changed) break;
|
|
71
|
+
}
|
|
72
|
+
for (const id of nodeIds) {
|
|
73
|
+
const rank = level.get(id);
|
|
74
|
+
(layers[rank] ??= []).push(id);
|
|
75
|
+
seen.add(id);
|
|
76
|
+
}
|
|
77
|
+
for (const layer of layers) layer?.sort();
|
|
78
|
+
}
|
|
79
|
+
for (const id of nodeIds) {
|
|
80
|
+
if (!seen.has(id)) (layers[0] ??= []).push(id);
|
|
81
|
+
}
|
|
82
|
+
return layers.filter(Boolean);
|
|
83
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--bg: #0d1117;
|
|
3
3
|
--surface: #161b22;
|
|
4
|
+
--surface-raised: #1c2128;
|
|
4
5
|
--border: #30363d;
|
|
5
6
|
--text: #c9d1d9;
|
|
6
7
|
--muted: #8b949e;
|
|
@@ -10,6 +11,17 @@
|
|
|
10
11
|
--yellow: #d29922;
|
|
11
12
|
--blue: #58a6ff;
|
|
12
13
|
--gray-red: #8b3a3a;
|
|
14
|
+
--space-1: 4px;
|
|
15
|
+
--space-2: 8px;
|
|
16
|
+
--space-3: 12px;
|
|
17
|
+
--space-4: 16px;
|
|
18
|
+
--space-6: 24px;
|
|
19
|
+
--space-8: 32px;
|
|
20
|
+
--radius-sm: 4px;
|
|
21
|
+
--radius-md: 8px;
|
|
22
|
+
--radius-lg: 12px;
|
|
23
|
+
--shadow-1: 0 4px 18px rgba(0, 0, 0, 0.2);
|
|
24
|
+
--transition-fast: 140ms ease;
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
* {
|
|
@@ -18,13 +30,25 @@
|
|
|
18
30
|
|
|
19
31
|
body {
|
|
20
32
|
margin: 0;
|
|
21
|
-
font-family: ui
|
|
33
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
22
34
|
font-size: 13px;
|
|
23
35
|
line-height: 1.5;
|
|
24
36
|
background: var(--bg);
|
|
25
37
|
color: var(--text);
|
|
26
38
|
}
|
|
27
39
|
|
|
40
|
+
code,
|
|
41
|
+
pre,
|
|
42
|
+
.output-block,
|
|
43
|
+
.rank-node-id,
|
|
44
|
+
.dag-node-label,
|
|
45
|
+
.dag-node-meta,
|
|
46
|
+
.dag-node-status,
|
|
47
|
+
td:first-child,
|
|
48
|
+
.meta-item dd {
|
|
49
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
#app {
|
|
29
53
|
max-width: 1200px;
|
|
30
54
|
margin: 0 auto;
|
|
@@ -103,11 +127,96 @@ h3,
|
|
|
103
127
|
.panel {
|
|
104
128
|
background: var(--surface);
|
|
105
129
|
border: 1px solid var(--border);
|
|
106
|
-
border-radius:
|
|
130
|
+
border-radius: var(--radius-md);
|
|
107
131
|
padding: 0.75rem 1rem;
|
|
108
132
|
margin-bottom: 1rem;
|
|
109
133
|
}
|
|
110
134
|
|
|
135
|
+
.dag-graph-wrap {
|
|
136
|
+
display: grid;
|
|
137
|
+
gap: var(--space-2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.dag-graph-toolbar {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
gap: var(--space-3);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.dag-graph-summary,
|
|
148
|
+
.dag-graph-fallback {
|
|
149
|
+
color: var(--muted);
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.dag-graph-fallback { margin: 0; }
|
|
154
|
+
|
|
155
|
+
.dag-graph-reset {
|
|
156
|
+
appearance: none;
|
|
157
|
+
border: 1px solid var(--border);
|
|
158
|
+
border-radius: var(--radius-sm);
|
|
159
|
+
background: var(--surface-raised);
|
|
160
|
+
color: var(--text);
|
|
161
|
+
padding: 6px 10px;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.dag-graph-reset:hover,
|
|
166
|
+
.dag-graph-reset:focus-visible { border-color: var(--link); }
|
|
167
|
+
|
|
168
|
+
.dag-graph-viewport {
|
|
169
|
+
min-height: 220px;
|
|
170
|
+
max-height: 620px;
|
|
171
|
+
overflow: auto;
|
|
172
|
+
border: 1px solid var(--border);
|
|
173
|
+
border-radius: var(--radius-md);
|
|
174
|
+
background: radial-gradient(circle at 1px 1px, rgba(139, 148, 158, 0.18) 1px, transparent 0) 0 0 / 20px 20px, var(--bg);
|
|
175
|
+
scroll-behavior: smooth;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.dag-graph { display: block; }
|
|
179
|
+
.dag-edge { fill: none; stroke: var(--muted); stroke-width: 2; opacity: 0.75; transition: opacity var(--transition-fast); }
|
|
180
|
+
.dag-edge + .dag-edge { stroke-width: 1.8; }
|
|
181
|
+
#dag-arrow path { fill: var(--muted); }
|
|
182
|
+
.dag-graph-node { cursor: pointer; transition: opacity var(--transition-fast); }
|
|
183
|
+
.dag-graph-node rect { fill: var(--surface-raised); stroke: var(--border); stroke-width: 1.5; }
|
|
184
|
+
.dag-graph-node:hover rect,
|
|
185
|
+
.dag-graph-node:focus-visible rect { stroke: var(--link); stroke-width: 2.5; }
|
|
186
|
+
.dag-graph-node:focus { outline: none; }
|
|
187
|
+
.dag-graph-node.selected rect { stroke: var(--link); stroke-width: 3; filter: drop-shadow(0 0 6px rgba(88, 166, 255, 0.45)); }
|
|
188
|
+
.dag-graph-node.status-running rect,
|
|
189
|
+
.dag-graph-node.status-started rect { stroke: var(--yellow); }
|
|
190
|
+
.dag-graph-node.status-failed rect,
|
|
191
|
+
.dag-graph-node.status-error rect { stroke: var(--red); fill: rgba(248, 81, 73, 0.1); }
|
|
192
|
+
.dag-graph-node.status-skipped rect { stroke-dasharray: 5 4; }
|
|
193
|
+
.dag-node-label { fill: var(--text); font-size: 13px; font-weight: 700; }
|
|
194
|
+
.dag-node-meta { fill: var(--muted); font-size: 11px; }
|
|
195
|
+
.dag-node-status { fill: var(--text); font-size: 11px; }
|
|
196
|
+
.dag-dimmed { opacity: 0.25; }
|
|
197
|
+
.dag-graph-node.status-running { animation: graph-node-pulse 2s ease-in-out infinite; }
|
|
198
|
+
|
|
199
|
+
@keyframes graph-node-pulse {
|
|
200
|
+
50% { filter: drop-shadow(0 0 7px rgba(210, 153, 34, 0.45)); }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@media (max-width: 680px) {
|
|
204
|
+
#app { padding-inline: var(--space-3); }
|
|
205
|
+
.meta-grid { grid-template-columns: 1fr; }
|
|
206
|
+
.dag-graph-viewport { max-height: 480px; }
|
|
207
|
+
table { min-width: 760px; }
|
|
208
|
+
.panel { overflow-x: auto; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@media (prefers-reduced-motion: reduce) {
|
|
212
|
+
*, *::before, *::after {
|
|
213
|
+
animation-duration: 0.01ms !important;
|
|
214
|
+
animation-iteration-count: 1 !important;
|
|
215
|
+
scroll-behavior: auto !important;
|
|
216
|
+
transition-duration: 0.01ms !important;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
111
220
|
.kpi-grid {
|
|
112
221
|
display: grid;
|
|
113
222
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
@@ -2,6 +2,7 @@ import { access, readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
5
|
+
import { validateTaskSpec } from "../task-spec/validate.js";
|
|
5
6
|
import { acceptanceSpecSchema } from "./acceptance-schema.js";
|
|
6
7
|
import { computeReadyQueue } from "./ready-queue.js";
|
|
7
8
|
import { taskGraphSpecSchema } from "./task-graph-schema.js";
|
|
@@ -13,12 +14,57 @@ export async function validateFeatureTaskGraph(featureDir, options = {}) {
|
|
|
13
14
|
return result("", acceptance, graph, errors);
|
|
14
15
|
}
|
|
15
16
|
checkDuplicateNodeIds(graph, errors);
|
|
17
|
+
checkDuplicateAcceptanceIds(acceptance, errors);
|
|
16
18
|
checkAcceptanceTaskRefs(acceptance, graph, errors);
|
|
17
19
|
checkUnknownDependencies(graph, errors);
|
|
18
20
|
checkCycles(graph, errors);
|
|
19
|
-
await checkTaskFiles(featureDir, graph, errors);
|
|
21
|
+
await checkTaskFiles(featureDir, graph, acceptance, errors);
|
|
22
|
+
await checkQaCloseoutOrder(featureDir, options, errors);
|
|
20
23
|
return result(graph.feature_id, acceptance, graph, errors);
|
|
21
24
|
}
|
|
25
|
+
async function checkQaCloseoutOrder(featureDir, options, errors) {
|
|
26
|
+
const closeoutPath = path.join(featureDir, "closeout.yaml");
|
|
27
|
+
let raw = options.closeoutOverride;
|
|
28
|
+
if (raw === undefined) {
|
|
29
|
+
if (!(await exists(closeoutPath)))
|
|
30
|
+
return;
|
|
31
|
+
raw = YAML.parse(await readFile(closeoutPath, "utf-8"));
|
|
32
|
+
}
|
|
33
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
34
|
+
errors.push({ code: "closeout-schema-invalid", message: "closeout.yaml must be an object", path: "closeout.yaml" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const record = raw;
|
|
38
|
+
if (record.status !== "success")
|
|
39
|
+
return;
|
|
40
|
+
if (record.qa_verdict !== "pass") {
|
|
41
|
+
errors.push({
|
|
42
|
+
code: "qa-verdict-required-before-closeout",
|
|
43
|
+
message: "success closeout requires qa_verdict: pass",
|
|
44
|
+
path: "closeout.yaml:qa_verdict",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(record.qa_evidence) || record.qa_evidence.length === 0) {
|
|
48
|
+
errors.push({
|
|
49
|
+
code: "qa-evidence-required-before-closeout",
|
|
50
|
+
message: "success closeout requires non-empty qa_evidence",
|
|
51
|
+
path: "closeout.yaml:qa_evidence",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function checkDuplicateAcceptanceIds(acceptance, errors) {
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (const item of acceptance.acceptance) {
|
|
58
|
+
if (seen.has(item.id)) {
|
|
59
|
+
errors.push({
|
|
60
|
+
code: "duplicate-acceptance-id",
|
|
61
|
+
message: `duplicate acceptance id: ${item.id}`,
|
|
62
|
+
path: `acceptance.${item.id}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
seen.add(item.id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
22
68
|
async function loadAcceptanceSpec(featureDir, options, errors) {
|
|
23
69
|
const raw = options.acceptanceOverride ??
|
|
24
70
|
YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8"));
|
|
@@ -115,7 +161,8 @@ function checkCycles(graph, errors) {
|
|
|
115
161
|
for (const node of graph.nodes)
|
|
116
162
|
visit(node.id, []);
|
|
117
163
|
}
|
|
118
|
-
async function checkTaskFiles(featureDir, graph, errors) {
|
|
164
|
+
async function checkTaskFiles(featureDir, graph, acceptance, errors) {
|
|
165
|
+
const acceptanceIds = new Set(acceptance.acceptance.map((item) => item.id));
|
|
119
166
|
for (const node of graph.nodes) {
|
|
120
167
|
const taskPath = path.join(featureDir, "tasks", node.task);
|
|
121
168
|
if (!(await exists(taskPath))) {
|
|
@@ -126,7 +173,8 @@ async function checkTaskFiles(featureDir, graph, errors) {
|
|
|
126
173
|
});
|
|
127
174
|
continue;
|
|
128
175
|
}
|
|
129
|
-
const
|
|
176
|
+
const rawTask = YAML.parse(await readFile(taskPath, "utf-8"));
|
|
177
|
+
const parsedTask = taskSpecSchema.safeParse(rawTask);
|
|
130
178
|
if (!parsedTask.success) {
|
|
131
179
|
errors.push({
|
|
132
180
|
code: "task-spec-schema-invalid",
|
|
@@ -136,6 +184,23 @@ async function checkTaskFiles(featureDir, graph, errors) {
|
|
|
136
184
|
continue;
|
|
137
185
|
}
|
|
138
186
|
const taskSpec = parsedTask.data;
|
|
187
|
+
const detailed = await validateTaskSpec(rawTask, { taskSpecPath: taskPath });
|
|
188
|
+
for (const issue of detailed.errors) {
|
|
189
|
+
errors.push({
|
|
190
|
+
code: `task-${issue.code}`,
|
|
191
|
+
message: `${node.id}: ${issue.message}`,
|
|
192
|
+
path: `${node.task}:${issue.path ?? ""}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
for (const acceptanceRef of taskSpec.acceptance_refs) {
|
|
196
|
+
if (!acceptanceIds.has(acceptanceRef)) {
|
|
197
|
+
errors.push({
|
|
198
|
+
code: "task-acceptance-ref-missing",
|
|
199
|
+
message: `${node.id} references missing acceptance ${acceptanceRef}`,
|
|
200
|
+
path: `${node.task}:acceptance_refs`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
139
204
|
if (taskSpec.id !== node.id) {
|
|
140
205
|
errors.push({
|
|
141
206
|
code: "task-id-mismatch",
|
|
@@ -426,12 +426,12 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
426
426
|
phase: "intermediate",
|
|
427
427
|
taskConfig,
|
|
428
428
|
}),
|
|
429
|
-
final: adapter.getVerifyCommands(repoRoot, {
|
|
429
|
+
final: mergeFinalVerifyCommands(repoRoot, taskConfig, adapter.getVerifyCommands(repoRoot, {
|
|
430
430
|
preset,
|
|
431
431
|
quota: "full",
|
|
432
432
|
phase: "final",
|
|
433
433
|
taskConfig,
|
|
434
|
-
}),
|
|
434
|
+
})),
|
|
435
435
|
};
|
|
436
436
|
if (verifyCommands.final.length === 0) {
|
|
437
437
|
throw new Error("adapter returned no final verification commands");
|
|
@@ -455,6 +455,22 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
455
455
|
verifyCommands,
|
|
456
456
|
};
|
|
457
457
|
}
|
|
458
|
+
function mergeFinalVerifyCommands(repoRoot, taskConfig, adapterCommands) {
|
|
459
|
+
const taskCommands = taskConfig.verifyCommands.map((command) => ({
|
|
460
|
+
args: ["bash", "-lc", command.command],
|
|
461
|
+
cwd: repoRoot,
|
|
462
|
+
label: command.label,
|
|
463
|
+
timeoutMs: command.timeoutMs,
|
|
464
|
+
}));
|
|
465
|
+
const seen = new Set();
|
|
466
|
+
return [...taskCommands, ...adapterCommands].filter((command) => {
|
|
467
|
+
const key = `${command.cwd}\0${command.args.join("\0")}`;
|
|
468
|
+
if (seen.has(key))
|
|
469
|
+
return false;
|
|
470
|
+
seen.add(key);
|
|
471
|
+
return true;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
458
474
|
export function buildStandardHybridDagFromTask(sources) {
|
|
459
475
|
const { taskConfig } = sources;
|
|
460
476
|
const forbiddenPaths = mergeForbiddenPaths(taskConfig);
|
package/docs/README.md
CHANGED
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
- `exec-plans/completed/README.md` — 已完成的执行计划
|
|
35
35
|
- `progress/README.md` — 进度交接日志
|
|
36
36
|
- `reports/README.md` — 验证与审计报告
|
|
37
|
+
- `reports/2026-07-11-round3-worker-expansion.md` — Round 3 Worker 扩量、0.7.4 FE green、QA review blocker 与第二轮 owner decision
|
|
38
|
+
- `reports/2026-07-11-observe-dag-visualization.md` — Observe DAG edges、SVG 图形化、交互与真实 Chrome smoke 验证证据
|
|
39
|
+
- `reports/2026-07-11-0.7.5-init-evolution-review.md` — 0.7.5 package/version 变化对目标项目初始化 surface 的影响审查
|
|
37
40
|
- `decisions/README.md` — 架构决策
|
|
38
41
|
- `skills/README.md` — repo-local skill registry and vetting notes
|
|
39
42
|
- `templates/` — 可复用的规划、报告与 DAG 模板
|
|
@@ -54,6 +57,7 @@
|
|
|
54
57
|
- `templates/worker-dogfood-setup.md` — 发布控制器下的真实 Worker sample setup 与 retry 纪律
|
|
55
58
|
- `templates/worker-dogfood-evidence.md` — BE/FE/QA sample、Observe、morning report 与 coverage evidence 模板
|
|
56
59
|
- `templates/interactive-ui-round2-experiment.md` — interactive UI prompt/model A/B/C 对照实验与统一指标模板
|
|
60
|
+
- `templates/product-line/` — 可投影的 Feature/Task/QA/Links 产品线包;配合 `agent-worker task validate-feature` 做 docs CI
|
|
57
61
|
- `templates/production-readiness-checklist.md` — 低/中风险单仓库 DAG readiness 检查清单
|
|
58
62
|
- `templates/init-evolution-review.md` — 初始化能力演化审查报告模板
|
|
59
63
|
- `templates/adr.md` — 架构决策记录(ADR)
|
|
@@ -7,6 +7,5 @@
|
|
|
7
7
|
当前 active execution plan:
|
|
8
8
|
|
|
9
9
|
- `2026-07-10-release-0.6.0-nightly-drill.md` — 技术演练已完成;等待 0.6.0 发布 owner 对 Day-1 provisional remediation 作明确 ratify / revise / reject。
|
|
10
|
-
- `2026-07-11-round-2-interactive-ui-productization.md` — 补齐 Round-1 owner gate,发布 interactive-ui controller,完成 React A/B/C、Round 2/3 扩量、CI/cron 和产品线模板/docs CI。
|
|
11
10
|
|
|
12
|
-
- 已归档:`../completed/2026-07-11-interactive-ui-writer-routing.md`、`../completed/2026-07-10-next-stage-worker-evidence.md` 及更早计划。
|
|
11
|
+
- 已归档:`../completed/2026-07-11-observe-dag-visualization.md`、`../completed/2026-07-11-round-2-interactive-ui-productization.md`、`../completed/2026-07-11-interactive-ui-writer-routing.md`、`../completed/2026-07-10-next-stage-worker-evidence.md` 及更早计划。
|
|
@@ -9,6 +9,8 @@ npm 包携带本 README 作为目录契约。具体 completed plan 属于目标
|
|
|
9
9
|
- [`2026-07-04-runtime-boundary-remediation.md`](2026-07-04-runtime-boundary-remediation.md) — 整合 CLI/skill/runtime 边界,抽出 DAG/Loop runtime seam,集中 harness store/guard 策略
|
|
10
10
|
- [`2026-07-10-next-stage-worker-evidence.md`](2026-07-10-next-stage-worker-evidence.md) — Worker retry、EnvFailure、边界治理、初始化投影与真实 BE/FE/QA dogfood evidence 闭环
|
|
11
11
|
- [`2026-07-11-interactive-ui-writer-routing.md`](2026-07-11-interactive-ui-writer-routing.md) — 新增 interactive-ui writer-only HIGH 路由、UI 交付契约、真实 React dogfood fixture 与 Round-2 A/B/C 实验入口
|
|
12
|
+
- [`2026-07-11-round-2-interactive-ui-productization.md`](2026-07-11-round-2-interactive-ui-productization.md) — 完成 owner gate、0.7.0–0.7.4 发布、React A/B/C、20 个 Worker runs、外部 nightly、产品线模板与 docs CI
|
|
13
|
+
- [`2026-07-11-observe-dag-visualization.md`](2026-07-11-observe-dag-visualization.md) — Observe 投影 canonical DAG edges,新增确定性 SVG 依赖图、节点交互、视觉 tokens、可访问性与真实 Chrome smoke
|
|
12
14
|
- [`2026-07-04-dag-role-skill-alignment.md`](2026-07-04-dag-role-skill-alignment.md) — 对齐 DAG/Dynamic Workflow role 与 repo-local vetted skills,新增 strict skill audit
|
|
13
15
|
- [`2026-07-06-production-readiness-hardening.md`](2026-07-06-production-readiness-hardening.md) — 冻结 Production Readiness v0.1,打磨 DAG 主路径 next steps、failure routing、doctor/report/failure handoff 与 dogfood 验证
|
|
14
16
|
- [`2026-07-08-taskspec-schema-validate.md`](2026-07-08-taskspec-schema-validate.md) — 新增 TaskSpec v0.1 schema、三层校验器、risk→complexity 映射和 5 个 dogfood TaskSpec 样例
|
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
"docs/templates/worker-dogfood-setup.md",
|
|
18
18
|
"docs/templates/worker-dogfood-evidence.md",
|
|
19
19
|
"docs/templates/interactive-ui-round2-experiment.md",
|
|
20
|
+
"docs/templates/product-line/README.md",
|
|
21
|
+
"docs/templates/product-line/AGENTS.md",
|
|
22
|
+
"docs/templates/product-line/requirement.md",
|
|
23
|
+
"docs/templates/product-line/acceptance.yaml",
|
|
24
|
+
"docs/templates/product-line/design.md",
|
|
25
|
+
"docs/templates/product-line/test-plan.md",
|
|
26
|
+
"docs/templates/product-line/task-graph.yaml",
|
|
27
|
+
"docs/templates/product-line/task.yaml",
|
|
28
|
+
"docs/templates/product-line/closeout.yaml",
|
|
29
|
+
"docs/templates/product-line/links.md",
|
|
30
|
+
"scripts/check-product-line-docs.sh",
|
|
20
31
|
"docs/templates/agent-dag.schema.json",
|
|
21
32
|
"examples/example-dag.json",
|
|
22
33
|
"skills/loop-agent/SKILL.md",
|
|
@@ -46,6 +57,17 @@
|
|
|
46
57
|
"docs/templates/worker-dogfood-setup.md",
|
|
47
58
|
"docs/templates/worker-dogfood-evidence.md",
|
|
48
59
|
"docs/templates/interactive-ui-round2-experiment.md",
|
|
60
|
+
"docs/templates/product-line/README.md",
|
|
61
|
+
"docs/templates/product-line/AGENTS.md",
|
|
62
|
+
"docs/templates/product-line/requirement.md",
|
|
63
|
+
"docs/templates/product-line/acceptance.yaml",
|
|
64
|
+
"docs/templates/product-line/design.md",
|
|
65
|
+
"docs/templates/product-line/test-plan.md",
|
|
66
|
+
"docs/templates/product-line/task-graph.yaml",
|
|
67
|
+
"docs/templates/product-line/task.yaml",
|
|
68
|
+
"docs/templates/product-line/closeout.yaml",
|
|
69
|
+
"docs/templates/product-line/links.md",
|
|
70
|
+
"scripts/check-product-line-docs.sh",
|
|
49
71
|
"scripts/check-repo.sh",
|
|
50
72
|
"scripts/ci-governance.sh",
|
|
51
73
|
"scripts/ci-tests.sh",
|
|
@@ -91,6 +113,17 @@
|
|
|
91
113
|
"docs/templates/worker-dogfood-setup.md": "copied",
|
|
92
114
|
"docs/templates/worker-dogfood-evidence.md": "copied",
|
|
93
115
|
"docs/templates/interactive-ui-round2-experiment.md": "copied",
|
|
116
|
+
"docs/templates/product-line/README.md": "copied",
|
|
117
|
+
"docs/templates/product-line/AGENTS.md": "copied",
|
|
118
|
+
"docs/templates/product-line/requirement.md": "copied",
|
|
119
|
+
"docs/templates/product-line/acceptance.yaml": "copied",
|
|
120
|
+
"docs/templates/product-line/design.md": "copied",
|
|
121
|
+
"docs/templates/product-line/test-plan.md": "copied",
|
|
122
|
+
"docs/templates/product-line/task-graph.yaml": "copied",
|
|
123
|
+
"docs/templates/product-line/task.yaml": "copied",
|
|
124
|
+
"docs/templates/product-line/closeout.yaml": "copied",
|
|
125
|
+
"docs/templates/product-line/links.md": "copied",
|
|
126
|
+
"scripts/check-product-line-docs.sh": "copied",
|
|
94
127
|
"scripts/check-repo.sh": "generated",
|
|
95
128
|
"scripts/ci-governance.sh": "generated",
|
|
96
129
|
"scripts/ci-tests.sh": "generated",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Product-line execution rules
|
|
2
|
+
|
|
3
|
+
- Read requirement, acceptance, design, test plan, task graph, and the selected TaskSpec before writing.
|
|
4
|
+
- Treat `constraints.allowed_paths` and `constraints.forbidden_paths` as machine-enforced write boundaries; review DAG writer `writeSet` before execution.
|
|
5
|
+
- Advance only tasks whose dependencies are complete. Preserve failed run records; retries receive new worker run IDs.
|
|
6
|
+
- A Ready task must have acceptance references, non-empty allowed/forbidden paths, and deterministic verification commands.
|
|
7
|
+
- QA records an independent verdict and evidence. Do not write `status: success` closeout until `qa_verdict: pass` and non-empty `qa_evidence` exist.
|
|
8
|
+
- Human gates remain human decisions. Record owner, time, reason, evidence, and follow-up without rewriting failed history.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Product-line feature packet
|
|
2
|
+
|
|
3
|
+
Copy this directory for each product feature. Replace angle-bracket placeholders, keep IDs stable, and run:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
agent-worker task validate-feature <feature-dir>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The validator checks acceptance ID uniqueness, task references and dependencies, cycles, TaskSpec/path/verification completeness, and QA evidence before a successful closeout.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
feature_id: F-YYYY-NNN
|
|
3
|
+
acceptance:
|
|
4
|
+
- id: AC-FEATURE-001
|
|
5
|
+
title: Observable outcome is delivered
|
|
6
|
+
priority: must
|
|
7
|
+
type: behavior
|
|
8
|
+
given: the documented precondition
|
|
9
|
+
when: the user or system performs the action
|
|
10
|
+
then: the observable result occurs
|
|
11
|
+
verification:
|
|
12
|
+
expected_task_refs:
|
|
13
|
+
- BE-001
|
|
14
|
+
- QA-001
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Design — <Feature title>
|
|
2
|
+
|
|
3
|
+
## Context and boundaries
|
|
4
|
+
|
|
5
|
+
Describe existing components, public contracts, trust boundaries, and protected areas.
|
|
6
|
+
|
|
7
|
+
## Proposed change
|
|
8
|
+
|
|
9
|
+
Describe the smallest end-to-end path and its failure behavior.
|
|
10
|
+
|
|
11
|
+
## Decisions and alternatives
|
|
12
|
+
|
|
13
|
+
Record material trade-offs and link ADRs where needed.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Traceability links
|
|
2
|
+
|
|
3
|
+
| Relationship | Link or ID |
|
|
4
|
+
|---|---|
|
|
5
|
+
| Feature | `F-YYYY-NNN` |
|
|
6
|
+
| Acceptance → tests | `AC-FEATURE-001` → `TC-001` |
|
|
7
|
+
| Acceptance → tasks | `AC-FEATURE-001` → `BE-001`, `QA-001` |
|
|
8
|
+
| Task → worker/DAG run | `<workerRunId>` / `<dagRunId>` |
|
|
9
|
+
| Change/MR/PR | `<link>` |
|
|
10
|
+
| QA evidence | `<artifact path or link>` |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# <Feature title>
|
|
2
|
+
|
|
3
|
+
- Feature ID: `<F-YYYY-NNN>`
|
|
4
|
+
- Owner: `<human owner>`
|
|
5
|
+
- Outcome: `<user or business outcome>`
|
|
6
|
+
|
|
7
|
+
## In scope
|
|
8
|
+
|
|
9
|
+
- `<observable capability>`
|
|
10
|
+
|
|
11
|
+
## Out of scope
|
|
12
|
+
|
|
13
|
+
- `<explicit non-goal>`
|
|
14
|
+
|
|
15
|
+
## Constraints
|
|
16
|
+
|
|
17
|
+
- `<security, compatibility, cost, or operational boundary>`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
feature_id: F-YYYY-NNN
|
|
3
|
+
nodes:
|
|
4
|
+
- id: BE-001
|
|
5
|
+
task: BE-001.yaml
|
|
6
|
+
type: backend-feature
|
|
7
|
+
depends_on: []
|
|
8
|
+
- id: QA-001
|
|
9
|
+
task: QA-001.yaml
|
|
10
|
+
type: qa-execute
|
|
11
|
+
depends_on:
|
|
12
|
+
- BE-001
|
|
13
|
+
parallel_policy:
|
|
14
|
+
max_parallel_tasks: 2
|
|
15
|
+
disallow_same_file_parallel_writes: true
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
id: BE-001
|
|
3
|
+
feature_id: F-YYYY-NNN
|
|
4
|
+
title: <Bounded tracer-bullet task>
|
|
5
|
+
description: <Observable delivery>
|
|
6
|
+
repo:
|
|
7
|
+
name: <repository-name>
|
|
8
|
+
path_ref: <repository-path-reference>
|
|
9
|
+
default_branch: main
|
|
10
|
+
type: backend-feature
|
|
11
|
+
priority: P1
|
|
12
|
+
risk_level: medium
|
|
13
|
+
depends_on: []
|
|
14
|
+
source_docs:
|
|
15
|
+
requirement: ../requirement.md
|
|
16
|
+
acceptance: ../acceptance.yaml
|
|
17
|
+
design: ../design.md
|
|
18
|
+
test_plan: ../test-plan.md
|
|
19
|
+
acceptance_refs:
|
|
20
|
+
- AC-FEATURE-001
|
|
21
|
+
scope:
|
|
22
|
+
goals:
|
|
23
|
+
- <goal>
|
|
24
|
+
non_goals:
|
|
25
|
+
- <non-goal>
|
|
26
|
+
constraints:
|
|
27
|
+
allowed_paths:
|
|
28
|
+
- <owned/path/**>
|
|
29
|
+
forbidden_paths:
|
|
30
|
+
- <protected/path/**>
|
|
31
|
+
hard_constraints:
|
|
32
|
+
- Do not access production secrets
|
|
33
|
+
verify:
|
|
34
|
+
preset: standard
|
|
35
|
+
mode: serial
|
|
36
|
+
commands:
|
|
37
|
+
- id: focused-test
|
|
38
|
+
label: Focused deterministic verification
|
|
39
|
+
command: <project-specific verification command>
|
|
40
|
+
required: true
|
|
41
|
+
timeout_ms: 300000
|
|
42
|
+
worker:
|
|
43
|
+
execution_mode: generate-validate-run
|
|
44
|
+
max_attempts: 1
|
|
45
|
+
timeout_ms: 7200000
|
|
46
|
+
require_clean_worktree: true
|
|
47
|
+
require_human_review: true
|
|
48
|
+
create_worktree: false
|
|
49
|
+
loop_agent:
|
|
50
|
+
profile_policy: mapped
|
|
51
|
+
strict_models: true
|
|
52
|
+
strict_governance: true
|
|
53
|
+
no_cursor: false
|
|
54
|
+
max_concurrent: 1
|
|
55
|
+
outputs:
|
|
56
|
+
required:
|
|
57
|
+
- dag_json
|
|
58
|
+
- run_record
|
|
59
|
+
- shell_verification
|
|
60
|
+
- diff_patch
|
|
61
|
+
- closeout_or_failure_handoff
|
|
62
|
+
failure_policy:
|
|
63
|
+
failed_run_promote: false
|
|
64
|
+
generate_closeout_draft: true
|
|
65
|
+
create_followup_task: true
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Test plan — <Feature title>
|
|
2
|
+
|
|
3
|
+
| Test ID | Acceptance | Level | Scenario | Deterministic command |
|
|
4
|
+
|---|---|---|---|---|
|
|
5
|
+
| TC-001 | AC-FEATURE-001 | integration | happy path and primary failure | `<project-specific command>` |
|
|
6
|
+
|
|
7
|
+
Include negative cases, regression scope, environment assumptions, and evidence locations. Commands must match the target project's real toolchain.
|
|
@@ -26,6 +26,14 @@ npm run dev -- --help
|
|
|
26
26
|
npm pack --dry-run
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
产品线 Feature/Task/QA 文档或 TaskSpec 约束变更还应运行:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bash scripts/check-product-line-docs.sh
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
该检查对仓库中的 feature packet 执行 `agent-worker task validate-feature`,覆盖 AC 唯一性、依赖/环、TaskSpec 路径与验证命令、QA verdict 与 success closeout 顺序。
|
|
36
|
+
|
|
29
37
|
Windows 上通过 Git Bash 或已配置的兼容 Bash 运行 `scripts/*.sh`。跨平台 CLI 代码对真实文件操作用原生路径;`/` 仅用于稳定 repo 引用、JSON/Markdown 证据引用和 glob 约定。
|
|
30
38
|
|
|
31
39
|
没有相关门禁的新鲜命令输出,不得声明完成。
|
package/harness.json
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"checkHarnessRuntimeClean": "scripts/check-harness-runtime-clean.sh",
|
|
51
51
|
"checkInitEvolutionNeeded": "scripts/check-init-evolution-needed.sh",
|
|
52
52
|
"checkInitSurface": "scripts/check-init-surface.sh",
|
|
53
|
+
"checkProductLineDocs": "scripts/check-product-line-docs.sh",
|
|
53
54
|
"ci": "scripts/ci.sh"
|
|
54
55
|
},
|
|
55
56
|
"executors": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tea-agent/loop-agent",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"loop-agent": "bin/loop-agent.js",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"examples/",
|
|
26
26
|
"harness.json",
|
|
27
27
|
"skills/",
|
|
28
|
+
"scripts/check-product-line-docs.sh",
|
|
28
29
|
"README.md",
|
|
29
30
|
"CHANGELOG.md"
|
|
30
31
|
],
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
cd "${ROOT_DIR}"
|
|
6
|
+
|
|
7
|
+
feature_dirs=()
|
|
8
|
+
while IFS= read -r acceptance; do
|
|
9
|
+
feature_dirs+=("$(dirname "$acceptance")")
|
|
10
|
+
done < <(find dogfood/features -mindepth 2 -maxdepth 2 -name acceptance.yaml -type f | sort)
|
|
11
|
+
|
|
12
|
+
if [[ ${#feature_dirs[@]} -eq 0 ]]; then
|
|
13
|
+
echo "product-line docs check skipped: no feature packets found"
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
for feature_dir in "${feature_dirs[@]}"; do
|
|
18
|
+
echo "==> validate product-line feature: ${feature_dir}"
|
|
19
|
+
node --import tsx/esm src/worker/cli.ts task validate-feature "${feature_dir}"
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
echo "product-line docs checks passed: ${#feature_dirs[@]} feature packet(s)"
|