@tea-agent/loop-agent 0.35.1-beta.4 → 0.35.2
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 +43 -26
- 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/chat/assistant-content.js +110 -0
- package/dist/worker/console/chat/pi-runtime.js +13 -32
- package/dist/worker/console/chat/routes.js +9 -4
- package/dist/worker/console/chat/session-store.js +3 -0
- package/dist/worker/console/chat/shortcuts.js +9 -7
- package/dist/worker/console/chat/turn-process.js +62 -15
- 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-DRqZiQ7J.css +1 -0
- package/dist/worker/console/static/assets/index-gVHrlqI9.js +56 -0
- 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/console/static-src/operator-chat/chat-sse-events.js +10 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -6
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +11 -6
- package/dist/worker/console/static-src/operator-chat/useComposer.js +15 -1
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- 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/harness.json +1 -1
- package/package.json +2 -2
- package/dist/build-stamp.json +0 -6
- package/dist/worker/console/static/assets/index-CvsQgALl.js +0 -56
- package/dist/worker/console/static/assets/index-hJqCPs_g.css +0 -1
- 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-gVHrlqI9.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DRqZiQ7J.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,6 +2,7 @@ import { artifactFromEventPayload, mergeArtifactCardState, } from "../../chat/ar
|
|
|
2
2
|
import { humanGateCardFromEventPayload, mergeHumanGateCardState, } from "../../chat/human-gate-card.js";
|
|
3
3
|
import { mergeOperationCardState, operationFromEventPayload, } from "../../chat/operation-card.js";
|
|
4
4
|
import { requestId } from "./format.js";
|
|
5
|
+
import { parseAssistantContent } from "../../chat/assistant-content.js";
|
|
5
6
|
/** Shared handler for a single parsed Chat SSE event (M1 reconnect).
|
|
6
7
|
*
|
|
7
8
|
* Used by BOTH the inline prompt POST SSE and the background reconnect
|
|
@@ -214,10 +215,17 @@ export function createChatSseEventApplier(deps) {
|
|
|
214
215
|
return;
|
|
215
216
|
}
|
|
216
217
|
const text = typeof payload.text === "string" ? payload.text : "";
|
|
218
|
+
const content = parseAssistantContent(payload.content);
|
|
217
219
|
setMessages((m) => {
|
|
218
220
|
const exists = m.some((msg) => msg.id === resolvedAssistantId);
|
|
219
221
|
if (exists) {
|
|
220
|
-
return m.map((msg) => msg.id === resolvedAssistantId
|
|
222
|
+
return m.map((msg) => msg.id === resolvedAssistantId
|
|
223
|
+
? {
|
|
224
|
+
...msg,
|
|
225
|
+
text,
|
|
226
|
+
...(content ? { content } : {}),
|
|
227
|
+
}
|
|
228
|
+
: msg);
|
|
221
229
|
}
|
|
222
230
|
// Reconnect path: the assistant message may not exist yet
|
|
223
231
|
// (e.g. only tool_call events arrived). Append a placeholder.
|
|
@@ -228,6 +236,7 @@ export function createChatSseEventApplier(deps) {
|
|
|
228
236
|
role: "assistant",
|
|
229
237
|
text,
|
|
230
238
|
createdAt: Date.now(),
|
|
239
|
+
...(content ? { content } : {}),
|
|
231
240
|
},
|
|
232
241
|
];
|
|
233
242
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { parseAssistantContent } from "../../chat/assistant-content.js";
|
|
2
3
|
import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
|
|
3
4
|
import { hydrateSessionThread } from "../../chat/turn-process.js";
|
|
4
5
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
@@ -301,12 +302,16 @@ export function useChatSessions(params) {
|
|
|
301
302
|
setInput(body.session.composerDraft ?? "");
|
|
302
303
|
setMessages((body.session.messages ?? [])
|
|
303
304
|
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
304
|
-
.map((message) =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
.map((message) => {
|
|
306
|
+
const content = parseAssistantContent(message.content);
|
|
307
|
+
return {
|
|
308
|
+
id: message.id,
|
|
309
|
+
role: message.role,
|
|
310
|
+
text: message.text,
|
|
311
|
+
createdAt: Date.parse(message.at) || Date.now(),
|
|
312
|
+
...(content && content.length > 0 ? { content } : {}),
|
|
313
|
+
};
|
|
314
|
+
}));
|
|
310
315
|
}
|
|
311
316
|
catch {
|
|
312
317
|
// Recovery is best-effort; a new session remains available.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { parseAssistantContent } from "../../chat/assistant-content.js";
|
|
2
3
|
import { buildTurnProcessGroups } from "../../chat/turn-process.js";
|
|
3
4
|
import { createChatSseEventApplier, } from "./chat-sse-events.js";
|
|
4
5
|
import { taskContextUnchanged } from "./format.js";
|
|
@@ -220,12 +221,16 @@ export function useChatThread(params) {
|
|
|
220
221
|
refs.turnAssistantIdsRef.current = new Map();
|
|
221
222
|
setMessages((body.session.messages ?? [])
|
|
222
223
|
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
223
|
-
.map((message) =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
.map((message) => {
|
|
225
|
+
const content = parseAssistantContent(message.content);
|
|
226
|
+
return {
|
|
227
|
+
id: message.id,
|
|
228
|
+
role: message.role,
|
|
229
|
+
text: message.text,
|
|
230
|
+
createdAt: Date.parse(message.at) || Date.now(),
|
|
231
|
+
...(content && content.length > 0 ? { content } : {}),
|
|
232
|
+
};
|
|
233
|
+
}));
|
|
229
234
|
setStreamingAssistantId(null);
|
|
230
235
|
return true;
|
|
231
236
|
}
|
|
@@ -6,10 +6,24 @@ import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
|
|
|
6
6
|
* IME composition guards, auto-grow and server-side draft sync. */
|
|
7
7
|
export function useComposer(params) {
|
|
8
8
|
const { origin, sessionId, refs, setError } = params;
|
|
9
|
-
const [input,
|
|
9
|
+
const [input, setInputState] = useState("");
|
|
10
10
|
const [fileOptions, setFileOptions] = useState([]);
|
|
11
11
|
const [pendingImages, setPendingImages] = useState([]);
|
|
12
12
|
const [shortcutOpen, setShortcutOpen] = useState(false);
|
|
13
|
+
/** Any composer text that starts with `/` should reveal the palette,
|
|
14
|
+
* including draft restore. Escape can still close it without clearing. */
|
|
15
|
+
const setInput = useCallback((value) => {
|
|
16
|
+
if (typeof value === "function") {
|
|
17
|
+
setInputState((prev) => {
|
|
18
|
+
const next = value(prev);
|
|
19
|
+
setShortcutOpen(next.startsWith("/"));
|
|
20
|
+
return next;
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setInputState(value);
|
|
25
|
+
setShortcutOpen(value.startsWith("/"));
|
|
26
|
+
}, []);
|
|
13
27
|
const [shortcutIndex, setShortcutIndex] = useState(0);
|
|
14
28
|
const [dynamicCommands, setDynamicCommands] = useState([]);
|
|
15
29
|
const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
|
|
@@ -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 {
|
|
@@ -359,11 +359,16 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
359
359
|
if (status === "no-evidence") {
|
|
360
360
|
summaryLines.push("未观察到任何规范证据:无 skill 注入、无文件读取、无检索操作。");
|
|
361
361
|
}
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// separately from the generic spec reads/searches.
|
|
362
|
+
// Separate openspec spec directory reads and searches for Dashboard display.
|
|
363
|
+
// Knowledge base and openspec are parallel sources.
|
|
365
364
|
const openspecReads = specReads.filter((r) => isOpenspecPath(r.path));
|
|
366
365
|
const openspecSearches = searches.filter((s) => isOpenspecSearch(s.query, s.path));
|
|
366
|
+
if (openspecReads.length > 0) {
|
|
367
|
+
summaryLines.push(`openspec 已读取 ${openspecReads.length} 个文件:${openspecReads.map((r) => r.path).join("、")}`);
|
|
368
|
+
}
|
|
369
|
+
if (openspecSearches.length > 0) {
|
|
370
|
+
summaryLines.push(`openspec 检索 ${openspecSearches.length} 次。`);
|
|
371
|
+
}
|
|
367
372
|
return {
|
|
368
373
|
dagRunId,
|
|
369
374
|
nodeId,
|
|
@@ -360,9 +360,15 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
360
360
|
evidence.sourceBinding?.requirementIds,
|
|
361
361
|
(id) => el("li", null, id),
|
|
362
362
|
);
|
|
363
|
+
const openspecReadPaths = new Set(
|
|
364
|
+
(evidence.openspecReads ?? []).map((entry) => entry.path),
|
|
365
|
+
);
|
|
366
|
+
const otherSpecReads = (evidence.specReads ?? []).filter(
|
|
367
|
+
(entry) => !openspecReadPaths.has(entry.path),
|
|
368
|
+
);
|
|
363
369
|
appendListSection(
|
|
364
|
-
|
|
365
|
-
|
|
370
|
+
`其他已读取规范文件(${otherSpecReads.length})`,
|
|
371
|
+
otherSpecReads,
|
|
366
372
|
(read) => {
|
|
367
373
|
const item = el("li", null);
|
|
368
374
|
const button = document.createElement("button");
|
|
@@ -374,7 +380,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
374
380
|
button.setAttribute("data-read-at", read.timestamp);
|
|
375
381
|
}
|
|
376
382
|
button.id = `spec-evidence-btn-read-${read.path}`;
|
|
377
|
-
const icon = el("i", "ri-
|
|
383
|
+
const icon = el("i", "ri-file-text-line");
|
|
378
384
|
icon.setAttribute("aria-hidden", "true");
|
|
379
385
|
button.append(icon, el("code", null, read.path));
|
|
380
386
|
if (read.timestamp) {
|
|
@@ -399,10 +405,9 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
399
405
|
item.appendChild(button);
|
|
400
406
|
return item;
|
|
401
407
|
},
|
|
402
|
-
"unavailable:未观察到规范读取。",
|
|
403
408
|
);
|
|
404
409
|
appendListSection(
|
|
405
|
-
|
|
410
|
+
`检索操作(${evidence.specSearches?.length ?? 0})`,
|
|
406
411
|
evidence.specSearches,
|
|
407
412
|
(search) => {
|
|
408
413
|
const item = el("li", null);
|
|
@@ -414,7 +419,6 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
414
419
|
);
|
|
415
420
|
return item;
|
|
416
421
|
},
|
|
417
|
-
"unavailable:未观察到规范检索。",
|
|
418
422
|
);
|
|
419
423
|
appendListSection(
|
|
420
424
|
`知识库查询(${evidence.knowledgeBaseQueries?.length ?? 0})`,
|
|
@@ -429,6 +433,67 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
429
433
|
"unavailable:未观察到知识库查询;connector 可能未配置或未调用。",
|
|
430
434
|
);
|
|
431
435
|
|
|
436
|
+
// openspec reads are displayed independently from skill/docs reads.
|
|
437
|
+
appendListSection(
|
|
438
|
+
`openspec 已读取(${evidence.openspecReads?.length ?? 0})`,
|
|
439
|
+
evidence.openspecReads,
|
|
440
|
+
(read) => {
|
|
441
|
+
const item = el("li", null);
|
|
442
|
+
const button = document.createElement("button");
|
|
443
|
+
button.type = "button";
|
|
444
|
+
button.className = "spec-evidence-file-btn";
|
|
445
|
+
button.setAttribute("data-source", "read");
|
|
446
|
+
button.setAttribute("data-path", read.path);
|
|
447
|
+
if (read.timestamp) {
|
|
448
|
+
button.setAttribute("data-read-at", read.timestamp);
|
|
449
|
+
}
|
|
450
|
+
button.id = `spec-evidence-btn-read-${read.path}`;
|
|
451
|
+
const icon = el("i", "ri-book-open-line");
|
|
452
|
+
icon.setAttribute("aria-hidden", "true");
|
|
453
|
+
button.append(icon, el("code", null, read.path));
|
|
454
|
+
if (read.timestamp) {
|
|
455
|
+
button.appendChild(
|
|
456
|
+
el(
|
|
457
|
+
"span",
|
|
458
|
+
"spec-evidence-time",
|
|
459
|
+
formatSessionEventTime({ timestamp: read.timestamp }),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
button.addEventListener("click", () =>
|
|
464
|
+
onSpecEvidenceFileClick(
|
|
465
|
+
content,
|
|
466
|
+
dagRunId,
|
|
467
|
+
nodeId,
|
|
468
|
+
"read",
|
|
469
|
+
read.path,
|
|
470
|
+
button,
|
|
471
|
+
),
|
|
472
|
+
);
|
|
473
|
+
item.appendChild(button);
|
|
474
|
+
return item;
|
|
475
|
+
},
|
|
476
|
+
evidence.openspecSearches?.length
|
|
477
|
+
? "已检索但未观察到 openspec 成功读取;若生成期存在候选,预写门禁会阻断 writer。"
|
|
478
|
+
: "unavailable:未观察到 openspec 成功读取。",
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
appendListSection(
|
|
482
|
+
`openspec 检索(${evidence.openspecSearches?.length ?? 0})`,
|
|
483
|
+
evidence.openspecSearches,
|
|
484
|
+
(search) => {
|
|
485
|
+
const item = el("li", null);
|
|
486
|
+
const icon = el("i", "ri-search-line");
|
|
487
|
+
icon.setAttribute("aria-hidden", "true");
|
|
488
|
+
item.append(
|
|
489
|
+
icon,
|
|
490
|
+
document.createTextNode(` ${search.tool}: ${search.query}`),
|
|
491
|
+
);
|
|
492
|
+
return item;
|
|
493
|
+
},
|
|
494
|
+
"unavailable:未观察到 openspec 检索。",
|
|
495
|
+
);
|
|
496
|
+
|
|
432
497
|
if (
|
|
433
498
|
evidence.status === "source-bound" ||
|
|
434
499
|
evidence.status === "spec-injected" ||
|
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) {
|