@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.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.
Files changed (118) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +72 -163
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/infrastructure/harness/atomic-write.js +1 -2
  18. package/dist/shared/operator/capabilities.js +13 -200
  19. package/dist/shared/package-metadata.js +0 -42
  20. package/dist/task/frontend-project-capability.js +12 -162
  21. package/dist/task/source-prepare/completeness.js +0 -17
  22. package/dist/task/source-prepare/parse-intent.js +1 -15
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -61
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -86
  29. package/dist/worker/console/chat/routes.js +8 -36
  30. package/dist/worker/console/chat/session-store.js +0 -3
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -97
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +13 -373
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +1 -6
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/failure-routing.js +4 -9
  70. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  71. package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
  72. package/dist/workflows/dag/frontend-repair.js +18 -219
  73. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  74. package/dist/workflows/dag/init-hybrid.js +86 -238
  75. package/dist/workflows/dag/lifecycle.js +0 -4
  76. package/dist/workflows/dag/node-execution.js +6 -105
  77. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  78. package/dist/workflows/dag/report.js +0 -6
  79. package/dist/workflows/dag/retry-policy.js +0 -11
  80. package/dist/workflows/dag/runner.js +12 -314
  81. package/dist/workflows/dag/scheduler.js +3 -257
  82. package/dist/workflows/dag/types.js +9 -132
  83. package/dist/workflows/dag/validate.js +2 -7
  84. package/docs/README.md +3 -3
  85. package/docs/architecture/evolution.md +67 -102
  86. package/docs/templates/backend-test-dag.json +50 -32
  87. package/docs/templates/frontend-design-contract.md +14 -34
  88. package/docs/templates/frontend-task-constraints.md +13 -33
  89. package/docs/templates/frontend-task-requirement.md +20 -54
  90. package/docs/templates/init-managed-agents.md +2 -5
  91. package/harness.json +2 -2
  92. package/package.json +3 -4
  93. package/skills/loop-agent/SKILL.md +0 -1
  94. package/skills/loop-agent/references/command-reference.md +0 -3
  95. package/dist/build-stamp.json +0 -6
  96. package/dist/commands/dag-request-interrupt.js +0 -20
  97. package/dist/worker/console/chat/assistant-content.js +0 -121
  98. package/dist/worker/console/chat/semantic-activity.js +0 -471
  99. package/dist/worker/console/operation-run-facts.js +0 -190
  100. package/dist/worker/console/operation-wait.js +0 -355
  101. package/dist/worker/console/recovery-error-copy.js +0 -198
  102. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  103. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
  104. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  105. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  106. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  107. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  108. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  109. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  110. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  111. package/dist/workflows/dag/contract-output-registry.js +0 -14
  112. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  113. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  114. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  115. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  116. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  117. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  118. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Mutual-exclusion state for large spatial overlays in Operator Chat.
3
- * At most one of: sidebar drawer, process, runtime context, tools, activity.
3
+ * At most one of: sidebar drawer, process, runtime context, tools.
4
4
  * Small composer menus (model/thinking) are out of scope.
5
5
  */
6
6
  export function createSpatialOverlayState(active = "none") {
@@ -33,6 +33,5 @@ export function spatialOverlayFlags(state) {
33
33
  processOpen: state.active === "process",
34
34
  runtimeContextOpen: state.active === "runtime-context",
35
35
  toolsOpen: state.active === "tools",
36
- activityOpen: state.active === "activity",
37
36
  };
38
37
  }
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { parseAssistantContent } from "../../chat/assistant-content.js";
3
2
  import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
4
3
  import { hydrateSessionThread } from "../../chat/turn-process.js";
5
4
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
@@ -90,7 +89,6 @@ export function useChatSessions(params) {
90
89
  refs.lastEventIdRef.current = null;
91
90
  refs.seenEventIdsRef.current = new Set();
92
91
  refs.turnAssistantIdsRef.current = new Map();
93
- refs.sealedAssistantIdsRef.current = new Set();
94
92
  refs.settledTurnIdsRef.current = new Set();
95
93
  refs.activeTurnIdRef.current = null;
96
94
  resetForNewSession();
@@ -146,6 +144,8 @@ export function useChatSessions(params) {
146
144
  }
147
145
  }, [origin, refs, resetActiveProjection, setSession]);
148
146
  const deleteSession = useCallback(async (sessionId) => {
147
+ if (!window.confirm("确定删除该会话?删除后不可恢复。"))
148
+ return;
149
149
  try {
150
150
  const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`, {
151
151
  method: "DELETE",
@@ -156,7 +156,7 @@ export function useChatSessions(params) {
156
156
  });
157
157
  if (!res.ok) {
158
158
  setError("删除会话失败");
159
- return false;
159
+ return;
160
160
  }
161
161
  titleWatcherRef.current?.cancel(sessionId);
162
162
  if (refs.sessionRef.current?.sessionId === sessionId) {
@@ -167,11 +167,9 @@ export function useChatSessions(params) {
167
167
  window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
168
168
  }
169
169
  await refreshSessionList();
170
- return true;
171
170
  }
172
171
  catch (error) {
173
172
  setError(error instanceof Error ? error.message : String(error));
174
- return false;
175
173
  }
176
174
  }, [
177
175
  origin,
@@ -200,7 +198,6 @@ export function useChatSessions(params) {
200
198
  refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
201
199
  refs.seenEventIdsRef.current = new Set();
202
200
  refs.turnAssistantIdsRef.current = new Map();
203
- refs.sealedAssistantIdsRef.current = new Set();
204
201
  refs.settledTurnIdsRef.current = new Set();
205
202
  refs.lastEventIdRef.current = null;
206
203
  setSession(next);
@@ -298,23 +295,18 @@ export function useChatSessions(params) {
298
295
  refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
299
296
  refs.seenEventIdsRef.current = new Set();
300
297
  refs.turnAssistantIdsRef.current = new Map();
301
- refs.sealedAssistantIdsRef.current = new Set();
302
298
  refs.settledTurnIdsRef.current = new Set();
303
299
  refs.lastEventIdRef.current = null;
304
300
  setSession(restored);
305
301
  setInput(body.session.composerDraft ?? "");
306
302
  setMessages((body.session.messages ?? [])
307
303
  .filter((message) => message.role === "user" || message.role === "assistant")
308
- .map((message) => {
309
- const content = parseAssistantContent(message.content);
310
- return {
311
- id: message.id,
312
- role: message.role,
313
- text: message.text,
314
- createdAt: Date.parse(message.at) || Date.now(),
315
- ...(content && content.length > 0 ? { content } : {}),
316
- };
317
- }));
304
+ .map((message) => ({
305
+ id: message.id,
306
+ role: message.role,
307
+ text: message.text,
308
+ createdAt: Date.parse(message.at) || Date.now(),
309
+ })));
318
310
  }
319
311
  catch {
320
312
  // Recovery is best-effort; a new session remains available.
@@ -372,26 +364,15 @@ export function useChatSessions(params) {
372
364
  createSessionAndPrimeTitleWatcherForHook,
373
365
  ]);
374
366
  // Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
375
- /** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
376
- const compactSession = useCallback(async (instructions) => {
367
+ const compactSession = useCallback(async () => {
377
368
  const current = refs.sessionRef.current;
378
369
  if (!current || refs.streamingRef.current)
379
370
  return;
380
- const trimmed = typeof instructions === "string" ? instructions.trim() : "";
381
371
  try {
382
- const headers = {
383
- "x-loop-console-confirmation": confirmationToken(),
384
- };
385
- if (trimmed) {
386
- headers["content-type"] = "application/json";
387
- }
388
372
  const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/compact`, {
389
373
  method: "POST",
390
374
  credentials: "include",
391
- headers,
392
- ...(trimmed
393
- ? { body: JSON.stringify({ instructions: trimmed }) }
394
- : {}),
375
+ headers: { "x-loop-console-confirmation": confirmationToken() },
395
376
  });
396
377
  const body = (await response.json().catch(() => ({})));
397
378
  if (!response.ok) {
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { parseAssistantContent } from "../../chat/assistant-content.js";
3
2
  import { buildTurnProcessGroups } from "../../chat/turn-process.js";
4
3
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
5
4
  import { taskContextUnchanged } from "./format.js";
@@ -219,19 +218,14 @@ export function useChatThread(params) {
219
218
  refs.lastEventIdRef.current = body.lastEventId ?? null;
220
219
  refs.seenEventIdsRef.current = new Set();
221
220
  refs.turnAssistantIdsRef.current = new Map();
222
- refs.sealedAssistantIdsRef.current = new Set();
223
221
  setMessages((body.session.messages ?? [])
224
222
  .filter((message) => message.role === "user" || message.role === "assistant")
225
- .map((message) => {
226
- const content = parseAssistantContent(message.content);
227
- return {
228
- id: message.id,
229
- role: message.role,
230
- text: message.text,
231
- createdAt: Date.parse(message.at) || Date.now(),
232
- ...(content && content.length > 0 ? { content } : {}),
233
- };
234
- }));
223
+ .map((message) => ({
224
+ id: message.id,
225
+ role: message.role,
226
+ text: message.text,
227
+ createdAt: Date.parse(message.at) || Date.now(),
228
+ })));
235
229
  setStreamingAssistantId(null);
236
230
  return true;
237
231
  }
@@ -1,52 +1,27 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
2
2
  import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
3
3
  import { confirmationToken } from "./format.js";
4
- import { clampShortcutIndex } from "./slash-palette-nav.js";
5
4
  import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
6
5
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
7
6
  * IME composition guards, auto-grow and server-side draft sync. */
8
7
  export function useComposer(params) {
9
8
  const { origin, sessionId, refs, setError } = params;
10
- const [input, setInputState] = useState("");
9
+ const [input, setInput] = useState("");
11
10
  const [fileOptions, setFileOptions] = useState([]);
12
11
  const [pendingImages, setPendingImages] = useState([]);
13
12
  const [shortcutOpen, setShortcutOpen] = useState(false);
14
- /** Any composer text that starts with `/` should reveal the palette,
15
- * including draft restore. Escape can still close it without clearing. */
16
- const setInput = useCallback((value) => {
17
- if (typeof value === "function") {
18
- setInputState((prev) => {
19
- const next = value(prev);
20
- setShortcutOpen(next.startsWith("/"));
21
- return next;
22
- });
23
- return;
24
- }
25
- setInputState(value);
26
- setShortcutOpen(value.startsWith("/"));
27
- }, []);
28
13
  const [shortcutIndex, setShortcutIndex] = useState(0);
29
14
  const [dynamicCommands, setDynamicCommands] = useState([]);
30
15
  const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
31
16
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
32
17
  const [commandsEpoch, setCommandsEpoch] = useState(0);
33
18
  const textareaRef = useRef(null);
34
- /** Active session generation for /commands; bumped immediately on switch (AC-2). */
35
- const commandsSessionRef = useRef(sessionId);
36
- const commandsGenerationRef = useRef(0);
19
+ const commandsSessionRef = useRef(undefined);
37
20
  const handleInputChange = useCallback((value) => {
38
21
  setInput(value);
39
22
  setShortcutOpen(value.startsWith("/"));
40
23
  setShortcutIndex(0);
41
24
  }, []);
42
- // Immediate invalidation on A→B even when B palette never opens (AC-2).
43
- useLayoutEffect(() => {
44
- commandsSessionRef.current = sessionId;
45
- commandsGenerationRef.current += 1;
46
- setDynamicCommands([]);
47
- setDynamicCommandsError(null);
48
- setDynamicCommandsLoading(false);
49
- }, [sessionId]);
50
25
  const loadDynamicCommands = useCallback(async () => {
51
26
  if (!sessionId) {
52
27
  setDynamicCommands([]);
@@ -55,15 +30,12 @@ export function useComposer(params) {
55
30
  return;
56
31
  }
57
32
  const target = sessionId;
58
- const generation = commandsGenerationRef.current;
59
33
  commandsSessionRef.current = target;
60
34
  setDynamicCommandsLoading(true);
61
35
  setDynamicCommandsError(null);
62
- const isCurrent = () => commandsSessionRef.current === target &&
63
- commandsGenerationRef.current === generation;
64
36
  try {
65
37
  const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(target)}/commands`, { credentials: "include" });
66
- if (!isCurrent())
38
+ if (commandsSessionRef.current !== target)
67
39
  return;
68
40
  if (!res.ok) {
69
41
  setDynamicCommandsError("动态命令加载失败");
@@ -89,27 +61,26 @@ export function useComposer(params) {
89
61
  source,
90
62
  });
91
63
  }
92
- if (!isCurrent())
64
+ if (commandsSessionRef.current !== target)
93
65
  return;
94
66
  setDynamicCommands(items);
95
- // Compact server fail-soft / partial-source warning; local Operator/Web still merge.
96
- const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
97
- setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
98
67
  }
99
68
  catch {
100
- if (!isCurrent())
69
+ if (commandsSessionRef.current !== target)
101
70
  return;
102
71
  setDynamicCommandsError("动态命令加载失败");
103
72
  setDynamicCommands([]);
104
73
  }
105
74
  finally {
106
- if (isCurrent()) {
75
+ if (commandsSessionRef.current === target) {
107
76
  setDynamicCommandsLoading(false);
108
77
  }
109
78
  }
110
79
  }, [origin, sessionId]);
111
80
  // Lazy-load dynamic commands when slash palette opens or session changes.
112
81
  useEffect(() => {
82
+ setDynamicCommands([]);
83
+ setDynamicCommandsError(null);
113
84
  if (!sessionId)
114
85
  return;
115
86
  if (!shortcutOpen && commandsEpoch === 0)
@@ -201,14 +172,6 @@ export function useComposer(params) {
201
172
  const filteredShortcuts = useMemo(() => input.startsWith("/")
202
173
  ? filterSlashCommands(mergedCommands, input)
203
174
  : [], [input, mergedCommands]);
204
- // Keep the active option index inside the current candidate range (UI-12
205
- // AC-011): dynamic loads completing/failing empty, retries, session
206
- // switches, late responses and search shrink all funnel through
207
- // filteredShortcuts.length. The clamped index always matches a rendered
208
- // `oc-shortcut-option-${globalIndex}` id (same source of truth).
209
- useEffect(() => {
210
- setShortcutIndex((index) => clampShortcutIndex(index, filteredShortcuts.length));
211
- }, [filteredShortcuts.length]);
212
175
  const onCompositionStart = useCallback(() => {
213
176
  refs.isComposingRef.current = true;
214
177
  }, [refs]);
@@ -17,13 +17,9 @@ export function resolveWorkspaceLayoutMode(widthPx) {
17
17
  return "compact";
18
18
  return "narrow";
19
19
  }
20
- /**
21
- * Tool-call history always uses the original right overlay panel.
22
- * Reflowing it as a comfortable-mode third column moves ChatMain and also
23
- * invalidates Activity Rail's fixed ChatMain anchor.
24
- */
25
- export function processUsesOverlay(_mode) {
26
- return true;
20
+ /** Whether process should render as a right overlay (no main reflow). */
21
+ export function processUsesOverlay(mode) {
22
+ return mode === "compact" || mode === "narrow";
27
23
  }
28
24
  /** Whether the permanent full sidebar is allowed. */
29
25
  export function fullSidebarAllowed(mode) {
@@ -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, normalizeCliVersionLabel, readPackageName, readPackageVersion, resolveBinPaths, resolveCmdShim, } from "../../shared/package-metadata.js";
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
- const normalized = normalizeCliVersionLabel(reportedVersion);
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: normalized };
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
- // 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.
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,
@@ -49,7 +49,6 @@ export function parseInspectObjectView(
49
49
  export function activeInspectTabHref(hashValue: string): string | null;
50
50
  export function parseInspectContext(hashValue: string): InspectContext | null;
51
51
  export function resolveOperateHrefFromInspectHash(hashValue: string): string;
52
- export function buildOperateInterruptHref(dagRunId: string): string;
53
52
  export function buildInspectDagHref(
54
53
  dagRunId: string,
55
54
  options?: { from?: string },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Operator Chrome — 统一壳(Operate React 与 Inspect vanilla 共用唯一实现)。
3
3
  *
4
- * 设计真源:docs/design/archive/2026-08-09-unified-operator-chrome.md
4
+ * 设计真源:docs/design/active/2026-08-09-unified-operator-chrome.md
5
5
  * - 一条 chrome 两条带:Band1 全局(品牌/仓库身份/模式分段控件/对象上下文/状态格),
6
6
  * Band2 工作区(下划线 tab,随模式切换内容)。
7
7
  * - 模式语义:操作=做,观测=看同一对象;模式切换在持有 DAG 选中时不丢对象。
@@ -118,11 +118,6 @@ export function resolveOperateHrefFromInspectHash(hashValue) {
118
118
  return "/#/";
119
119
  }
120
120
 
121
- /** Inspect DAG 详情 → Operate Recovery 协作中止深链(只改导航,不发 mutation)。 */
122
- export function buildOperateInterruptHref(dagRunId) {
123
- return `/#/recovery?dagRunId=${encodeURIComponent(dagRunId)}&intent=interrupt`;
124
- }
125
-
126
121
  /** 操作侧 → Inspect 的 canonical 深链(可携带 from 以激活上下文 chip)。 */
127
122
  export function buildInspectDagHref(dagRunId, options = {}) {
128
123
  const base = `/inspect/#/dag/${encodeURIComponent(dagRunId)}`;
@@ -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
- `规范已读取(${evidence.specReads?.length ?? 0})`,
365
- evidence.specReads,
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-book-open-line");
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
- `规范检索(${evidence.specSearches?.length ?? 0})`,
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" ||
@@ -355,18 +355,6 @@ export async function renderDagDetail(dagRunId, initial = true) {
355
355
  primaryEntries.push(["状态一致性", "不一致,需要处理"]);
356
356
  if (visibility.showResume) primaryEntries.push(["允许恢复", "是"]);
357
357
  if (visibility.showReconcile) primaryEntries.push(["允许收口", "是"]);
358
- const liveStatus = String(dag.effectiveStatus ?? dag.status ?? "").toLowerCase();
359
- if (liveStatus === "running" || liveStatus === "active") {
360
- const interruptLink = el(
361
- "a",
362
- "page-action-btn",
363
- "在操作台中止此运行 →",
364
- );
365
- interruptLink.href = `/#/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`;
366
- interruptLink.setAttribute("data-testid", "inspect-dag-interrupt-link");
367
- interruptLink.title = "只跳转到操作台 Recovery,不会在观测页执行中止";
368
- primaryEntries.push(["协作中止", interruptLink]);
369
- }
370
358
  primaryEntries.push(
371
359
  ["开始时间", formatTs(dag.startedAt)],
372
360
  ["结束时间", formatTs(dag.finishedAt)],
@@ -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 = normalizeCliVersionLabel(version.stdout);
50
+ const reportedVersion = version.stdout.trim();
52
51
  let controllerIdentity = identity
53
52
  ? { ...identity, reportedVersion }
54
53
  : undefined;