@tea-agent/loop-agent 0.20.1 → 0.22.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/CHANGELOG.md +72 -0
- package/bin/agent-worker.js +0 -0
- package/dist/adapters/loop-agent.js +52 -0
- package/dist/commands/init.js +104 -0
- package/dist/executors/dag-pi-executor.js +26 -0
- package/dist/executors/pi-executor.js +111 -36
- package/dist/executors/pi-sdk-executor.js +105 -29
- package/dist/executors/shell-executor.js +215 -29
- package/dist/shared/openspec-spec.js +49 -0
- package/dist/worker/loop-agent/loop-agent-client.js +43 -9
- package/dist/worker/observability/read-model.js +28 -2
- package/dist/worker/observe/spec-evidence.js +12 -15
- package/dist/worker/observe/static/constants.js +5 -0
- package/dist/worker/observe/static/dag-helpers.js +22 -0
- package/dist/worker/observe/static/format-pool.js +22 -3
- package/dist/worker/observe/static/styles.css +32 -3
- package/dist/worker/observe/static/views/dag-inspector.js +2 -2
- package/dist/worker/observe/static/views/dag.js +5 -0
- package/dist/worker/run-task/run-task.js +16 -6
- package/dist/workflows/dag/backend-test-markdown-workflow.js +328 -97
- package/dist/workflows/dag/backend-test-result-contract.js +10 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +141 -32
- package/dist/workflows/dag/frontend-lint-baseline.js +471 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +79 -16
- package/dist/workflows/dag/frontend-project-capability.js +11 -8
- package/dist/workflows/dag/frontend-repair.js +6 -4
- package/dist/workflows/dag/frontend-review-context.js +67 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +71 -66
- package/dist/workflows/dag/frontend-verification-trace.js +31 -1
- package/dist/workflows/dag/frontend-worktree-diff.js +81 -6
- package/dist/workflows/dag/init-hybrid.js +370 -79
- package/dist/workflows/dag/lifecycle.js +60 -4
- package/dist/workflows/dag/liveness-policy.js +250 -0
- package/dist/workflows/dag/node-execution.js +49 -0
- package/dist/workflows/dag/runner.js +21 -1
- package/dist/workflows/dag/types.js +67 -1
- package/docs/README.md +5 -6
- package/docs/architecture/dag-execution.md +11 -0
- package/docs/architecture/facts-and-state.md +1 -0
- package/docs/architecture/worker-and-feature.md +10 -0
- package/docs/templates/agent-dag.schema.json +15 -5
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -2
- package/docs/templates/backend-test-dag.json +15 -15
- package/docs/templates/frontend-implementation-contract.schema.json +4 -3
- package/docs/templates/frontend-test-case-checklist.md +6 -2
- package/docs/templates/frontend-test-dag.json +2 -2
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +12 -10
- package/skills/frontend-design-review/references/review-checklist.md +4 -4
- package/skills/frontend-implementation/SKILL.md +2 -2
- package/skills/frontend-implementation/references/code-standards.md +4 -3
- package/skills/frontend-implementation/references/design-spec.md +19 -14
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/frontend-review/SKILL.md +15 -28
- package/skills/frontend-review/references/review-findings.md +16 -18
- package/skills/frontend-verification/SKILL.md +16 -13
- package/skills/frontend-verification/references/verification-checklist.md +18 -30
- package/skills/loop-agent/references/command-reference.md +2 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
|
@@ -206,11 +206,12 @@ export class LoopAgentClient {
|
|
|
206
206
|
const stdoutPath = path.join(artifactDir, "stdout.txt");
|
|
207
207
|
const stderrPath = path.join(artifactDir, "stderr.txt");
|
|
208
208
|
const resultPath = path.join(artifactDir, "result.json");
|
|
209
|
+
const configuredTimeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
209
210
|
const { stdout, stdoutBytes, stdoutTruncated, stderr, stderrBytes, stderrTruncated, exitCode, timedOut, } = await spawnCommand({
|
|
210
211
|
command,
|
|
211
212
|
args: commandArgs,
|
|
212
213
|
cwd: options.cwd,
|
|
213
|
-
|
|
214
|
+
hardTimeoutMs: configuredTimeoutMs > 0 ? configuredTimeoutMs : undefined,
|
|
214
215
|
env: { ...process.env, ...this.env, ...options.env },
|
|
215
216
|
stdoutPath, stderrPath, resultPath,
|
|
216
217
|
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
@@ -362,23 +363,56 @@ function spawnCommand(input) {
|
|
|
362
363
|
let stdout = createBoundedOutput();
|
|
363
364
|
let stderr = createBoundedOutput();
|
|
364
365
|
let timedOut = false;
|
|
365
|
-
|
|
366
|
+
let settled = false;
|
|
367
|
+
const settle = (payload) => {
|
|
368
|
+
if (settled)
|
|
369
|
+
return;
|
|
370
|
+
settled = true;
|
|
371
|
+
clearTimers();
|
|
372
|
+
resolve({
|
|
373
|
+
stdout: formatBoundedOutput(stdout),
|
|
374
|
+
stdoutBytes: stdout.bytes,
|
|
375
|
+
stdoutTruncated: stdout.truncated,
|
|
376
|
+
stderr: formatBoundedOutput(stderr),
|
|
377
|
+
stderrBytes: stderr.bytes,
|
|
378
|
+
stderrTruncated: stderr.truncated,
|
|
379
|
+
exitCode: payload.exitCode,
|
|
380
|
+
timedOut: payload.timedOut,
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
let hardTimeout;
|
|
384
|
+
if (input.hardTimeoutMs !== undefined && input.hardTimeoutMs > 0) {
|
|
385
|
+
hardTimeout = setTimeout(() => {
|
|
386
|
+
timedOut = true;
|
|
387
|
+
child.kill("SIGTERM");
|
|
388
|
+
}, input.hardTimeoutMs);
|
|
389
|
+
}
|
|
366
390
|
let heartbeatTimer;
|
|
367
391
|
if (input.heartbeatIntervalMs > 0) {
|
|
368
392
|
heartbeatTimer = setInterval(() => { invokeHeartbeatCallback(input.onHeartbeat, startedAtMs); }, input.heartbeatIntervalMs);
|
|
369
393
|
}
|
|
370
|
-
const clearTimers = () => {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
394
|
+
const clearTimers = () => {
|
|
395
|
+
if (hardTimeout !== undefined)
|
|
396
|
+
clearTimeout(hardTimeout);
|
|
397
|
+
if (heartbeatTimer !== undefined) {
|
|
398
|
+
clearInterval(heartbeatTimer);
|
|
399
|
+
heartbeatTimer = undefined;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
374
402
|
child.stdout.setEncoding("utf8");
|
|
375
403
|
child.stderr.setEncoding("utf8");
|
|
376
404
|
child.stdout.on("data", (chunk) => { stdout = appendBoundedOutput(stdout, chunk); appendChunkBestEffort(input.stdoutPath, chunk); invokeChunkCallback(input.onStdout, chunk); });
|
|
377
405
|
child.stderr.on("data", (chunk) => { stderr = appendBoundedOutput(stderr, chunk); appendChunkBestEffort(input.stderrPath, chunk); invokeChunkCallback(input.onStderr, chunk); });
|
|
378
|
-
child.on("error", (error) => {
|
|
379
|
-
|
|
406
|
+
child.on("error", (error) => { if (!settled) {
|
|
407
|
+
settled = true;
|
|
380
408
|
clearTimers();
|
|
381
|
-
|
|
409
|
+
reject(error);
|
|
410
|
+
} });
|
|
411
|
+
child.on("close", (exitCode) => {
|
|
412
|
+
settle({
|
|
413
|
+
exitCode,
|
|
414
|
+
timedOut,
|
|
415
|
+
});
|
|
382
416
|
});
|
|
383
417
|
});
|
|
384
418
|
}
|
|
@@ -8,6 +8,7 @@ import { getTaskPoolRoot, listLegacyStateFiles, listTaskPoolStates, } from "../p
|
|
|
8
8
|
import { assessDagRunLiveness, assessDagRunRecoveryEligibility, deriveDagRunEffectiveStatus, } from "../../workflows/dag/lifecycle.js";
|
|
9
9
|
import { parseDagSpec, resolveModelForTask, } from "../../workflows/dag/types.js";
|
|
10
10
|
import { loadFeatureDecisionModels } from "../feature/decision-loader.js";
|
|
11
|
+
import { frontendLintAssessmentArtifactSchema } from "../../workflows/dag/frontend-lint-baseline.js";
|
|
11
12
|
/** Internal map key: featureId + NUL + taskId (stable, non-printable delimiter). */
|
|
12
13
|
export function taskIdentityKey(featureId, taskId) {
|
|
13
14
|
return `${featureId}\0${taskId}`;
|
|
@@ -717,6 +718,8 @@ function computeHealth(batches, tasks, dagRuns) {
|
|
|
717
718
|
const ACTIVE_EFFECTIVE_STATUSES = new Set([
|
|
718
719
|
"running",
|
|
719
720
|
"running-quiet",
|
|
721
|
+
"running-suspected-stall",
|
|
722
|
+
"needs-attention",
|
|
720
723
|
"remote-unknown",
|
|
721
724
|
"pending",
|
|
722
725
|
]);
|
|
@@ -796,8 +799,12 @@ function computeDagHealth(dagRuns) {
|
|
|
796
799
|
pausedRuns++;
|
|
797
800
|
if (dag.effectiveStatus === "failed")
|
|
798
801
|
failedRuns++;
|
|
799
|
-
if (liveness === "stale"
|
|
802
|
+
if (liveness === "stale"
|
|
803
|
+
|| liveness === "orphaned"
|
|
804
|
+
|| liveness === "suspected-stall"
|
|
805
|
+
|| liveness === "needs-attention") {
|
|
800
806
|
staleRuns++;
|
|
807
|
+
}
|
|
801
808
|
if (dag.effectiveStatus === "interrupted")
|
|
802
809
|
interruptedRuns++;
|
|
803
810
|
if (dag.stateConsistent === false)
|
|
@@ -1519,6 +1526,21 @@ async function loadBackendTestProjection(runDir, nodes) {
|
|
|
1519
1526
|
...(manifest && manifest.coverageSummary ? { coverage: manifest.coverageSummary } : {}),
|
|
1520
1527
|
};
|
|
1521
1528
|
}
|
|
1529
|
+
async function loadFrontendLintProjection(runDir) {
|
|
1530
|
+
const decoded = await safeReadJson(path.join(runDir, "contracts", "frontend-lint-assessment.json"));
|
|
1531
|
+
if (!decoded)
|
|
1532
|
+
return undefined;
|
|
1533
|
+
const parsed = frontendLintAssessmentArtifactSchema.safeParse(decoded);
|
|
1534
|
+
if (!parsed.success)
|
|
1535
|
+
return undefined;
|
|
1536
|
+
return {
|
|
1537
|
+
status: parsed.data.status,
|
|
1538
|
+
writerChangedFiles: parsed.data.writerChangedFiles,
|
|
1539
|
+
toleratedDiagnosticCount: parsed.data.toleratedDiagnosticCount,
|
|
1540
|
+
blockingDiagnosticCount: parsed.data.blockingDiagnostics.length,
|
|
1541
|
+
blockingReasons: parsed.data.blockingReasons,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1522
1544
|
async function parseDagStateFile(statePath, now) {
|
|
1523
1545
|
try {
|
|
1524
1546
|
if (!existsSync(statePath))
|
|
@@ -1573,7 +1595,10 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1573
1595
|
liveness: liveness.status,
|
|
1574
1596
|
})
|
|
1575
1597
|
: undefined;
|
|
1576
|
-
const backendTest = await
|
|
1598
|
+
const [backendTest, frontendLint] = await Promise.all([
|
|
1599
|
+
loadBackendTestProjection(runDir, nodes),
|
|
1600
|
+
loadFrontendLintProjection(runDir),
|
|
1601
|
+
]);
|
|
1577
1602
|
const continuationRecord = state.continuation &&
|
|
1578
1603
|
typeof state.continuation.parentRunId === "string" &&
|
|
1579
1604
|
typeof state.continuation.effectiveFromNodeId === "string" &&
|
|
@@ -1625,6 +1650,7 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1625
1650
|
dagPath: runDir,
|
|
1626
1651
|
...(continuation ? { continuation } : {}),
|
|
1627
1652
|
...(backendTest ? { backendTest } : {}),
|
|
1653
|
+
...(frontendLint ? { frontendLint } : {}),
|
|
1628
1654
|
};
|
|
1629
1655
|
}
|
|
1630
1656
|
catch {
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
5
|
+
import { isOpenspecSpecFilePath, isOpenspecSpecPath, isOpenspecSpecSearchTarget, } from "../../shared/openspec-spec.js";
|
|
5
6
|
const DAG_RUN_LIFECYCLE_DIRS = ["active", "completed", "paused"];
|
|
6
7
|
/**
|
|
7
8
|
* Known knowledge-base connector tool names.
|
|
@@ -18,7 +19,8 @@ const KB_CONNECTOR_TOOLS = new Set([
|
|
|
18
19
|
]);
|
|
19
20
|
/**
|
|
20
21
|
* Pattern for detecting spec-related files:
|
|
21
|
-
* - openspec
|
|
22
|
+
* - openspec files (generic evidence, including non-normative paths)
|
|
23
|
+
* - ai_workspace files
|
|
22
24
|
* - *.spec.md / *.spec.ts / *.spec.tsx
|
|
23
25
|
* - project-specs/**
|
|
24
26
|
* - design-spec.md, code-standards.md, review-checklist.md, etc.
|
|
@@ -27,6 +29,7 @@ const KB_CONNECTOR_TOOLS = new Set([
|
|
|
27
29
|
*/
|
|
28
30
|
const SPEC_FILE_PATTERNS = [
|
|
29
31
|
/openspec\//i,
|
|
32
|
+
/ai_workspace\//i,
|
|
30
33
|
/\/project-specs\//i,
|
|
31
34
|
/\/spec\//i,
|
|
32
35
|
/\.spec\.(md|tsx?|jsx?)$/i,
|
|
@@ -47,22 +50,16 @@ function isSpecFilePath(filePath) {
|
|
|
47
50
|
function isKnowledgeBaseTool(toolName) {
|
|
48
51
|
return KB_CONNECTOR_TOOLS.has(toolName);
|
|
49
52
|
}
|
|
50
|
-
/** A repo-relative path references
|
|
53
|
+
/** A repo-relative path references a canonical openspec spec directory. */
|
|
51
54
|
function isOpenspecPath(filePath) {
|
|
52
|
-
|
|
53
|
-
return normalized === "openspec" || normalized.startsWith("openspec/");
|
|
55
|
+
return isOpenspecSpecFilePath(filePath.replaceAll(path.sep, "/"));
|
|
54
56
|
}
|
|
55
|
-
/** A search query targets
|
|
57
|
+
/** A search query targets a canonical openspec spec directory. */
|
|
56
58
|
function isOpenspecSearch(query, searchPath) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
lower === "openspec" ||
|
|
62
|
-
lower === "openspec/" ||
|
|
63
|
-
lower.startsWith("openspec/") ||
|
|
64
|
-
lower.includes("openspec/**") ||
|
|
65
|
-
lower.includes("openspec/*"));
|
|
59
|
+
const normalizedPath = searchPath?.replaceAll(path.sep, "/");
|
|
60
|
+
if (normalizedPath && isOpenspecSpecPath(normalizedPath))
|
|
61
|
+
return true;
|
|
62
|
+
return isOpenspecSpecSearchTarget(query);
|
|
66
63
|
}
|
|
67
64
|
function resolveSessionEventsPath(repoRoot, dagRunId, nodeId) {
|
|
68
65
|
if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
|
|
@@ -362,7 +359,7 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
362
359
|
if (status === "no-evidence") {
|
|
363
360
|
summaryLines.push("未观察到任何规范证据:无 skill 注入、无文件读取、无检索操作。");
|
|
364
361
|
}
|
|
365
|
-
// Separate openspec
|
|
362
|
+
// Separate openspec spec directory reads and searches for Dashboard display.
|
|
366
363
|
// Knowledge base and openspec are parallel sources.
|
|
367
364
|
const openspecReads = specReads.filter((r) => isOpenspecPath(r.path));
|
|
368
365
|
const openspecSearches = searches.filter((s) => isOpenspecSearch(s.query, s.path));
|
|
@@ -75,6 +75,9 @@ export const LIVENESS_LABELS = {
|
|
|
75
75
|
active: "执行器正常",
|
|
76
76
|
"node-quiet": "执行器正常,节点长时间无活动",
|
|
77
77
|
quiet: "无输出(存活)",
|
|
78
|
+
"suspected-stall": "疑似卡死",
|
|
79
|
+
"needs-attention": "需人工关注",
|
|
80
|
+
"needs-reconcile": "需对账",
|
|
78
81
|
stale: "执行器心跳中断",
|
|
79
82
|
"timeout-risk": "即将超时",
|
|
80
83
|
orphaned: "执行器已退出",
|
|
@@ -86,6 +89,8 @@ export const DAG_EFFECTIVE_STATUS_LABELS = {
|
|
|
86
89
|
pending: "等待执行",
|
|
87
90
|
running: "正在执行",
|
|
88
91
|
"running-quiet": "运行可疑",
|
|
92
|
+
"running-suspected-stall": "疑似卡死",
|
|
93
|
+
"needs-attention": "需人工关注",
|
|
89
94
|
paused: "已暂停",
|
|
90
95
|
interrupted: "执行已中断",
|
|
91
96
|
"remote-unknown": "远端状态未知",
|
|
@@ -46,6 +46,28 @@ export function getActiveNodes(dag) {
|
|
|
46
46
|
return (dag.nodes ?? []).filter((n) => isNodeActive(n.status));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function frontendLintSummary(frontendLint) {
|
|
50
|
+
if (!frontendLint) return null;
|
|
51
|
+
const statusLabels = {
|
|
52
|
+
passed: "passed",
|
|
53
|
+
"baseline-debt": "baseline-debt",
|
|
54
|
+
failed: "failed",
|
|
55
|
+
unavailable: "unavailable",
|
|
56
|
+
};
|
|
57
|
+
const status = statusLabels[frontendLint.status] ?? "unavailable";
|
|
58
|
+
const changed = frontendLint.writerChangedFiles?.length ?? 0;
|
|
59
|
+
const tolerated = frontendLint.toleratedDiagnosticCount ?? 0;
|
|
60
|
+
const blocked = frontendLint.blockingDiagnosticCount ?? 0;
|
|
61
|
+
const reason = (frontendLint.blockingReasons ?? []).join(";");
|
|
62
|
+
return [
|
|
63
|
+
status,
|
|
64
|
+
`修改文件 ${changed}`,
|
|
65
|
+
`容忍存量诊断 ${tolerated}`,
|
|
66
|
+
`阻断诊断 ${blocked}`,
|
|
67
|
+
...(reason ? [`原因:${reason}`] : []),
|
|
68
|
+
].join(" · ");
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
export function dagSortTime(dag) {
|
|
50
72
|
return dag.startedAt ?? dag.finishedAt ?? "";
|
|
51
73
|
}
|
|
@@ -71,7 +71,18 @@ export function isPoolAttentionTask(task) {
|
|
|
71
71
|
const st = (task?.status ?? "").toLowerCase();
|
|
72
72
|
const live = (task?.liveness ?? "").toLowerCase();
|
|
73
73
|
if (["failed", "error", "blocked", "stale"].includes(st)) return true;
|
|
74
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
[
|
|
76
|
+
"stale",
|
|
77
|
+
"orphaned",
|
|
78
|
+
"timeout-risk",
|
|
79
|
+
"suspected-stall",
|
|
80
|
+
"needs-attention",
|
|
81
|
+
"needs-reconcile",
|
|
82
|
+
].includes(live)
|
|
83
|
+
) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
75
86
|
return false;
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -79,8 +90,16 @@ export function attentionRank(task) {
|
|
|
79
90
|
const st = (task?.status ?? "").toLowerCase();
|
|
80
91
|
const live = (task?.liveness ?? "").toLowerCase();
|
|
81
92
|
if (st === "failed" || st === "error") return 0;
|
|
82
|
-
if (live === "timeout-risk") return 1;
|
|
83
|
-
if (
|
|
93
|
+
if (live === "timeout-risk" || live === "needs-attention") return 1;
|
|
94
|
+
if (
|
|
95
|
+
st === "stale"
|
|
96
|
+
|| live === "stale"
|
|
97
|
+
|| live === "orphaned"
|
|
98
|
+
|| live === "suspected-stall"
|
|
99
|
+
|| live === "needs-reconcile"
|
|
100
|
+
) {
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
84
103
|
if (st === "blocked") return 3;
|
|
85
104
|
return 9;
|
|
86
105
|
}
|
|
@@ -845,10 +845,14 @@ td:first-child,
|
|
|
845
845
|
.kpi-tip {
|
|
846
846
|
position: absolute;
|
|
847
847
|
left: 12px;
|
|
848
|
-
right:
|
|
848
|
+
right: auto;
|
|
849
849
|
top: calc(100% - 8px);
|
|
850
850
|
z-index: 10;
|
|
851
|
-
|
|
851
|
+
/* Stable readable width independent of narrow KPI cards; viewport-safe cap. */
|
|
852
|
+
width: min(280px, calc(100vw - 24px));
|
|
853
|
+
min-width: min(240px, calc(100vw - 24px));
|
|
854
|
+
max-width: min(280px, calc(100vw - 24px));
|
|
855
|
+
box-sizing: border-box;
|
|
852
856
|
padding: 8px 10px;
|
|
853
857
|
border: 1px solid var(--hairline-strong);
|
|
854
858
|
border-radius: var(--radius-md);
|
|
@@ -863,6 +867,12 @@ td:first-child,
|
|
|
863
867
|
pointer-events: none;
|
|
864
868
|
transition: opacity var(--transition-fast);
|
|
865
869
|
}
|
|
870
|
+
/* Five-column default: right two columns open inward (left) to avoid viewport overflow. */
|
|
871
|
+
.kpi-grid:not(.pool-health-grid) > .kpi-card:nth-child(5n-1) .kpi-tip,
|
|
872
|
+
.kpi-grid:not(.pool-health-grid) > .kpi-card:nth-child(5n) .kpi-tip {
|
|
873
|
+
left: auto;
|
|
874
|
+
right: 12px;
|
|
875
|
+
}
|
|
866
876
|
.kpi-help:hover + .kpi-tip,
|
|
867
877
|
.kpi-help:focus-visible + .kpi-tip {
|
|
868
878
|
opacity: 1;
|
|
@@ -2015,7 +2025,7 @@ body.is-resizing-dag-graph {
|
|
|
2015
2025
|
.spec-evidence-back { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--hairline); border-radius: var(--radius-pill); background: var(--surface); color: var(--ink); font-size: 12px; font-weight: 650; cursor: pointer; }
|
|
2016
2026
|
.spec-evidence-back:hover, .spec-evidence-back:focus-visible { background: var(--orange-soft); border-color: var(--orange); color: var(--orange-active); outline: none; }
|
|
2017
2027
|
.spec-evidence-detail-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; color: var(--body); }
|
|
2018
|
-
.spec-evidence-detail-content { min-height: 0; max-width: 100%; margin: 0; padding: var(--space-3); border: 1px solid var(--hairline); border-radius: var(--radius-md); background: var(--canvas-soft);
|
|
2028
|
+
.spec-evidence-detail-content { min-height: 0; max-width: 100%; margin: 0; padding: var(--space-3); border: 1px solid var(--hairline); border-radius: var(--radius-md); background: var(--canvas-soft); overflow-wrap: anywhere; font-size: 12px; line-height: 1.6; }
|
|
2019
2029
|
.spec-evidence-detail-error { margin: 0; color: var(--red, #c00); font-size: 12px; }
|
|
2020
2030
|
.spec-evidence-detail-truncated { margin: 0; color: var(--muted); font-size: 11px; }
|
|
2021
2031
|
|
|
@@ -2289,6 +2299,16 @@ body.is-resizing-dag-graph {
|
|
|
2289
2299
|
.kpi-grid.pool-health-grid .kpi-card:first-child {
|
|
2290
2300
|
border-left: 0;
|
|
2291
2301
|
}
|
|
2302
|
+
/* Three-column: reset five-column tip edges, then rightmost column opens inward. */
|
|
2303
|
+
.kpi-grid:not(.pool-health-grid) > .kpi-card:nth-child(5n-1) .kpi-tip,
|
|
2304
|
+
.kpi-grid:not(.pool-health-grid) > .kpi-card:nth-child(5n) .kpi-tip {
|
|
2305
|
+
left: 12px;
|
|
2306
|
+
right: auto;
|
|
2307
|
+
}
|
|
2308
|
+
.kpi-grid:not(.pool-health-grid) > .kpi-card:nth-child(3n) .kpi-tip {
|
|
2309
|
+
left: auto;
|
|
2310
|
+
right: 12px;
|
|
2311
|
+
}
|
|
2292
2312
|
.panel-risk {
|
|
2293
2313
|
position: static;
|
|
2294
2314
|
}
|
|
@@ -2378,6 +2398,15 @@ body.is-resizing-dag-graph {
|
|
|
2378
2398
|
.kpi-grid.pool-health-grid .kpi-card:first-child {
|
|
2379
2399
|
border-left: 0;
|
|
2380
2400
|
}
|
|
2401
|
+
/* Two-column: reset three-column tip edges, then right column opens inward. */
|
|
2402
|
+
.kpi-grid:not(.pool-health-grid) > .kpi-card:nth-child(3n) .kpi-tip {
|
|
2403
|
+
left: 12px;
|
|
2404
|
+
right: auto;
|
|
2405
|
+
}
|
|
2406
|
+
.kpi-grid:not(.pool-health-grid) > .kpi-card:nth-child(2n) .kpi-tip {
|
|
2407
|
+
left: auto;
|
|
2408
|
+
right: 12px;
|
|
2409
|
+
}
|
|
2381
2410
|
.dag-inspector {
|
|
2382
2411
|
top: 12px;
|
|
2383
2412
|
right: 12px;
|
|
@@ -515,8 +515,8 @@ function renderSpecEvidenceDetail(content, dagRunId, nodeId) {
|
|
|
515
515
|
error.setAttribute("role", "alert");
|
|
516
516
|
wrapper.appendChild(error);
|
|
517
517
|
} else if (detail.data) {
|
|
518
|
-
const body = el("
|
|
519
|
-
body.
|
|
518
|
+
const body = el("div", "spec-evidence-detail-content");
|
|
519
|
+
body.appendChild(renderMarkdown(detail.data.content ?? ""));
|
|
520
520
|
wrapper.appendChild(body);
|
|
521
521
|
if (detail.data.truncated) {
|
|
522
522
|
const note = el(
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
} from "../shell-chrome.js";
|
|
80
80
|
import {
|
|
81
81
|
dagProgress,
|
|
82
|
+
frontendLintSummary,
|
|
82
83
|
isNodeFailed,
|
|
83
84
|
isNodeFinished,
|
|
84
85
|
isNodeActive,
|
|
@@ -223,6 +224,10 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
223
224
|
["成功/失败/跳过", `${succeededCount}/${failedCount}/${skippedCount}`],
|
|
224
225
|
["DAG 路径", dag.dagPath ?? "—"],
|
|
225
226
|
);
|
|
227
|
+
const lintSummary = frontendLintSummary(dag.frontendLint);
|
|
228
|
+
if (lintSummary) {
|
|
229
|
+
primaryEntries.push(["前端 lint 判定", lintSummary]);
|
|
230
|
+
}
|
|
226
231
|
const primaryGrid = metaGrid(primaryEntries);
|
|
227
232
|
const diagnosticGrid = metaGrid([
|
|
228
233
|
["当前判定", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
@@ -10,7 +10,8 @@ import { getTaskPoolRoot } from "../pool/run-store.js";
|
|
|
10
10
|
import { checkRequiredOutputs } from "../outcomes/gate.js";
|
|
11
11
|
import { projectOutcome } from "../outcomes/projector.js";
|
|
12
12
|
import { writeOutcome } from "../outcomes/store.js";
|
|
13
|
-
|
|
13
|
+
/** No outer wall-clock by default; the DAG kernel owns liveness supervision. */
|
|
14
|
+
export const DEFAULT_RUN_DAG_TIMEOUT_MS = 0;
|
|
14
15
|
export const MAX_WORKER_TIMEOUT_MS = 7_200_000;
|
|
15
16
|
export async function runTaskSpec(options) {
|
|
16
17
|
let controllerIdentity = resolveControllerIdentity(options.client, options.controllerIdentity);
|
|
@@ -346,6 +347,11 @@ async function runRequiredCommand(repoRoot, client, artifactName, args, expectJs
|
|
|
346
347
|
* not know the flag; their parser rejects before any DAG node can execute, so
|
|
347
348
|
* retrying the exact command without it is safe and preserves the same run id.
|
|
348
349
|
*/
|
|
350
|
+
function assertRunDagCommandOk(result, args) {
|
|
351
|
+
if (!result.ok) {
|
|
352
|
+
throw new Error(`loop-agent command failed: ${args.join(" ")}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
349
355
|
async function runDagWithEventsFallback(repoRoot, client, args, timeoutMs, eventCtx) {
|
|
350
356
|
const result = await client.run(args, {
|
|
351
357
|
cwd: repoRoot,
|
|
@@ -356,7 +362,7 @@ async function runDagWithEventsFallback(repoRoot, client, args, timeoutMs, event
|
|
|
356
362
|
if (result.ok)
|
|
357
363
|
return result;
|
|
358
364
|
if (!rejectsEventsJsonl(result) || !args.includes("--events-jsonl")) {
|
|
359
|
-
|
|
365
|
+
assertRunDagCommandOk(result, args);
|
|
360
366
|
}
|
|
361
367
|
const fallbackArgs = withoutFlagAndValue(args, "--events-jsonl");
|
|
362
368
|
eventCtx.progress.note("run-dag controller does not support --events-jsonl; retrying without DAG event stream");
|
|
@@ -366,9 +372,7 @@ async function runDagWithEventsFallback(repoRoot, client, args, timeoutMs, event
|
|
|
366
372
|
expectJson: true,
|
|
367
373
|
timeoutMs,
|
|
368
374
|
});
|
|
369
|
-
|
|
370
|
-
throw new Error(`loop-agent command failed: ${fallbackArgs.join(" ")}`);
|
|
371
|
-
}
|
|
375
|
+
assertRunDagCommandOk(fallback, fallbackArgs);
|
|
372
376
|
return fallback;
|
|
373
377
|
}
|
|
374
378
|
function rejectsEventsJsonl(result) {
|
|
@@ -424,8 +428,14 @@ async function runObservedStep(ctx, stepName, run, options) {
|
|
|
424
428
|
throw error;
|
|
425
429
|
}
|
|
426
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Explicit worker.timeout_ms remains an operator hard limit. Omission disables
|
|
433
|
+
* the Worker wall clock so the DAG kernel can supervise active nodes.
|
|
434
|
+
*/
|
|
427
435
|
function resolveRunDagTimeoutMs(taskSpec) {
|
|
428
|
-
return
|
|
436
|
+
return taskSpec.worker.timeout_ms === undefined
|
|
437
|
+
? DEFAULT_RUN_DAG_TIMEOUT_MS
|
|
438
|
+
: Math.min(taskSpec.worker.timeout_ms, MAX_WORKER_TIMEOUT_MS);
|
|
429
439
|
}
|
|
430
440
|
async function collectFailureArtifacts(input) {
|
|
431
441
|
const doctor = await runObservedStep(input.eventCtx, "dag-doctor", () => input.client.run(["dag", "doctor", "--run-id", input.workerRunId, "--markdown"], {
|