@tea-agent/loop-agent 0.34.4 → 0.35.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +2 -2
  4. package/dist/shared/operator/capabilities.js +0 -7
  5. package/dist/worker/cli.js +21 -27
  6. package/dist/worker/console/chat/chat-event-store.js +49 -1
  7. package/dist/worker/console/chat/pi-runtime.js +78 -1
  8. package/dist/worker/console/chat/routes.js +24 -1
  9. package/dist/worker/console/chat/shortcuts.js +208 -9
  10. package/dist/worker/console/chat/tool-preview.js +162 -5
  11. package/dist/worker/console/doctor.js +1 -1
  12. package/dist/worker/console/index.js +1 -0
  13. package/dist/worker/console/open-browser.js +134 -0
  14. package/dist/worker/console/recovery-cta.js +1 -1
  15. package/dist/worker/console/server.js +1 -1
  16. package/dist/worker/console/static/assets/index-qpkysQYW.css +1 -0
  17. package/dist/worker/console/static/assets/index-y980PqtP.js +56 -0
  18. package/dist/worker/console/static/index.html +2 -2
  19. package/dist/worker/console/static-src/chat-markdown-security.js +38 -0
  20. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +1 -3
  21. package/dist/worker/console/static-src/operator-chat/format.js +2 -2
  22. package/dist/worker/console/static-src/operator-chat/useChatThread.js +11 -5
  23. package/dist/worker/console/static-src/operator-chat/useComposer.js +81 -3
  24. package/dist/workflows/dag/frontend-implementation-contract.js +49 -0
  25. package/dist/workflows/dag/frontend-prewrite-gate.js +5 -0
  26. package/dist/workflows/dag/init-hybrid.js +62 -4
  27. package/docs/architecture/evolution.md +1 -1
  28. package/docs/architecture/system-overview.md +1 -1
  29. package/docs/architecture/worker-and-feature.md +1 -1
  30. package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
  31. package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
  32. package/docs/templates/init-managed-agents.md +2 -2
  33. package/package.json +4 -1
  34. package/skills/agent-worker/SKILL.md +1 -1
  35. package/skills/agent-worker/references/agent-worker-operator.md +2 -2
  36. package/skills/loop-agent/references/command-reference.md +2 -3
  37. package/dist/worker/console/static/assets/index-B6Qdbk8V.js +0 -29
  38. package/dist/worker/console/static/assets/index-Bt0NUxcQ.css +0 -1
@@ -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-B6Qdbk8V.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-Bt0NUxcQ.css">
9
+ <script type="module" crossorigin src="/assets/index-y980PqtP.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-qpkysQYW.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pure security helpers for Operator Chat Markdown (UI-07).
3
+ * Kept free of JSX so root `tsc` (no jsx) and vitest can import them.
4
+ */
5
+ const DANGEROUS_PROTOCOLS = /^(javascript|vbscript|data):/i;
6
+ export function isSafeMarkdownHref(href) {
7
+ if (!href)
8
+ return false;
9
+ const trimmed = href.trim();
10
+ if (!trimmed)
11
+ return false;
12
+ if (DANGEROUS_PROTOCOLS.test(trimmed))
13
+ return false;
14
+ return true;
15
+ }
16
+ /** Controlled local/session image paths may load; remote/unknown never auto-fetch. */
17
+ export function isAllowedMarkdownImageSrc(src) {
18
+ if (!src)
19
+ return false;
20
+ const trimmed = src.trim();
21
+ if (!trimmed)
22
+ return false;
23
+ if (DANGEROUS_PROTOCOLS.test(trimmed))
24
+ return false;
25
+ if (trimmed.startsWith("blob:") || trimmed.startsWith("data:image/")) {
26
+ return false;
27
+ }
28
+ if (/^https?:\/\//i.test(trimmed))
29
+ return false;
30
+ if (trimmed.startsWith("/") ||
31
+ trimmed.startsWith("./") ||
32
+ trimmed.startsWith("../")) {
33
+ return true;
34
+ }
35
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed))
36
+ return true;
37
+ return false;
38
+ }
@@ -9,7 +9,7 @@ import { requestId } from "./format.js";
9
9
  * original request would have. `assistantId` is the in-flight assistant
10
10
  * message id to patch for streaming text; reconnect creates one if none. */
11
11
  export function createChatSseEventApplier(deps) {
12
- const { refs, onReconcile, setError, setProcessOpen, setInterview, setHumanGateCards, setArtifactCards, setStreaming, setStreamingAssistantId, setUnread, setOperationCards, setUsage, setMessages, setToolCalls, } = deps;
12
+ const { refs, onReconcile, setError, setInterview, setHumanGateCards, setArtifactCards, setStreaming, setStreamingAssistantId, setUnread, setOperationCards, setUsage, setMessages, setToolCalls, } = deps;
13
13
  return (eventName, data, assistantId, options) => {
14
14
  if (options?.generation !== undefined &&
15
15
  refs.sessionGenerationRef.current !== options.generation)
@@ -275,8 +275,6 @@ export function createChatSseEventApplier(deps) {
275
275
  ? payload.resultPreview
276
276
  : "{}";
277
277
  const now = Date.now();
278
- if (isError)
279
- setProcessOpen(true);
280
278
  setToolCalls((prev) => {
281
279
  const exists = prev.find((t) => t.id === id);
282
280
  if (exists) {
@@ -1,4 +1,4 @@
1
- import { truncateToolResult } from "../../chat/tool-preview.js";
1
+ import { extractToolResultText, truncateToolResult } from "../../chat/tool-preview.js";
2
2
  export function confirmationToken() {
3
3
  const w = window;
4
4
  return w.__CONSOLE_CONFIRMATION_TOKEN__ ?? "";
@@ -41,7 +41,7 @@ export function fmtDaySep(ts) {
41
41
  }
42
42
  export function resultPreview(result) {
43
43
  try {
44
- const s = typeof result === "string" ? result : JSON.stringify(result, null, 2);
44
+ const s = extractToolResultText(result);
45
45
  return truncateToolResult(s).text;
46
46
  }
47
47
  catch {
@@ -15,7 +15,10 @@ const EMPTY_USAGE = {
15
15
  * slices it patches and the derived timeline projections.
16
16
  */
17
17
  export function useChatThread(params) {
18
- const { origin, refs, sessionId, setError, setProcessOpen, onReconcile } = params;
18
+ const { origin, refs, sessionId, setError, onReconcile } = params;
19
+ // setProcessOpen remains on the hook API for session lifecycle close/reset
20
+ // (UI-08); the SSE applier must not open the process panel.
21
+ void params.setProcessOpen;
19
22
  const [messages, setMessages] = useState([]);
20
23
  const [toolCalls, setToolCalls] = useState([]);
21
24
  const [operationCards, setOperationCards] = useState([]);
@@ -37,7 +40,6 @@ export function useChatThread(params) {
37
40
  refs,
38
41
  onReconcile,
39
42
  setError,
40
- setProcessOpen,
41
43
  setInterview,
42
44
  setHumanGateCards,
43
45
  setArtifactCards,
@@ -48,7 +50,7 @@ export function useChatThread(params) {
48
50
  setUsage,
49
51
  setMessages,
50
52
  setToolCalls,
51
- }), [refs, onReconcile, setError, setProcessOpen]);
53
+ }), [refs, onReconcile, setError]);
52
54
  /** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
53
55
  * eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
54
56
  const applySseBlock = useCallback((block, assistantId, options) => {
@@ -138,13 +140,17 @@ export function useChatThread(params) {
138
140
  let statusLabel = "完成";
139
141
  let statusKind = "ok";
140
142
  if (errored.length > 0) {
141
- statusLabel = `${errored.length} 出错`;
143
+ statusLabel = `${errored.length} 次出错`;
142
144
  statusKind = "error";
143
145
  }
144
146
  else if (running.length > 0) {
145
- statusLabel = current ? `正在调用 ${current.toolName}` : "运行中";
147
+ statusLabel = current ? `${current.toolName} 运行中` : "运行中";
146
148
  statusKind = "running";
147
149
  }
150
+ else {
151
+ // Completed is default/neutral; not shown as a permanent green success chip.
152
+ statusLabel = "";
153
+ }
148
154
  return {
149
155
  count: toolCalls.length,
150
156
  statusLabel,
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
2
- import { SHORTCUTS } from "../../chat/shortcuts.js";
2
+ import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
3
3
  import { confirmationToken } from "./format.js";
4
4
  import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
5
5
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
@@ -11,12 +11,86 @@ export function useComposer(params) {
11
11
  const [pendingImages, setPendingImages] = useState([]);
12
12
  const [shortcutOpen, setShortcutOpen] = useState(false);
13
13
  const [shortcutIndex, setShortcutIndex] = useState(0);
14
+ const [dynamicCommands, setDynamicCommands] = useState([]);
15
+ const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
16
+ const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
17
+ const [commandsEpoch, setCommandsEpoch] = useState(0);
14
18
  const textareaRef = useRef(null);
19
+ const commandsSessionRef = useRef(undefined);
15
20
  const handleInputChange = useCallback((value) => {
16
21
  setInput(value);
17
22
  setShortcutOpen(value.startsWith("/"));
18
23
  setShortcutIndex(0);
19
24
  }, []);
25
+ const loadDynamicCommands = useCallback(async () => {
26
+ if (!sessionId) {
27
+ setDynamicCommands([]);
28
+ setDynamicCommandsError(null);
29
+ setDynamicCommandsLoading(false);
30
+ return;
31
+ }
32
+ const target = sessionId;
33
+ commandsSessionRef.current = target;
34
+ setDynamicCommandsLoading(true);
35
+ setDynamicCommandsError(null);
36
+ try {
37
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(target)}/commands`, { credentials: "include" });
38
+ if (commandsSessionRef.current !== target)
39
+ return;
40
+ if (!res.ok) {
41
+ setDynamicCommandsError("动态命令加载失败");
42
+ setDynamicCommands([]);
43
+ return;
44
+ }
45
+ const body = (await res.json().catch(() => null));
46
+ const items = [];
47
+ for (const raw of body?.commands ?? []) {
48
+ const source = raw.source;
49
+ if (source !== "extension" &&
50
+ source !== "prompt" &&
51
+ source !== "skill") {
52
+ continue;
53
+ }
54
+ const command = String(raw.command ?? raw.name ?? "").trim();
55
+ if (!command)
56
+ continue;
57
+ items.push({
58
+ command: command.startsWith("/") ? command : `/${command}`,
59
+ label: String(raw.label ?? raw.name ?? command),
60
+ description: String(raw.description ?? ""),
61
+ source,
62
+ });
63
+ }
64
+ if (commandsSessionRef.current !== target)
65
+ return;
66
+ setDynamicCommands(items);
67
+ }
68
+ catch {
69
+ if (commandsSessionRef.current !== target)
70
+ return;
71
+ setDynamicCommandsError("动态命令加载失败");
72
+ setDynamicCommands([]);
73
+ }
74
+ finally {
75
+ if (commandsSessionRef.current === target) {
76
+ setDynamicCommandsLoading(false);
77
+ }
78
+ }
79
+ }, [origin, sessionId]);
80
+ // Lazy-load dynamic commands when slash palette opens or session changes.
81
+ useEffect(() => {
82
+ setDynamicCommands([]);
83
+ setDynamicCommandsError(null);
84
+ if (!sessionId)
85
+ return;
86
+ if (!shortcutOpen && commandsEpoch === 0)
87
+ return;
88
+ void loadDynamicCommands();
89
+ }, [sessionId, shortcutOpen, commandsEpoch, loadDynamicCommands]);
90
+ const retryDynamicCommands = useCallback(() => {
91
+ setCommandsEpoch((n) => n + 1);
92
+ void loadDynamicCommands();
93
+ }, [loadDynamicCommands]);
20
94
  useEffect(() => {
21
95
  if (!sessionId)
22
96
  return;
@@ -94,9 +168,10 @@ export function useComposer(params) {
94
168
  }, 400);
95
169
  return () => window.clearTimeout(timer);
96
170
  }, [origin, sessionId, input]);
171
+ const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands), [dynamicCommands]);
97
172
  const filteredShortcuts = useMemo(() => input.startsWith("/")
98
- ? SHORTCUTS.filter((item) => item.command.startsWith(input.trim().split(/\s+/, 1)[0].toLowerCase()))
99
- : [], [input]);
173
+ ? filterSlashCommands(mergedCommands, input)
174
+ : [], [input, mergedCommands]);
100
175
  const onCompositionStart = useCallback(() => {
101
176
  refs.isComposingRef.current = true;
102
177
  }, [refs]);
@@ -132,6 +207,9 @@ export function useComposer(params) {
132
207
  shortcutIndex,
133
208
  setShortcutIndex,
134
209
  filteredShortcuts,
210
+ dynamicCommandsLoading,
211
+ dynamicCommandsError,
212
+ retryDynamicCommands,
135
213
  insertFileReference,
136
214
  textareaRef,
137
215
  onCompositionStart,
@@ -8,6 +8,40 @@ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-s
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
+ /**
12
+ * Extract frozen command labels from the DAG run spec (run.json).
13
+ * The run spec is written before any node executes, so it is always available
14
+ * when the prewrite gate materializes the contract.
15
+ */
16
+ async function deriveFrozenCommandLabelsFromRun(runDir) {
17
+ const specPath = path.join(runDir, "run.json");
18
+ let raw;
19
+ try {
20
+ raw = await readFile(specPath, "utf8");
21
+ }
22
+ catch {
23
+ // run.json is guaranteed to exist in production (DAG runner writes it
24
+ // before any node executes). When absent (e.g. test fixtures), there is
25
+ // no frozen set to validate against and the check is skipped downstream.
26
+ return [];
27
+ }
28
+ let spec;
29
+ try {
30
+ spec = JSON.parse(raw);
31
+ }
32
+ catch (error) {
33
+ throw new Error(`cannot derive frozen command labels: run.json is not valid JSON at ${specPath}: ${error.message}`);
34
+ }
35
+ const labels = new Set();
36
+ for (const task of spec.tasks ?? []) {
37
+ const cmdLabels = task.shell?.verifyEvidence?.commandLabels;
38
+ if (cmdLabels) {
39
+ for (const label of cmdLabels)
40
+ labels.add(label);
41
+ }
42
+ }
43
+ return [...labels];
44
+ }
11
45
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
12
46
  /**
13
47
  * Load the canonical frontend-implementation-contract-v1 JSON Schema from the
@@ -1228,6 +1262,21 @@ export async function materializeFrontendImplementationContract(input) {
1228
1262
  parsed = canonicalizeRequirementIdsInPayload(parsed);
1229
1263
  parsed = canonicalizeVerificationTargetAliases(parsed);
1230
1264
  assertFrontendContractPathsSafe(parsed);
1265
+ // Validate verificationTarget commandLabels against frozen command set.
1266
+ // The frozen set is derived from DAG verification shell task verifyEvidence.
1267
+ const frozenLabels = await deriveFrozenCommandLabelsFromRun(input.runDir);
1268
+ if (frozenLabels.length > 0) {
1269
+ const frozen = new Set(frozenLabels);
1270
+ const parsedVt = asRecord(parsed)?.verificationTargets;
1271
+ if (Array.isArray(parsedVt)) {
1272
+ for (const vt of parsedVt) {
1273
+ const label = asString(asRecord(vt)?.commandLabel);
1274
+ if (label && !frozen.has(label)) {
1275
+ throw new Error(`invalid-output: verificationTarget commandLabel "${label}" is not in the frozen command set [${[...frozen].join(", ")}]`);
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1231
1280
  const parsedTargets = asRecord(parsed)?.targets;
1232
1281
  const parsedTargetFiles = asStringArray(asRecord(parsedTargets)?.files);
1233
1282
  if (parsedTargetFiles.some((file) => file.startsWith("/") || file.includes("\\")))
@@ -191,6 +191,11 @@ export async function runFrontendPrewriteGate(input) {
191
191
  if (uncoveredTargets.length > 0) {
192
192
  throw new Error(`frontend prewrite gate contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`);
193
193
  }
194
+ const nonStaticVTs = contract.verificationTargets.filter((vt) => vt.type !== "static");
195
+ const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
196
+ if (uncoveredVTs.length > 0) {
197
+ throw new Error(`frontend prewrite gate verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`);
198
+ }
194
199
  }
195
200
  const candidatePaths = input.config.openspecCandidatePaths ?? [];
196
201
  const openspecReadPaths = await checkOpenspecReadEvidence({
@@ -2264,6 +2264,44 @@ async function buildFrontendHybridDagFromTask(sources) {
2264
2264
  "- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
2265
2265
  "- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
2266
2266
  "- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
2267
+ "",
2268
+ "## Bad / Good contract field examples",
2269
+ "",
2270
+ "### verificationTargets - BAD (invented commandLabel, missing file):",
2271
+ '{"id":"vt-1","type":"static","commandLabel":"lint","file":"","requirementIds":["AC-001"],"uiStates":[]} <-- REJECTED: commandLabel not in frozen command set; empty file path',
2272
+ "",
2273
+ '### verificationTargets - GOOD (real frozen label, real file):',
2274
+ '{"id":"vt-1","type":"static","commandLabel":"npm run typecheck","file":"tsconfig.json","requirementIds":["AC-001"],"uiStates":[]} <-- Matches frozen command set; real file path',
2275
+ "",
2276
+ "### requirements - BAD (missing expectedOutcome):",
2277
+ '{"id":"AC-001","expectedOutcome":"","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]} <-- REJECTED: empty expectedOutcome',
2278
+ "",
2279
+ "### requirements - GOOD (concrete expectedOutcome):",
2280
+ '{"id":"AC-001","expectedOutcome":"TypeScript compilation exits with code 0 and produces no errors in dist/","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]}',
2281
+ "",
2282
+ "### interactions - BAD (empty trigger/expectedBehavior):",
2283
+ '{"name":"save-click","trigger":"","expectedBehavior":"","implementationTargets":["src/button.tsx"],"verificationTargetIds":["vt-3"]} <-- REJECTED: empty trigger and expectedBehavior',
2284
+ "",
2285
+ "### interactions - GOOD:",
2286
+ '{"name":"save-click","trigger":"User clicks the Save button in the editor toolbar","expectedBehavior":"POST /api/save is called with editor content; success toast appears; button enters disabled+spinner state until response","implementationTargets":["src/editor/save-button.tsx"],"verificationTargetIds":["vt-3"]}',
2287
+ "",
2288
+ "### uiStates - BAD (applicable=true but missing expectedBehavior):",
2289
+ '{"name":"loading","applicable":true,"expectedBehavior":"","implementationTargets":[],"verificationTargetIds":[]} <-- REJECTED: applicable UI state requires non-empty expectedBehavior, implementationTargets, and verificationTargetIds',
2290
+ "",
2291
+ "### uiStates - GOOD (applicable=true with complete fields):",
2292
+ '{"name":"loading","applicable":true,"expectedBehavior":"Skeleton placeholder visible while data fetches; aria-busy=true on the list container","implementationTargets":["src/dashboard/list-view.tsx"],"verificationTargetIds":["vt-3"]}',
2293
+ "",
2294
+ "### uiStates - BAD (applicable=false without notApplicableReason):",
2295
+ '{"name":"dark-mode","applicable":false} <-- REJECTED: non-applicable UI state requires notApplicableReason',
2296
+ "",
2297
+ "### uiStates - GOOD (applicable=false with reason):",
2298
+ '{"name":"dark-mode","applicable":false,"notApplicableReason":"Dark mode toggle is out of scope for this task; only light theme is targeted"}',
2299
+ "",
2300
+ "### mockApi.endpoints - BAD (strategy=native but empty endpoints):",
2301
+ '{"strategy":"native","productionDefaultOff":true,"activation":"env flag","endpoints":[]} <-- REJECTED: native strategy requires at least one endpoint with method, path, fixture, and consumer',
2302
+ "",
2303
+ "### mockApi.endpoints - GOOD (strategy=native with complete endpoint):",
2304
+ '{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
2267
2305
  ].join("\n");
2268
2306
  })();
2269
2307
  const sourceContext = [
@@ -2276,7 +2314,21 @@ async function buildFrontendHybridDagFromTask(sources) {
2276
2314
  mockCapability.verifyCommands.length > 0;
2277
2315
  const requirementIds = frontendSourceBinding.requirementIds;
2278
2316
  const requirementCoverageInstruction = requirementIds.length > 0
2279
- ? `Include a Requirement Coverage section that lists every exact source identifier: ${requirementIds.join(", ")}. Preserve each identifier verbatim and map it to concrete implementation and verification steps.`
2317
+ ? [
2318
+ `## Requirement Coverage (per-AC echo with bad/good examples)`,
2319
+ `For each requirement ID below, echo the ID verbatim and confirm: expectedOutcome (user-observable or logic-observable), implementation targets (files), and verification targets (commandLabel + file).`,
2320
+ `Do not skip any ID. Use the bad/good patterns below as reference for each field.`,
2321
+ ``,
2322
+ `Bad example (empty expectedOutcome, empty targets -- REJECTED at contract materialization):`,
2323
+ `- AC-001: expectedOutcome="" implementationTargets=[] verificationTargets=[]`,
2324
+ ``,
2325
+ `Good example (concrete expectedOutcome, real files, real verification targets):`,
2326
+ `- AC-001: expectedOutcome="TypeScript compilation exits with code 0 and produces no errors in dist/" implementationTargets=["src/app.tsx"] verificationTargets=["vt-typecheck":"npm run typecheck","tsconfig.json"]`,
2327
+ ``,
2328
+ ...requirementIds.map((id) => `- ${id}: [expectedOutcome] [implementation files] [verification targets]`),
2329
+ ``,
2330
+ `Every requirement MUST have a non-empty expectedOutcome. Every interaction MUST have non-empty trigger and expectedBehavior. UI states with applicable=true MUST have non-empty expectedBehavior. Empty strings or omitted fields for these will cause contract rejection.`,
2331
+ ].join("\n")
2280
2332
  : "";
2281
2333
  const strategy = resolveDagVerifyStrategy(taskConfig);
2282
2334
  const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
@@ -2469,7 +2521,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2469
2521
  allowedPaths: readOnlyPaths,
2470
2522
  forbiddenPaths,
2471
2523
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2472
- outputContract: "Markdown implementation plan with Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks, followed by exactly one fenced json object conforming to frontend-implementation-contract-v1 when this node is the effective plan source. No file writes.",
2524
+ outputContract: "Markdown implementation plan with Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks, followed by exactly ONE fenced json object (\`\`\`json ... \`\`\`) conforming to frontend-implementation-contract-v1 when this node is the effective plan source. Do NOT include multiple fenced JSON blocks; only the single authoritative contract JSON block is accepted. No file writes.",
2473
2525
  subtask_prompt: [
2474
2526
  "Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
2475
2527
  "Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
@@ -2520,7 +2572,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2520
2572
  allowedPaths: readOnlyPaths,
2521
2573
  forbiddenPaths,
2522
2574
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2523
- outputContract: "When the initial design review requests revision, return a complete Markdown revision plan followed by exactly one fenced json object conforming to frontend-implementation-contract-v1. The JSON is the authoritative materialization input. No file writes.",
2575
+ outputContract: "When the initial design review requests revision, return a complete Markdown revision plan followed by exactly ONE fenced json object (\`\`\`json ... \`\`\`) conforming to frontend-implementation-contract-v1. Do NOT include multiple fenced JSON blocks; only the single authoritative contract JSON block is accepted. The JSON is the authoritative materialization input. No file writes.",
2524
2576
  subtask_prompt: [
2525
2577
  "Consume frontend-plan-pi (original plan) and frontend-design-review-pi (first design review findings).",
2526
2578
  "This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce a complete revised implementation plan that addresses every Required Plan Correction from the design findings.",
@@ -2530,6 +2582,8 @@ async function buildFrontendHybridDagFromTask(sources) {
2530
2582
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
2531
2583
  "End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
2532
2584
  "Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the revised contract; do not reduce behavior semantics to IDs and paths.",
2585
+ "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
2586
+ fixedVerificationContext,
2533
2587
  sourceContext,
2534
2588
  frontendContractSchemaBlock,
2535
2589
  ].join("\n\n"),
@@ -2581,12 +2635,16 @@ async function buildFrontendHybridDagFromTask(sources) {
2581
2635
  allowedPaths: readOnlyPaths,
2582
2636
  forbiddenPaths,
2583
2637
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2584
- outputContract: "Return exactly one JSON object conforming to frontend-implementation-contract-v1. No Markdown, prose, comments, or code fences.",
2638
+ outputContract: "Return exactly one raw JSON object conforming to frontend-implementation-contract-v1. OUTPUT ONLY THE JSON OBJECT. NO MARKDOWN, NO PROSE, NO COMMENTS, NO CODE FENCES, NO BACKTICKS. The first character must be '{' and the last character must be '}'. Any text before or after the JSON will cause the output to be REJECTED.",
2585
2639
  subtask_prompt: [
2586
2640
  "Convert the effective reviewed frontend plan into the canonical frontend-implementation-contract-v1 JSON.",
2587
2641
  "Use frontend-plan-revision-pi when it is FINISHED; otherwise use frontend-plan-pi. Confirm the effective design review passed before producing the contract.",
2588
2642
  "Return only the JSON object. Do not wrap it in Markdown or a code fence. Do not add explanatory text.",
2589
2643
  "Preserve all requirement expectedOutcome, interaction trigger/expectedBehavior, target files, verification targets, Mock/API decisions, and Real Integration Gap from the effective plan.",
2644
+ "OUTPUT REQUIREMENT: The response must consist solely of a raw JSON object - no Markdown headers, no code fences (no \`\`\`json), no explanatory prose before or after. The very first character you output must be '{' and the very last character must be '}'. If you add ANY text, the extraction gate will reject the output.",
2645
+ "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
2646
+ requirementCoverageInstruction,
2647
+ fixedVerificationContext,
2590
2648
  frontendContractSchemaBlock,
2591
2649
  sourceContext,
2592
2650
  ].join("\n\n"),
@@ -21,7 +21,7 @@
21
21
  | Frontend-test | 默认 short-chain RAG;内容/evidence 质量进入 advisory findings,路径与安全边界仍 fail-closed | `CHANGELOG.md [0.21.0]` / `[0.22.0]` |
22
22
  | Frontend-implementation | Contract / trace / repair / Mock assess;`ai_workspace` + 知识库 + `openspec` 规范发现;lint 基线债务隔离 | `CHANGELOG.md [0.22.0]` |
23
23
  | Inspect(原 Observe) | 统一 Console 的 `/inspect/` 只读运营面与富时间线;独立 `observe serve` 已硬下线 | `website/docs/guides/observe-ui.md`、`CHANGELOG.md [Unreleased]` |
24
- | Local Operator Console | `agent-worker console serve\|doctor`(loopback);Operate + Inspect、Happy Path / Interview / split view / recovery CTA;**0.23.x 起含 General Operator Chat**(Pi SDK services session、operator-only tool surface);**非**远端多用户 Console | unify design;ADR 0005;completed `2026-07-25-console-phase-4-general-operator-chat.md` |
24
+ | Local Operator Console | `agent-worker console\|doctor`(loopback;默认打开系统浏览器,`--no-open` 禁止);Operate + Inspect、Happy Path / Interview / split view / recovery CTA;**0.23.x 起含 General Operator Chat**(Pi SDK services session、operator-only tool surface);**非**远端多用户 Console | unify design;ADR 0005;completed `2026-07-25-console-phase-4-general-operator-chat.md` |
25
25
  | Task Contract / operator surface | journaled `task contract *`;machine envelope;**DagSpec v4 `taskContractBinding`**(exclusive writer 强制) | `CHANGELOG.md [0.17.0]`–`[0.17.2]`;ADR 0005 |
26
26
  | DagSpec / repair | v3 `runtimeContract` + 显式 `repairNodeId`;新 writer 生成默认 v4 binding | `dag-execution.md`、`CHANGELOG.md [0.11.0]` / `[0.17.0]` |
27
27
  | 自适应 liveness | runner lease、Provider/tool/output 活动与 meaningful progress 分离;静默 stall 受控终止,退出未确认时禁止自动重试 | `dag-execution.md`、active `2026-07-23-dag-adaptive-liveness.md` |
@@ -61,7 +61,7 @@ workflow runtime(调度 Pi / shell / static 节点)
61
61
 
62
62
  ### 本地 Operator Console(Official,0.17.x MVP,与 Observe 协作)
63
63
 
64
- - `agent-worker console serve|doctor` 提供 loopback Operator Console(默认 `127.0.0.1:8790`);Happy Path / Interview 策略面、Task Contract 落地与 recovery CTA 已随 **0.17.0–0.17.2** 发布。
64
+ - `agent-worker console|doctor` 提供 loopback Operator Console(默认 `127.0.0.1:8790`;本地图形环境 listen 后默认打开浏览器,`--no-open` 禁止);Happy Path / Interview 策略面、Task Contract 落地与 recovery CTA 已随 **0.17.0–0.17.2** 发布。
65
65
  - canonical mutation 只经 sibling 已发布 `loop-agent`(`LoopAgentClient`),不 in-process 跑 DAG kernel。
66
66
  - Observe 保持**独立**只读进程(默认 `8787`);Console 通过 versioned Observe `/api/health`(`schemaVersion`、`repoFingerprint`、route capabilities)做深链 fail-closed,**不** mount / proxy。
67
67
  - openCode 等主会话仍是 Compatibility / Operator Assist,与 Official Console **不等同**。设计锚点:ADR 0005、`docs/design/archive/2026-07-21-local-operator-console-from-pi-web.md`;handoff `docs/reports/feature/2026-07-22-console-mvp-handoff.md`。
@@ -101,7 +101,7 @@ controller identity 与 DAG skill snapshot 是两个不同冻结层(前者跨
101
101
  - 模块:`src/worker/observe/`、`src/worker/observability/{read-model,event-store}.ts`。
102
102
  - 全局快照:`buildGlobalSnapshot({ repoRoot })`(`src/worker/observability/read-model.ts`),是 **derived** 视图,消费 `.harness/` 与 Task Pool 事实,**不**改变执行成败。
103
103
  - Observe read model 是本地只读 Inspect 能力;snapshot 投影失败返回安全错误摘要而非全零健康状态。
104
- - `agent-worker console serve` 在同一进程挂载 `/inspect/` 与现有 GET `/api/**`;`/api/health` 返回 `OperatorSurfaceHealthV1`。兼容期独立 Observe `:8787` 保持旧 health DTO 与 GET-only 行为。
104
+ - `agent-worker console` 在同一进程挂载 `/inspect/` 与现有 GET `/api/**`;`/api/health` 返回 `OperatorSurfaceHealthV1`。`observe serve` 已硬下线(REMOVED / exit 2)。
105
105
 
106
106
  ### Local Operator Console(Official,0.18.0)
107
107
 
@@ -37,7 +37,7 @@
37
37
  4. 涉及测试纪律/验证声明/调试时继续读:`docs/harness-methodology-*.md`。
38
38
  5. 查看最近提交、相关 plan/progress/report;`git status --short --branch`;跑最小基线验证。
39
39
  6. 后端/接口/pytest → `taskKind: "backend-test"`(不是 `--profile`);知识回写 `knowledge-sync`;图谱开荒 `knowledge-graph-bootstrap`。`--profile` 仅 `auto|minimal|standard|reviewed|supervised`。
40
- 7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;兼容 `agent-worker console serve --repo . --port 8790`)(`/inspect/` 只读);`observe serve` 已下线(REMOVED / exit 2);`observe snapshot` 仍可用。
40
+ 7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;`--no-open` 禁止自动打开浏览器)(`/inspect/` 只读);`observe serve` 已下线(REMOVED / exit 2);`observe snapshot` 仍可用。
41
41
  8. 分支合并 → 先读 `docs/operations/branch-merge-guideline.md`。
42
42
 
43
43
  ## 会话协议
@@ -54,7 +54,7 @@
54
54
  10. 检查 `git status --short --branch`。
55
55
  11. 运行本次任务相关的最小基线验证。
56
56
  12. 如果用户提到"后端测试"、"接口测试"、"pytest"、"自动化测试",在任务 `task.json` 中设置 `taskKind: "backend-test"` 再 `task advance`;不要用 `--profile backend-test`(CLI 不接受该值,专用模板只走 taskKind)。知识回写用 `taskKind: "knowledge-sync"`(须 `featureId`),图谱开荒用 `taskKind: "knowledge-graph-bootstrap"`。`--profile` 仅表示治理强度:`auto|minimal|standard|reviewed|supervised`。
57
- 13. 如果用户提到"看板"、"observe"、"监控面板"、"启动看板",使用 `agent-worker console` 启动统一 Operator Console(默认 repo=当前目录、port=8790;兼容入口 `agent-worker console serve --repo . --port 8790`);`/inspect/` 提供只读检视。`agent-worker observe serve` 已下线(REMOVED / exit 2)。
57
+ 13. 如果用户提到"看板"、"observe"、"监控面板"、"启动看板",使用 `agent-worker console` 启动统一 Operator Console(默认 repo=当前目录、port=8790;本地图形环境默认打开浏览器,`--no-open` 禁止);`/inspect/` 提供只读检视。`agent-worker observe serve` 已下线(REMOVED / exit 2)。
58
58
  14. 如果用户要求“合并 `<source>` 到 `<target>`”或“合并 origin/main 到当前分支”,先阅读 `docs/operations/branch-merge-guideline.md`,按影响自动选择快速、标准或深度模式;始终冻结 source SHA、审查双方功能、运行 merge-tree、生成 source-SHA 合并报告,并在提交前再次 fetch 防止主干前进。
59
59
 
60
60
  ## 会话协议
@@ -89,7 +89,7 @@
89
89
  - 长期决策写入 `docs/`,不要只留在聊天里。
90
90
  - 分支合并遵循 `docs/operations/branch-merge-guideline.md`;快速模式只用于可证明的低风险/no-op 合并,涉及冲突、init/package/runtime/release/public API 时必须升级为标准或深度模式。
91
91
  - 后端测试、接口/API 测试、pytest 或明确的后端自动化测试,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。`backend-test` 是 `taskKind`,不是 `--profile` 的可选值;`task advance` 继续使用 `--profile auto` 选择治理等级。仅说“自动化测试”且前后端不明时,先根据任务源和项目技术栈判断,禁止无条件路由。
92
- - 本地 Operator Console:`agent-worker console`(默认 repo=当前目录、port=8790;兼容 `agent-worker console serve --repo . --port 8790`),访问 `http://127.0.0.1:8790/`;其中 `/inspect/` 为只读运行检视。默认绑定本机 `127.0.0.1`;可用 `--host 0.0.0.0` / `--debug`,不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。
92
+ - 本地 Operator Console:`agent-worker console`(默认 repo=当前目录、port=8790;`--no-open` 可禁止自动打开浏览器),访问 `http://127.0.0.1:8790/`;其中 `/inspect/` 为只读运行检视。默认绑定本机 `127.0.0.1`;可用 `--host 0.0.0.0` / `--debug`,不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。
93
93
  - 面向使用者的新增、修改、删除或修复,应同步更新根目录 `CHANGELOG.md`;保持版本级摘要即可,不写过细技术细节。
94
94
  - 面向用户的中文更新日志、README 和说明文档应使用自然、结果导向的表达:先说明用户能获得什么或问题如何改善,保留必要的命令和产品术语,避免逐字翻译、内部实现细节和无意义的中英混杂。
95
95
  - 涉及 `loop-agent init` 或目标项目投影的改动,必须同步考虑目标项目生成物:`AGENTS.md`、`README.md`、`harness.json`、`ai_workspace/loop-agent/`、`scripts/`、`.agents/skills/`、`.harness/prompts`、`.gitignore`(loop-agent runtime managed block)和 npm 包内置 assets;目标项目根 `docs/` 和根 `skills/` 的旧投影需要由 `init update --apply-safe` 安全迁移或退役。
@@ -127,8 +127,8 @@ loop-agent task status <task-id> --json
127
127
  ### 运行看板(只读)
128
128
 
129
129
  ```bash
130
- agent-worker console # 默认 repo=当前目录,port=8790
131
- agent-worker console serve --repo . --port 8790 # 兼容入口,等价于上面裸入口
130
+ agent-worker console # 默认 repo=当前目录,port=8790;本地图形环境 listen 后默认打开浏览器
131
+ agent-worker console --no-open # 只启动服务,不打开浏览器
132
132
  ```
133
133
 
134
134
  浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。`agent-worker observe serve` 已下线(REMOVED / exit 2)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.34.4",
3
+ "version": "0.35.0-beta.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -75,12 +75,15 @@
75
75
  "@earendil-works/pi-coding-agent": "0.80.10"
76
76
  },
77
77
  "devDependencies": {
78
+ "@remixicon/react": "^4.9.0",
78
79
  "@types/node": "^24.6.0",
79
80
  "@types/react": "^19.1.8",
80
81
  "@types/react-dom": "^19.1.6",
81
82
  "@types/semver": "^7.7.1",
82
83
  "react": "^19.1.0",
83
84
  "react-dom": "^19.1.0",
85
+ "react-markdown": "^10.1.0",
86
+ "remark-gfm": "^4.0.1",
84
87
  "tsx": "^4.20.6",
85
88
  "typescript": "^5.9.3",
86
89
  "vite": "^7.0.0",
@@ -16,7 +16,7 @@ references:
16
16
  - **允许**:`agent-worker` / `loop-agent` CLI;只读 `pool doctor`、`observe`、status/report;冻结 controller identity;选择 Ready 工作与 recovery 命令。
17
17
  - **禁止**:绕过 CLI 直接 Edit 业务实现;Worker/DAG 失败后主会话「救火改文件」。
18
18
  - **失败时只允许**:保留 evidence → `task retry` / `task reconcile` / `pool mark-failed` / human gate → 再经 CLI 重跑;实现写入仍只经 published `loop-agent` DAG。
19
- - **Official vs Compatibility**:`agent-worker console` 是 Official 本地控制面(裸入口直接启动;默认 repo=当前目录、port=8790;兼容入口 `agent-worker console serve --repo . --port 8790` 等价)。同进程提供 Operate + Inspect,Inspect 路径 `/inspect/#/...`;openCode 等主会话仍是 Compatibility Assist,二者**不是**同等保证。`observe serve` 已硬下线(`OBSERVE_SERVE_REMOVED` + exit 2,不监听端口);只读检视请用 Console `/inspect/`;`observe snapshot` 仍输出 GlobalSnapshot JSON。
19
+ - **Official vs Compatibility**:`agent-worker console` 是 Official 本地控制面唯一启动入口(默认 repo=当前目录、port=8790;本地图形环境 listen 后默认打开系统浏览器,`--no-open` 禁止)。同进程提供 Operate + Inspect,Inspect 路径 `/inspect/#/...`;openCode 等主会话仍是 Compatibility Assist,二者**不是**同等保证。`observe serve` 已硬下线(`OBSERVE_SERVE_REMOVED` + exit 2,不监听端口);只读检视请用 Console `/inspect/`;`observe snapshot` 仍输出 GlobalSnapshot JSON。
20
20
 
21
21
  ## Route the Work
22
22
 
@@ -54,8 +54,8 @@ Leaf DAG nodes 不得递归启动 `agent-worker`。Worker 负责 DAG 之外的 s
54
54
  ```
55
55
 
56
56
  doctor 只读;migrate 默认零写入,apply 失败全回滚且不改 JSONL。
57
- 8. Inspect(原 Observe)只读:仅推荐 `agent-worker console`(或 `console serve`)的 `/inspect/#/...`;`observe snapshot` 仍可用。`observe serve` 已下线(REMOVED / exit 2)。canonical Task 路由为 `/api/features/:featureId/tasks/:taskId` 与 `#/feature/:featureId/task/:taskId`。8790 `/api/health` 为 `OperatorSurfaceHealthV1`。
58
- 9. Official Console:`agent-worker console serve|doctor`(默认 loopback;可 `--host 0.0.0.0` / `--debug`)。Recovery CTA 为 report/doctor/decision/resume/reconcile/regenerate/打开检视;无 Cancel、无主 CTA「直接改代码」。主会话 Compatibility Assist 不得替代 Console/CLI 执法。
57
+ 8. Inspect(原 Observe)只读:仅推荐 `agent-worker console` `/inspect/#/...`;`observe snapshot` 仍可用。`observe serve` 已下线(REMOVED / exit 2)。canonical Task 路由为 `/api/features/:featureId/tasks/:taskId` 与 `#/feature/:featureId/task/:taskId`。8790 `/api/health` 为 `OperatorSurfaceHealthV1`。
58
+ 9. Official Console:`agent-worker console|doctor`(默认 loopback;可 `--host 0.0.0.0` / `--debug` / `--no-open`)。Recovery CTA 为 report/doctor/decision/resume/reconcile/regenerate/打开检视;无 Cancel、无主 CTA「直接改代码」。主会话 Compatibility Assist 不得替代 Console/CLI 执法。
59
59
 
60
60
  ## Feature Packet Scaffold
61
61
 
@@ -575,8 +575,7 @@ agent-worker scheduler clock status --repo <repo-root> [--json] # CLI-only
575
575
  agent-worker scheduler clock uninstall --repo <repo-root> [--json] # CLI-only
576
576
  agent-worker scheduler submit --repo <repo-root> --feature-id <id> --task-id <id> ... # low-level planning fact
577
577
  agent-worker scheduler transition <schedule-id> --repo <repo-root> --to <status> ... # internal lifecycle
578
- agent-worker console [--repo <repo-root>] [--port 8790] [--host 127.0.0.1] # Official 裸入口;repo 默认当前目录
579
- agent-worker console serve --repo <repo-root> [--port 8790] [--host 127.0.0.1] # 兼容入口,等价于裸入口
578
+ agent-worker console [--repo <repo-root>] [--port 8790] [--host 127.0.0.1] [--no-open] # Official 唯一入口;repo 默认当前目录;默认打开浏览器
580
579
  agent-worker console doctor --repo <repo-root> [--json] [--console-url <url>]
581
580
  agent-worker observe serve ... # REMOVED:stderr OBSERVE_SERVE_REMOVED,exit 2,不监听端口;请用 console + /inspect/
582
581
  agent-worker observe snapshot --repo <repo-root> # 输出 GlobalSnapshot JSON 到 stdout
@@ -602,7 +601,7 @@ agent-worker observe snapshot --repo <repo-root> # 输出 GlobalSnapshot JSON
602
601
  - `report metrics` 按 UTC 月去重投影 Feature/Failure/Follow-up/Delivery/AC/decision/recovery/boundary 指标,同时写 JSON 与 Markdown;每项保留 numerator、denominator、sampleSize 和 missingData。
603
602
  - `task draft-followup` 会按全部 failure category 生成 TaskDraft 或人工行动卡:ProductBug/TestBug/FlakyTest/DependencyFailure 可批准;EnvFailure 连续两次后才生成 ENV-CHECK;Spec/Contract/Risk/Human/Unknown 只给行动卡。人工以 `feature approve-followup --dry-run` 预览,再带非空 `--owner` 批准 TaskDraft;行动卡不能批准。批准在 staging validation 后写 TaskSpec、graph、Ready/approval/event,原失败事实不改写,并有 rename/state/approval/index/event 回滚门禁。
604
603
  - `task retry` 是失败 Task 的唯一重试入口。它会保留原有运行记录和 failure handoff,并让下一次 `batch run-ready` 使用新的 `workerRunId`;不要删除运行态文件或手动修改状态来重试。
605
- - **推荐**裸入口 `agent-worker console`(repo 默认当前目录,默认 `127.0.0.1:8790`)提供 Operate + Inspect;`console serve` 是等价兼容入口。Inspect 路径为 `/inspect/#/...`,API 仍为根 `/api/**`。`observe serve` 已硬下线(`OBSERVE_SERVE_REMOVED` + exit 2,不监听);`observe snapshot` 保留。Inspect 本身不会启动、暂停或重试 Task / Worker / DAG。
604
+ - **推荐**裸入口 `agent-worker console`(repo 默认当前目录,默认 `127.0.0.1:8790`)提供 Operate + Inspect;本地图形环境 listen 成功后默认打开系统浏览器,`--no-open` 禁止;`console serve` 已删除。Inspect 路径为 `/inspect/#/...`,API 仍为根 `/api/**`。`observe serve` 已硬下线(`OBSERVE_SERVE_REMOVED` + exit 2,不监听);`observe snapshot` 保留。Inspect 本身不会启动、暂停或重试 Task / Worker / DAG。
606
605
  - Night Scheduler(本地夜间自治):白天 `admission prepare` 冻结 worktree + DAG writeSet gate;`scheduler add` 消费 gate 并预约 Task Pool `Queued`(不立即执行);`scheduler clock install` 安装本机 user-level OS timer(launchd / systemd --user / schtasks)周期调用 `scheduler tick`;tick 写统一 Clock receipt,doctor/morning/status 可解释 missing/stale/error/busy/drift;成功后 `pending-harvest`,早晨 `scheduler harvest`(exact-base FF)或 `scheduler discard`;`report morning --window night` 与 Inspect `#/night` / Operate 夜间面板读取同一套 facts。Clock install/status/uninstall 为 CLI-only。
607
606
  - 当前 Worker 仍是 v0(库 + CLI + dogfood);日间批处理未强制定时/CI 驱动;`report morning` 默认可从 Task Pool runs 汇总,并支持 `--window night` 投影 Scheduler facts。
608
607