@tea-agent/loop-agent 0.35.2 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/bin/loop-agent.js +37 -1
- package/dist/build-stamp.json +6 -0
- package/dist/cli/program.js +2 -2
- package/dist/executors/dag-pi-executor.js +44 -0
- package/dist/shared/package-metadata.js +42 -0
- package/dist/worker/console/chat/assistant-content.js +11 -0
- package/dist/worker/console/chat/pi-runtime.js +6 -2
- package/dist/worker/console/chat/turn-process.js +17 -9
- package/dist/worker/console/chat/workspace-landing.js +1 -1
- package/dist/worker/console/static/assets/index-DuVLjCIT.js +57 -0
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +15 -3
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +1 -0
- package/dist/worker/loop-agent/loop-agent-client.js +17 -3
- package/dist/worker/observability/read-model.js +20 -0
- package/dist/worker/observe/spec-evidence.js +3 -8
- package/dist/worker/observe/static/views/dag-inspector.js +6 -71
- package/dist/worker/preflight.js +2 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +33 -23
- package/dist/workflows/dag/contract-output-registry.js +14 -0
- package/dist/workflows/dag/contract-validator-registrations.js +8 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +9 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +233 -39
- package/dist/workflows/dag/frontend-prewrite-gate.js +364 -61
- package/dist/workflows/dag/frontend-recovery-plan.js +73 -0
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +123 -0
- package/dist/workflows/dag/frontend-recovery-run.js +539 -0
- package/dist/workflows/dag/frontend-repair.js +219 -18
- package/dist/workflows/dag/frontend-verification-trace.js +47 -32
- package/dist/workflows/dag/frontend-writer-recovery.js +106 -0
- package/dist/workflows/dag/frontend-writer-rollback.js +821 -0
- package/dist/workflows/dag/init-hybrid.js +49 -24
- package/dist/workflows/dag/node-execution.js +89 -0
- package/dist/workflows/dag/recovery-recommendation.js +58 -0
- package/dist/workflows/dag/runner.js +245 -11
- package/dist/workflows/dag/scheduler.js +257 -3
- package/dist/workflows/dag/types.js +130 -2
- package/package.json +4 -3
- package/dist/worker/console/static/assets/index-gVHrlqI9.js +0 -56
|
@@ -6,7 +6,7 @@
|
|
|
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-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DuVLjCIT.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-DRqZiQ7J.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -22,6 +22,7 @@ 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);
|
|
25
26
|
const [manualRunId, setManualRunId] = useState(false);
|
|
26
27
|
const [manualNodeId, setManualNodeId] = useState(false);
|
|
27
28
|
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
@@ -124,6 +125,7 @@ export function useRecoveryConsole(params) {
|
|
|
124
125
|
setRecoveryNodes([]);
|
|
125
126
|
setRecoveryRunStatus(null);
|
|
126
127
|
setRecoveryFailureCategory(null);
|
|
128
|
+
setFrontendRecovery(null);
|
|
127
129
|
return;
|
|
128
130
|
}
|
|
129
131
|
setRecoveryNodesLoading(true);
|
|
@@ -167,11 +169,13 @@ export function useRecoveryConsole(params) {
|
|
|
167
169
|
n.failureCategory &&
|
|
168
170
|
n.failureCategory !== "success")?.failureCategory;
|
|
169
171
|
setRecoveryFailureCategory(runFailure ?? nodeFailure ?? null);
|
|
172
|
+
setFrontendRecovery(body.frontendRecovery ?? null);
|
|
170
173
|
}
|
|
171
174
|
catch {
|
|
172
175
|
setRecoveryNodes([]);
|
|
173
176
|
setRecoveryRunStatus(null);
|
|
174
177
|
setRecoveryFailureCategory(null);
|
|
178
|
+
setFrontendRecovery(null);
|
|
175
179
|
}
|
|
176
180
|
finally {
|
|
177
181
|
setRecoveryNodesLoading(false);
|
|
@@ -323,6 +327,7 @@ export function useRecoveryConsole(params) {
|
|
|
323
327
|
recoveryRunsLoading,
|
|
324
328
|
recoveryNodesLoading,
|
|
325
329
|
recoveryRunStatus,
|
|
330
|
+
frontendRecovery,
|
|
326
331
|
manualRunId,
|
|
327
332
|
setManualRunId,
|
|
328
333
|
manualNodeId,
|
|
@@ -214,12 +214,22 @@ export function createChatSseEventApplier(deps) {
|
|
|
214
214
|
// a duplicate bubble.
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
|
+
const sealedIds = refs.sealedAssistantIdsRef.current;
|
|
218
|
+
if (eventName === "message_update" &&
|
|
219
|
+
sealedIds.has(resolvedAssistantId)) {
|
|
220
|
+
const nextId = requestId("a");
|
|
221
|
+
if (turnId)
|
|
222
|
+
refs.turnAssistantIdsRef.current.set(turnId, nextId);
|
|
223
|
+
resolvedAssistantId = nextId;
|
|
224
|
+
setStreamingAssistantId(nextId);
|
|
225
|
+
}
|
|
217
226
|
const text = typeof payload.text === "string" ? payload.text : "";
|
|
218
227
|
const content = parseAssistantContent(payload.content);
|
|
228
|
+
const assistantIdForEvent = resolvedAssistantId;
|
|
219
229
|
setMessages((m) => {
|
|
220
|
-
const exists = m.some((msg) => msg.id ===
|
|
230
|
+
const exists = m.some((msg) => msg.id === assistantIdForEvent);
|
|
221
231
|
if (exists) {
|
|
222
|
-
return m.map((msg) => msg.id ===
|
|
232
|
+
return m.map((msg) => msg.id === assistantIdForEvent
|
|
223
233
|
? {
|
|
224
234
|
...msg,
|
|
225
235
|
text,
|
|
@@ -232,7 +242,7 @@ export function createChatSseEventApplier(deps) {
|
|
|
232
242
|
return [
|
|
233
243
|
...m,
|
|
234
244
|
{
|
|
235
|
-
id:
|
|
245
|
+
id: assistantIdForEvent,
|
|
236
246
|
role: "assistant",
|
|
237
247
|
text,
|
|
238
248
|
createdAt: Date.now(),
|
|
@@ -240,6 +250,8 @@ export function createChatSseEventApplier(deps) {
|
|
|
240
250
|
},
|
|
241
251
|
];
|
|
242
252
|
});
|
|
253
|
+
if (eventName === "message_end")
|
|
254
|
+
sealedIds.add(assistantIdForEvent);
|
|
243
255
|
}
|
|
244
256
|
else if (eventName === "tool_call") {
|
|
245
257
|
const id = typeof payload.toolCallId === "string"
|
|
@@ -12,6 +12,7 @@ export function useChatRefs() {
|
|
|
12
12
|
const settledTurnIdsRef = useRef(new Set());
|
|
13
13
|
const seenEventIdsRef = useRef(new Set());
|
|
14
14
|
const turnAssistantIdsRef = useRef(new Map());
|
|
15
|
+
const sealedAssistantIdsRef = useRef(new Set());
|
|
15
16
|
const reconnectAbortRef = useRef(null);
|
|
16
17
|
const activeTurnIdRef = useRef(null);
|
|
17
18
|
const submitInFlightRef = useRef(false);
|
|
@@ -40,6 +41,7 @@ export function useChatRefs() {
|
|
|
40
41
|
settledTurnIdsRef,
|
|
41
42
|
seenEventIdsRef,
|
|
42
43
|
turnAssistantIdsRef,
|
|
44
|
+
sealedAssistantIdsRef,
|
|
43
45
|
reconnectAbortRef,
|
|
44
46
|
activeTurnIdRef,
|
|
45
47
|
submitInFlightRef,
|
|
@@ -66,6 +68,7 @@ export function useChatRefs() {
|
|
|
66
68
|
settledTurnIdsRef,
|
|
67
69
|
seenEventIdsRef,
|
|
68
70
|
turnAssistantIdsRef,
|
|
71
|
+
sealedAssistantIdsRef,
|
|
69
72
|
reconnectAbortRef,
|
|
70
73
|
activeTurnIdRef,
|
|
71
74
|
submitInFlightRef,
|
|
@@ -90,6 +90,7 @@ export function useChatSessions(params) {
|
|
|
90
90
|
refs.lastEventIdRef.current = null;
|
|
91
91
|
refs.seenEventIdsRef.current = new Set();
|
|
92
92
|
refs.turnAssistantIdsRef.current = new Map();
|
|
93
|
+
refs.sealedAssistantIdsRef.current = new Set();
|
|
93
94
|
refs.settledTurnIdsRef.current = new Set();
|
|
94
95
|
refs.activeTurnIdRef.current = null;
|
|
95
96
|
resetForNewSession();
|
|
@@ -199,6 +200,7 @@ export function useChatSessions(params) {
|
|
|
199
200
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
200
201
|
refs.seenEventIdsRef.current = new Set();
|
|
201
202
|
refs.turnAssistantIdsRef.current = new Map();
|
|
203
|
+
refs.sealedAssistantIdsRef.current = new Set();
|
|
202
204
|
refs.settledTurnIdsRef.current = new Set();
|
|
203
205
|
refs.lastEventIdRef.current = null;
|
|
204
206
|
setSession(next);
|
|
@@ -296,6 +298,7 @@ export function useChatSessions(params) {
|
|
|
296
298
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
297
299
|
refs.seenEventIdsRef.current = new Set();
|
|
298
300
|
refs.turnAssistantIdsRef.current = new Map();
|
|
301
|
+
refs.sealedAssistantIdsRef.current = new Set();
|
|
299
302
|
refs.settledTurnIdsRef.current = new Set();
|
|
300
303
|
refs.lastEventIdRef.current = null;
|
|
301
304
|
setSession(restored);
|
|
@@ -219,6 +219,7 @@ export function useChatThread(params) {
|
|
|
219
219
|
refs.lastEventIdRef.current = body.lastEventId ?? null;
|
|
220
220
|
refs.seenEventIdsRef.current = new Set();
|
|
221
221
|
refs.turnAssistantIdsRef.current = new Map();
|
|
222
|
+
refs.sealedAssistantIdsRef.current = new Set();
|
|
222
223
|
setMessages((body.session.messages ?? [])
|
|
223
224
|
.filter((message) => message.role === "user" || message.role === "assistant")
|
|
224
225
|
.map((message) => {
|
|
@@ -2,11 +2,15 @@ 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, readPackageName, readPackageVersion, resolveBinPaths, resolveCmdShim, } from "../../shared/package-metadata.js";
|
|
5
|
+
import { computeBinarySha256, computePackageFingerprint, findPackageRoot, isJavaScriptScript, normalizeCliVersionLabel, 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;
|
|
10
14
|
const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
|
|
11
15
|
/**
|
|
12
16
|
* Bind evidence supplied by an orchestration layer to the identity exposed by
|
|
@@ -178,10 +182,11 @@ export class LoopAgentClient {
|
|
|
178
182
|
if (!this._identity) {
|
|
179
183
|
throw new Error("controller identity: cannot record a reported version without a pinned identity");
|
|
180
184
|
}
|
|
181
|
-
|
|
185
|
+
const normalized = normalizeCliVersionLabel(reportedVersion);
|
|
186
|
+
if (normalized !== this._identity.packageVersion) {
|
|
182
187
|
throw new Error(`controller identity: reported version ${reportedVersion} does not match package version ${this._identity.packageVersion}`);
|
|
183
188
|
}
|
|
184
|
-
this._identity = { ...this._identity, reportedVersion };
|
|
189
|
+
this._identity = { ...this._identity, reportedVersion: normalized };
|
|
185
190
|
return this._identity;
|
|
186
191
|
}
|
|
187
192
|
async run(args, options) {
|
|
@@ -381,10 +386,17 @@ function spawnCommand(input) {
|
|
|
381
386
|
});
|
|
382
387
|
};
|
|
383
388
|
let hardTimeout;
|
|
389
|
+
let killGraceTimeout;
|
|
384
390
|
if (input.hardTimeoutMs !== undefined && input.hardTimeoutMs > 0) {
|
|
385
391
|
hardTimeout = setTimeout(() => {
|
|
386
392
|
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.
|
|
387
396
|
child.kill("SIGTERM");
|
|
397
|
+
killGraceTimeout = setTimeout(() => {
|
|
398
|
+
child.kill("SIGKILL");
|
|
399
|
+
}, HARD_TIMEOUT_KILL_GRACE_MS);
|
|
388
400
|
}, input.hardTimeoutMs);
|
|
389
401
|
}
|
|
390
402
|
let heartbeatTimer;
|
|
@@ -394,6 +406,8 @@ function spawnCommand(input) {
|
|
|
394
406
|
const clearTimers = () => {
|
|
395
407
|
if (hardTimeout !== undefined)
|
|
396
408
|
clearTimeout(hardTimeout);
|
|
409
|
+
if (killGraceTimeout !== undefined)
|
|
410
|
+
clearTimeout(killGraceTimeout);
|
|
397
411
|
if (heartbeatTimer !== undefined) {
|
|
398
412
|
clearInterval(heartbeatTimer);
|
|
399
413
|
heartbeatTimer = undefined;
|
|
@@ -1491,6 +1491,7 @@ 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,
|
|
1494
1495
|
title: incoming.title ?? existing.title,
|
|
1495
1496
|
startedAt,
|
|
1496
1497
|
finishedAt,
|
|
@@ -1790,6 +1791,23 @@ async function loadFrontendLintProjection(runDir) {
|
|
|
1790
1791
|
blockingReasons: parsed.data.blockingReasons,
|
|
1791
1792
|
};
|
|
1792
1793
|
}
|
|
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
|
+
}
|
|
1793
1811
|
async function parseDagStateFile(statePath, now) {
|
|
1794
1812
|
try {
|
|
1795
1813
|
if (!existsSync(statePath))
|
|
@@ -1860,6 +1878,7 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1860
1878
|
loadBackendTestProjection(runDir, nodes),
|
|
1861
1879
|
loadFrontendLintProjection(runDir),
|
|
1862
1880
|
]);
|
|
1881
|
+
const frontendRecovery = projectFrontendRecovery(state);
|
|
1863
1882
|
const continuationRecord = state.continuation &&
|
|
1864
1883
|
typeof state.continuation.parentRunId === "string" &&
|
|
1865
1884
|
typeof state.continuation.effectiveFromNodeId === "string" &&
|
|
@@ -1918,6 +1937,7 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1918
1937
|
...(continuation ? { continuation } : {}),
|
|
1919
1938
|
...(backendTest ? { backendTest } : {}),
|
|
1920
1939
|
...(frontendLint ? { frontendLint } : {}),
|
|
1940
|
+
...(frontendRecovery ? { frontendRecovery } : {}),
|
|
1921
1941
|
};
|
|
1922
1942
|
}
|
|
1923
1943
|
catch {
|
|
@@ -359,16 +359,11 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
359
359
|
if (status === "no-evidence") {
|
|
360
360
|
summaryLines.push("未观察到任何规范证据:无 skill 注入、无文件读取、无检索操作。");
|
|
361
361
|
}
|
|
362
|
-
//
|
|
363
|
-
//
|
|
362
|
+
// Openspec directory reads/searches remain in the payload for API consumers
|
|
363
|
+
// (tests and downstream projections); the inspector no longer displays them
|
|
364
|
+
// separately from the generic spec reads/searches.
|
|
364
365
|
const openspecReads = specReads.filter((r) => isOpenspecPath(r.path));
|
|
365
366
|
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
|
-
}
|
|
372
367
|
return {
|
|
373
368
|
dagRunId,
|
|
374
369
|
nodeId,
|
|
@@ -360,15 +360,9 @@ 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
|
-
);
|
|
369
363
|
appendListSection(
|
|
370
|
-
|
|
371
|
-
|
|
364
|
+
`规范已读取(${evidence.specReads?.length ?? 0})`,
|
|
365
|
+
evidence.specReads,
|
|
372
366
|
(read) => {
|
|
373
367
|
const item = el("li", null);
|
|
374
368
|
const button = document.createElement("button");
|
|
@@ -380,7 +374,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
380
374
|
button.setAttribute("data-read-at", read.timestamp);
|
|
381
375
|
}
|
|
382
376
|
button.id = `spec-evidence-btn-read-${read.path}`;
|
|
383
|
-
const icon = el("i", "ri-
|
|
377
|
+
const icon = el("i", "ri-book-open-line");
|
|
384
378
|
icon.setAttribute("aria-hidden", "true");
|
|
385
379
|
button.append(icon, el("code", null, read.path));
|
|
386
380
|
if (read.timestamp) {
|
|
@@ -405,9 +399,10 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
405
399
|
item.appendChild(button);
|
|
406
400
|
return item;
|
|
407
401
|
},
|
|
402
|
+
"unavailable:未观察到规范读取。",
|
|
408
403
|
);
|
|
409
404
|
appendListSection(
|
|
410
|
-
|
|
405
|
+
`规范检索(${evidence.specSearches?.length ?? 0})`,
|
|
411
406
|
evidence.specSearches,
|
|
412
407
|
(search) => {
|
|
413
408
|
const item = el("li", null);
|
|
@@ -419,6 +414,7 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
419
414
|
);
|
|
420
415
|
return item;
|
|
421
416
|
},
|
|
417
|
+
"unavailable:未观察到规范检索。",
|
|
422
418
|
);
|
|
423
419
|
appendListSection(
|
|
424
420
|
`知识库查询(${evidence.knowledgeBaseQueries?.length ?? 0})`,
|
|
@@ -433,67 +429,6 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
433
429
|
"unavailable:未观察到知识库查询;connector 可能未配置或未调用。",
|
|
434
430
|
);
|
|
435
431
|
|
|
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
|
-
|
|
497
432
|
if (
|
|
498
433
|
evidence.status === "source-bound" ||
|
|
499
434
|
evidence.status === "spec-injected" ||
|
package/dist/worker/preflight.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { normalizeCliVersionLabel } from "../shared/package-metadata.js";
|
|
3
4
|
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "./loop-agent/loop-agent-client.js";
|
|
4
5
|
export const DEFAULT_CHECK_REPO_TIMEOUT_MS = 300_000;
|
|
5
6
|
export async function preflightTargetRepo(input) {
|
|
@@ -47,7 +48,7 @@ export async function preflightTargetRepo(input) {
|
|
|
47
48
|
controllerIdentity: identity,
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
|
-
const reportedVersion = version.stdout
|
|
51
|
+
const reportedVersion = normalizeCliVersionLabel(version.stdout);
|
|
51
52
|
let controllerIdentity = identity
|
|
52
53
|
? { ...identity, reportedVersion }
|
|
53
54
|
: undefined;
|
|
@@ -724,30 +724,40 @@ 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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
727
|
+
// Prefer `python3` (present on modern POSIX images and macOS); fall back to
|
|
728
|
+
// `python` for older or Windows layouts where only the unversioned shim exists.
|
|
729
|
+
// A missing interpreter must not silently roll back an otherwise-valid
|
|
730
|
+
// deterministic rewrite.
|
|
731
|
+
const candidates = ["python3", "python"];
|
|
732
|
+
let lastError;
|
|
733
|
+
for (const interpreter of candidates) {
|
|
734
|
+
const result = spawnSync(interpreter, ["-c", "import ast,sys; ast.parse(sys.stdin.read())"], {
|
|
735
|
+
input: source,
|
|
736
|
+
encoding: "utf8",
|
|
737
|
+
env: {
|
|
738
|
+
...process.env,
|
|
739
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
740
|
+
PYTHONUTF8: "1",
|
|
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" };
|
|
749
756
|
}
|
|
750
|
-
return {
|
|
757
|
+
return {
|
|
758
|
+
ok: false,
|
|
759
|
+
detail: `python syntax validation unavailable: ${lastError ?? "python/python3 not found"}`,
|
|
760
|
+
};
|
|
751
761
|
}
|
|
752
762
|
export async function applyDeterministicScenarioParamRepairs(input) {
|
|
753
763
|
const repairable = input.facts.entries.filter((entry) => entry.status === "MISMATCH" && entry.repairability === "repairable");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { structuredContractOutputSchemaIds, } from "./types.js";
|
|
2
|
+
const validators = new Map();
|
|
3
|
+
export function registerStructuredContractValidator(schemaId, validator) {
|
|
4
|
+
if (validators.has(schemaId)) {
|
|
5
|
+
throw new Error(`structured contract validator already registered for ${schemaId}`);
|
|
6
|
+
}
|
|
7
|
+
validators.set(schemaId, validator);
|
|
8
|
+
}
|
|
9
|
+
export function getStructuredContractValidator(schemaId) {
|
|
10
|
+
if (structuredContractOutputSchemaIds.includes(schemaId)) {
|
|
11
|
+
return validators.get(schemaId);
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect module: registers every structured contract validator against
|
|
3
|
+
* its schemaId in the contract output registry. Imported once by the node
|
|
4
|
+
* executor; keeps per-contract imports out of the executor.
|
|
5
|
+
*/
|
|
6
|
+
import { validateFrontendContractNodeOutput } from "./frontend-implementation-contract.js";
|
|
7
|
+
import { registerStructuredContractValidator } from "./contract-output-registry.js";
|
|
8
|
+
registerStructuredContractValidator("frontend-implementation-contract-v1", validateFrontendContractNodeOutput);
|
|
@@ -31,7 +31,15 @@ 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
|
-
|
|
34
|
+
const line = (emphasized ? emphasized[1] : trimmed).trim();
|
|
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;
|
|
35
43
|
}
|
|
36
44
|
/** First line matching /^VERDICT:/ from node assistantText/stdout (markdown emphasis OK). */
|
|
37
45
|
export function extractFirstVerdictLineFromNodeText(text) {
|