@tea-agent/loop-agent 0.35.1-beta.3 → 0.35.1
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 +0 -2
- package/CHANGELOG.md +25 -24
- package/bin/loop-agent.js +1 -37
- package/dist/application/dag/generate-task-dag.js +4 -1
- package/dist/application/task-lifecycle/advance.js +14 -0
- package/dist/cli/program.js +2 -2
- package/dist/commands/task-advance.js +1 -0
- package/dist/executors/dag-pi-executor.js +0 -44
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/config-types.js +2 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/source-prepare/build-draft.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +37 -10
- package/dist/task/task-demand-routing.js +10 -0
- package/dist/worker/console/operator-actions.js +72 -6
- package/dist/worker/console/prd-intake-bridge.js +10 -3
- package/dist/worker/console/prd-reference-discovery.js +124 -0
- package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
- package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
- package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +26 -49
- package/dist/workflows/dag/node-execution.js +0 -89
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/runner.js +11 -245
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +2 -130
- package/package.json +2 -2
- package/dist/build-stamp.json +0 -6
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<link rel="stylesheet" href="/inspect/operator-chrome.css" />
|
|
8
8
|
<title>Loop 操作台 · Operator Console</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-M0BLEBfh.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-HX1pbOyl.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -2,6 +2,21 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
3
3
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
4
4
|
import { confirmationToken, CONFIRMATION_HEADER, LONG_RUNNING_ACTIONS, postOperator, waitForOperation, } from "./console-types.js";
|
|
5
|
+
/** Human-readable in-progress copy for sync actions that take tens of
|
|
6
|
+
* seconds server-side (imports + intake). Prevents the "page looks frozen"
|
|
7
|
+
* gap between click and the terminal flash message. */
|
|
8
|
+
const ACTION_PROGRESS_COPY = {
|
|
9
|
+
bootstrapFromPrd: "正在导入 PRD 并创建任务:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
|
|
10
|
+
importPrd: "正在导入 PRD:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
|
|
11
|
+
interviewStart: "正在启动需求访谈…",
|
|
12
|
+
interviewTurn: "正在提交访谈回答…",
|
|
13
|
+
interviewSkip: "正在跳过访谈并生成评估…",
|
|
14
|
+
contractValidate: "正在校验契约草稿…",
|
|
15
|
+
contractDiff: "正在计算契约差异…",
|
|
16
|
+
contractApply: "正在应用契约(写入需求与执行约束)…",
|
|
17
|
+
spineCheck: "正在运行 Spine 审计…",
|
|
18
|
+
dagGenerate: "正在生成 DAG…",
|
|
19
|
+
};
|
|
5
20
|
/** Central operator-action dispatcher: POST /api/operator/v1/operations with
|
|
6
21
|
* polling, long-running background waits, interview payload handoff and the
|
|
7
22
|
* per-operation SSE follow log. */
|
|
@@ -18,7 +33,10 @@ export function useOperatorActions(params) {
|
|
|
18
33
|
setBusy(true);
|
|
19
34
|
setOpError(null);
|
|
20
35
|
setOpErrorCode(null);
|
|
21
|
-
|
|
36
|
+
// Show an immediate progress hint instead of a blank flash region:
|
|
37
|
+
// sync actions (imports/intake) block for tens of seconds with no
|
|
38
|
+
// other UI feedback, which reads as a frozen page.
|
|
39
|
+
setOpMessage(ACTION_PROGRESS_COPY[action] ?? null);
|
|
22
40
|
try {
|
|
23
41
|
const { status, body } = await postOperator(action, actionParams, opts?.clientRequestId);
|
|
24
42
|
if (status === 412 || body.error?.code === "PI_SETUP_REQUIRED") {
|
|
@@ -22,7 +22,6 @@ export function useRecoveryConsole(params) {
|
|
|
22
22
|
const [recoveryNodesLoading, setRecoveryNodesLoading] = useState(false);
|
|
23
23
|
const [recoveryRunStatus, setRecoveryRunStatus] = useState(null);
|
|
24
24
|
const [recoveryFailureCategory, setRecoveryFailureCategory] = useState(null);
|
|
25
|
-
const [frontendRecovery, setFrontendRecovery] = useState(null);
|
|
26
25
|
const [manualRunId, setManualRunId] = useState(false);
|
|
27
26
|
const [manualNodeId, setManualNodeId] = useState(false);
|
|
28
27
|
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
@@ -125,7 +124,6 @@ export function useRecoveryConsole(params) {
|
|
|
125
124
|
setRecoveryNodes([]);
|
|
126
125
|
setRecoveryRunStatus(null);
|
|
127
126
|
setRecoveryFailureCategory(null);
|
|
128
|
-
setFrontendRecovery(null);
|
|
129
127
|
return;
|
|
130
128
|
}
|
|
131
129
|
setRecoveryNodesLoading(true);
|
|
@@ -169,13 +167,11 @@ export function useRecoveryConsole(params) {
|
|
|
169
167
|
n.failureCategory &&
|
|
170
168
|
n.failureCategory !== "success")?.failureCategory;
|
|
171
169
|
setRecoveryFailureCategory(runFailure ?? nodeFailure ?? null);
|
|
172
|
-
setFrontendRecovery(body.frontendRecovery ?? null);
|
|
173
170
|
}
|
|
174
171
|
catch {
|
|
175
172
|
setRecoveryNodes([]);
|
|
176
173
|
setRecoveryRunStatus(null);
|
|
177
174
|
setRecoveryFailureCategory(null);
|
|
178
|
-
setFrontendRecovery(null);
|
|
179
175
|
}
|
|
180
176
|
finally {
|
|
181
177
|
setRecoveryNodesLoading(false);
|
|
@@ -327,7 +323,6 @@ export function useRecoveryConsole(params) {
|
|
|
327
323
|
recoveryRunsLoading,
|
|
328
324
|
recoveryNodesLoading,
|
|
329
325
|
recoveryRunStatus,
|
|
330
|
-
frontendRecovery,
|
|
331
326
|
manualRunId,
|
|
332
327
|
setManualRunId,
|
|
333
328
|
manualNodeId,
|
|
@@ -208,12 +208,24 @@ export function useTaskWizard(params) {
|
|
|
208
208
|
const semanticNote = result.semanticIntake?.applied
|
|
209
209
|
? "semantic"
|
|
210
210
|
: (result.semanticIntake?.code ?? "no-semantic");
|
|
211
|
+
const autoImported = (result.autoImportedReferences ?? []).filter((ref) => ref.repoRelative);
|
|
212
|
+
const unresolvedRefs = (result.unresolvedReferences ?? []).filter((ref) => ref.token);
|
|
211
213
|
setOpMessage([
|
|
212
214
|
`已创建任务「${result.title ?? ""}」(${result.taskId ?? ""})`,
|
|
213
215
|
`来源:${result.identitySource ?? "prd"}`,
|
|
214
216
|
`解析:${parseLabel}(${semanticNote})`,
|
|
215
217
|
objectiveHint ? `目标:${objectiveHint.slice(0, 120)}` : null,
|
|
216
218
|
gapCount > 0 ? `缺口 ${gapCount} 项待确认` : null,
|
|
219
|
+
autoImported.length > 0
|
|
220
|
+
? `已自动导入 ${autoImported.length} 个引用文档(${autoImported
|
|
221
|
+
.map((ref) => `${ref.repoRelative}${ref.role ? `:${ref.role}` : ""}`)
|
|
222
|
+
.join("、")})`
|
|
223
|
+
: null,
|
|
224
|
+
unresolvedRefs.length > 0
|
|
225
|
+
? `有 ${unresolvedRefs.length} 个引用未能解析:${unresolvedRefs
|
|
226
|
+
.map((ref) => `${ref.token}(${ref.reason ?? "未知原因"})`)
|
|
227
|
+
.join("、")}`
|
|
228
|
+
: null,
|
|
217
229
|
]
|
|
218
230
|
.filter(Boolean)
|
|
219
231
|
.join(" · "));
|
|
@@ -2,15 +2,11 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { appendFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript,
|
|
5
|
+
import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript, readPackageName, readPackageVersion, resolveBinPaths, resolveCmdShim, } from "../../shared/package-metadata.js";
|
|
6
6
|
import { parseCommandJson } from "./parse-json.js";
|
|
7
7
|
export const DEFAULT_WORKER_COMMAND_TIMEOUT_MS = 120_000;
|
|
8
8
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
9
9
|
export const LOOP_AGENT_PACKAGE_NAME = "@tea-agent/loop-agent";
|
|
10
|
-
/** Grace window between the SIGTERM and SIGKILL stages of a hard timeout, so a
|
|
11
|
-
* runner can persist terminal state (see executeDagCheckpoint) before being
|
|
12
|
-
* force-killed. */
|
|
13
|
-
export const HARD_TIMEOUT_KILL_GRACE_MS = 10_000;
|
|
14
10
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
15
11
|
/**
|
|
16
12
|
* Bind evidence supplied by an orchestration layer to the identity exposed by
|
|
@@ -182,11 +178,10 @@ export class LoopAgentClient {
|
|
|
182
178
|
if (!this._identity) {
|
|
183
179
|
throw new Error("controller identity: cannot record a reported version without a pinned identity");
|
|
184
180
|
}
|
|
185
|
-
|
|
186
|
-
if (normalized !== this._identity.packageVersion) {
|
|
181
|
+
if (reportedVersion !== this._identity.packageVersion) {
|
|
187
182
|
throw new Error(`controller identity: reported version ${reportedVersion} does not match package version ${this._identity.packageVersion}`);
|
|
188
183
|
}
|
|
189
|
-
this._identity = { ...this._identity, reportedVersion
|
|
184
|
+
this._identity = { ...this._identity, reportedVersion };
|
|
190
185
|
return this._identity;
|
|
191
186
|
}
|
|
192
187
|
async run(args, options) {
|
|
@@ -386,17 +381,10 @@ function spawnCommand(input) {
|
|
|
386
381
|
});
|
|
387
382
|
};
|
|
388
383
|
let hardTimeout;
|
|
389
|
-
let killGraceTimeout;
|
|
390
384
|
if (input.hardTimeoutMs !== undefined && input.hardTimeoutMs > 0) {
|
|
391
385
|
hardTimeout = setTimeout(() => {
|
|
392
386
|
timedOut = true;
|
|
393
|
-
// Two-stage termination: SIGTERM first so the runner can persist a
|
|
394
|
-
// terminal state (see executeDagCheckpoint signal handler), then
|
|
395
|
-
// SIGKILL only after the grace window expires.
|
|
396
387
|
child.kill("SIGTERM");
|
|
397
|
-
killGraceTimeout = setTimeout(() => {
|
|
398
|
-
child.kill("SIGKILL");
|
|
399
|
-
}, HARD_TIMEOUT_KILL_GRACE_MS);
|
|
400
388
|
}, input.hardTimeoutMs);
|
|
401
389
|
}
|
|
402
390
|
let heartbeatTimer;
|
|
@@ -406,8 +394,6 @@ function spawnCommand(input) {
|
|
|
406
394
|
const clearTimers = () => {
|
|
407
395
|
if (hardTimeout !== undefined)
|
|
408
396
|
clearTimeout(hardTimeout);
|
|
409
|
-
if (killGraceTimeout !== undefined)
|
|
410
|
-
clearTimeout(killGraceTimeout);
|
|
411
397
|
if (heartbeatTimer !== undefined) {
|
|
412
398
|
clearInterval(heartbeatTimer);
|
|
413
399
|
heartbeatTimer = undefined;
|
|
@@ -1491,7 +1491,6 @@ function mergeDagRun(existing, incoming) {
|
|
|
1491
1491
|
failureCategory: incoming.failureCategory ?? existing.failureCategory,
|
|
1492
1492
|
stateConsistent: incoming.stateConsistent ?? existing.stateConsistent,
|
|
1493
1493
|
recoveryEligibility: incoming.recoveryEligibility ?? existing.recoveryEligibility,
|
|
1494
|
-
frontendRecovery: incoming.frontendRecovery ?? existing.frontendRecovery,
|
|
1495
1494
|
title: incoming.title ?? existing.title,
|
|
1496
1495
|
startedAt,
|
|
1497
1496
|
finishedAt,
|
|
@@ -1791,23 +1790,6 @@ async function loadFrontendLintProjection(runDir) {
|
|
|
1791
1790
|
blockingReasons: parsed.data.blockingReasons,
|
|
1792
1791
|
};
|
|
1793
1792
|
}
|
|
1794
|
-
function projectFrontendRecovery(state) {
|
|
1795
|
-
const s = state.frontendRecoveryState;
|
|
1796
|
-
const r = state.frontendRecoveryResult;
|
|
1797
|
-
if (!s && !r)
|
|
1798
|
-
return undefined;
|
|
1799
|
-
return {
|
|
1800
|
-
...(s?.recoveryRootRunId ? { recoveryRootRunId: s.recoveryRootRunId } : {}),
|
|
1801
|
-
...(s?.parentRunId ? { parentRunId: s.parentRunId } : {}),
|
|
1802
|
-
...(s?.childRunId ? { childRunId: s.childRunId } : {}),
|
|
1803
|
-
...(s?.attemptIndex !== undefined ? { attemptIndex: s.attemptIndex } : {}),
|
|
1804
|
-
...(s?.continuationCount !== undefined
|
|
1805
|
-
? { continuationCount: s.continuationCount }
|
|
1806
|
-
: {}),
|
|
1807
|
-
...(s?.phase ? { phase: s.phase } : {}),
|
|
1808
|
-
...(r?.outcome ? { outcome: r.outcome } : {}),
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
1811
1793
|
async function parseDagStateFile(statePath, now) {
|
|
1812
1794
|
try {
|
|
1813
1795
|
if (!existsSync(statePath))
|
|
@@ -1878,7 +1860,6 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1878
1860
|
loadBackendTestProjection(runDir, nodes),
|
|
1879
1861
|
loadFrontendLintProjection(runDir),
|
|
1880
1862
|
]);
|
|
1881
|
-
const frontendRecovery = projectFrontendRecovery(state);
|
|
1882
1863
|
const continuationRecord = state.continuation &&
|
|
1883
1864
|
typeof state.continuation.parentRunId === "string" &&
|
|
1884
1865
|
typeof state.continuation.effectiveFromNodeId === "string" &&
|
|
@@ -1937,7 +1918,6 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1937
1918
|
...(continuation ? { continuation } : {}),
|
|
1938
1919
|
...(backendTest ? { backendTest } : {}),
|
|
1939
1920
|
...(frontendLint ? { frontendLint } : {}),
|
|
1940
|
-
...(frontendRecovery ? { frontendRecovery } : {}),
|
|
1941
1921
|
};
|
|
1942
1922
|
}
|
|
1943
1923
|
catch {
|
package/dist/worker/preflight.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { normalizeCliVersionLabel } from "../shared/package-metadata.js";
|
|
4
3
|
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "./loop-agent/loop-agent-client.js";
|
|
5
4
|
export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
|
|
6
5
|
export async function preflightTargetRepo(input) {
|
|
@@ -48,7 +47,7 @@ export async function preflightTargetRepo(input) {
|
|
|
48
47
|
controllerIdentity: identity,
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
|
-
const reportedVersion =
|
|
50
|
+
const reportedVersion = version.stdout.trim();
|
|
52
51
|
let controllerIdentity = identity
|
|
53
52
|
? { ...identity, reportedVersion }
|
|
54
53
|
: undefined;
|
|
@@ -724,40 +724,30 @@ export function deterministicRewriteScenarioParam(input) {
|
|
|
724
724
|
return { ok: true, source: nextSource, detail: "rewritten" };
|
|
725
725
|
}
|
|
726
726
|
function validatePythonSyntaxWithoutExecution(source) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
timeout: 15_000,
|
|
743
|
-
});
|
|
744
|
-
if (result.error) {
|
|
745
|
-
lastError = result.error.message;
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (result.status !== 0) {
|
|
749
|
-
const detail = String(result.stderr || result.stdout || "invalid Python syntax")
|
|
750
|
-
.trim()
|
|
751
|
-
.replace(/\s+/g, " ")
|
|
752
|
-
.slice(0, 500);
|
|
753
|
-
return { ok: false, detail: `python syntax validation failed: ${detail}` };
|
|
754
|
-
}
|
|
755
|
-
return { ok: true, detail: "python ast.parse PASS" };
|
|
727
|
+
const result = spawnSync("python", ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
|
|
728
|
+
input: source,
|
|
729
|
+
encoding: "utf8",
|
|
730
|
+
env: {
|
|
731
|
+
...process.env,
|
|
732
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
733
|
+
PYTHONUTF8: "1",
|
|
734
|
+
},
|
|
735
|
+
timeout: 15_000,
|
|
736
|
+
});
|
|
737
|
+
if (result.error) {
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
detail: `python syntax validation unavailable: ${result.error.message}`,
|
|
741
|
+
};
|
|
756
742
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
743
|
+
if (result.status !== 0) {
|
|
744
|
+
const detail = String(result.stderr || result.stdout || "invalid Python syntax")
|
|
745
|
+
.trim()
|
|
746
|
+
.replace(/\s+/g, " ")
|
|
747
|
+
.slice(0, 500);
|
|
748
|
+
return { ok: false, detail: `python syntax validation failed: ${detail}` };
|
|
749
|
+
}
|
|
750
|
+
return { ok: true, detail: "python ast.parse PASS" };
|
|
761
751
|
}
|
|
762
752
|
export async function applyDeterministicScenarioParamRepairs(input) {
|
|
763
753
|
const repairable = input.facts.entries.filter((entry) => entry.status === "MISMATCH" && entry.repairability === "repairable");
|
|
@@ -31,15 +31,7 @@ export function getNodeOutputAsJson(state, nodeId) {
|
|
|
31
31
|
export function normalizeVerdictCandidateLine(value) {
|
|
32
32
|
const trimmed = String(value).trim();
|
|
33
33
|
const emphasized = trimmed.match(/^\*{1,3}\s*(VERDICT:[^*]+?)\s*\*{1,3}$/);
|
|
34
|
-
|
|
35
|
-
// Canonicalize the colon separator and trailing sentence punctuation so
|
|
36
|
-
// "VERDICT:pass", "VERDICT: pass", or "VERDICT: pass." match the declared
|
|
37
|
-
// enum lines without accepting free-text verdicts. Conflicting verdicts
|
|
38
|
-
// still canonicalize to distinct enum lines and remain fail-closed.
|
|
39
|
-
const verdictMatch = line.match(/^VERDICT:\s*(pass|request-revision)\s*[.!?。!?]*$/i);
|
|
40
|
-
if (verdictMatch)
|
|
41
|
-
return `VERDICT: ${verdictMatch[1].toLowerCase()}`;
|
|
42
|
-
return line;
|
|
34
|
+
return (emphasized ? emphasized[1] : trimmed).trim();
|
|
43
35
|
}
|
|
44
36
|
/** First line matching /^VERDICT:/ from node assistantText/stdout (markdown emphasis OK). */
|
|
45
37
|
export function extractFirstVerdictLineFromNodeText(text) {
|