@tea-agent/loop-agent 0.19.0 → 0.20.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 (31) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/application/dag/generate-task-dag.js +12 -1
  3. package/dist/executors/shell-executor.js +189 -2
  4. package/dist/worker/observe/spec-evidence.js +33 -0
  5. package/dist/worker/observe/static/views/dag-inspector.js +67 -4
  6. package/dist/workflows/dag/backend-test-markdown-workflow.js +163 -41
  7. package/dist/workflows/dag/backend-test-result-contract.js +30 -7
  8. package/dist/workflows/dag/frontend-prewrite-gate.js +172 -0
  9. package/dist/workflows/dag/frontend-project-capability.js +6 -2
  10. package/dist/workflows/dag/frontend-repair.js +7 -1
  11. package/dist/workflows/dag/frontend-review-context.js +43 -0
  12. package/dist/workflows/dag/frontend-verification-trace.js +34 -15
  13. package/dist/workflows/dag/governance-profile.js +14 -6
  14. package/dist/workflows/dag/init-hybrid.js +144 -399
  15. package/dist/workflows/dag/types.js +33 -0
  16. package/dist/workflows/dag/validate.js +22 -1
  17. package/docs/README.md +1 -0
  18. package/docs/templates/agent-dag.schema.json +40 -0
  19. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +23 -192
  20. package/docs/templates/backend-test-dag.json +8 -8
  21. package/docs/templates/backend-test-dag.review-cases.prompt.md +22 -75
  22. package/package.json +1 -1
  23. package/skills/frontend-design-review/SKILL.md +5 -3
  24. package/skills/frontend-design-review/references/review-checklist.md +3 -2
  25. package/skills/frontend-implementation/references/design-spec.md +16 -8
  26. package/skills/frontend-implementation/references/node-contracts.md +6 -8
  27. package/skills/frontend-review/SKILL.md +12 -9
  28. package/skills/frontend-review/references/review-findings.md +5 -1
  29. package/skills/frontend-verification/SKILL.md +5 -3
  30. package/skills/frontend-verification/references/verification-checklist.md +3 -2
  31. package/skills/loop-agent/references/hybrid-dag.md +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### 新增
6
+
7
+ - 前端规范读取将知识库与 `openspec/` 作为并列来源:无论知识库是否可用或命中,均继续递归枚举 `<repoRoot>/openspec/**` 并读取索引与任务相关规范正文。`discoverFrontendProjectCapability` 在没有 `package.json` 或 `package.json` 不可读时仍能发现 `openspec/**` 规范候选。
8
+ - 前端预写门禁会校验 plan/design-review 的真实读取事件;生成期存在 `openspec` 规范候选但没有成功读取匹配文件时,在 writer 执行前阻断 DAG 并列出候选路径、检查节点和原因。
9
+ - Dashboard 规范证据 API/UI 分别展示知识库查询、`openspec` 检索和 `openspec` 成功读取证据,三种类别独立展示。
10
+ - 前端规范证据 API 新增 `openspecReads` 与 `openspecSearches` 字段,与通用 `specReads`/`specSearches` 并行。
11
+
12
+ ## [0.20.0] - 2026-07-23
13
+
14
+ ### 新增
15
+
16
+ - streamline implementation DAG (#61)
17
+ - improve human-readable artifacts
18
+
19
+ ### 修复
20
+
21
+ - relax traceability mapping
22
+
5
23
  ## [0.19.0] - 2026-07-23
6
24
 
7
25
  ### 重点更新
@@ -17,6 +35,7 @@
17
35
 
18
36
  ### 改进
19
37
 
38
+ - `frontend-implementation` 保留独立 contract/scout,并把 Mock 策略与 implementation contract 合入 plan,把写前授权、验证/失败评估、复验和 review context 分别收敛为组合 shell 节点。standard/high-risk 固定 15 个顶层节点、small-risk 13 个;绿色路径执行 11 个节点、7 次 Pi,同时保留唯一 prewrite 写入授权、同 writeSet repair、真实 diff review 与 fail-closed closeout。
20
39
  - 优化操作与观测统一界面的导航与布局,合并确认与运行为「开始运行」,DAG 详情跳转路径更加准确
21
40
 
22
41
  ### 修复
@@ -105,6 +105,17 @@ function isVerificationShellTask(task) {
105
105
  const commands = resolveShellCommands(task.shell);
106
106
  return commands.some((command) => /(vitest|npm run (lint|typecheck|test)|check-repo\.sh|loop-agent-standard-verify)/.test(command));
107
107
  }
108
+ function resolveReviewPacketShellCommands(task) {
109
+ const bundle = task.shell?.frontendVerificationBundle;
110
+ if (bundle) {
111
+ return [
112
+ ...bundle.mockCommands,
113
+ ...bundle.staticCommands,
114
+ ...bundle.behaviorCommands,
115
+ ];
116
+ }
117
+ return resolveShellCommands(task.shell);
118
+ }
108
119
  async function buildReviewPacket(input) {
109
120
  const spec = parseDagSpec(JSON.parse(await readFile(input.dagPath, "utf-8")));
110
121
  const writers = collectWriterTasksForPacket(spec).map((task) => {
@@ -123,7 +134,7 @@ async function buildReviewPacket(input) {
123
134
  .filter(isVerificationShellTask)
124
135
  .map((task) => ({
125
136
  nodeId: task.id,
126
- commands: resolveShellCommands(task.shell),
137
+ commands: resolveReviewPacketShellCommands(task),
127
138
  }));
128
139
  return {
129
140
  profileRouting: {
@@ -12,10 +12,12 @@ import { materializeFrontendTestResult, } from "../workflows/dag/frontend-test-r
12
12
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
13
13
  import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
14
14
  import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
15
+ import { formatFrontendPrewriteGateStdout, runFrontendPrewriteGate, } from "../workflows/dag/frontend-prewrite-gate.js";
16
+ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
15
17
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
16
18
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
17
19
  import { materializeBackendTestResultFromRunDir, parseJunitXml } from "../workflows/dag/backend-test-result-contract.js";
18
- import { inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
20
+ import { collectBackendTestHumanCaseCatalog, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
19
21
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
20
22
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
21
23
  import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
@@ -356,14 +358,16 @@ async function executeBackendTestPipeline(input, meta) {
356
358
  if (![0, 1].includes(pytestExitCode))
357
359
  throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
358
360
  const parsed = parseJunitXml(junitContent);
361
+ const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
359
362
  const htmlContent = renderBackendTestHtml({
360
363
  title: meta.spec.title,
361
364
  parsed,
365
+ cases,
362
366
  environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8"),
363
367
  traceabilitySummary: await readFile(path.join(reportsDir, "backend-test-traceability.md"), "utf8"),
364
368
  });
365
369
  const htmlPath = await writeRunReport(meta.runDir, "backend-test.html", htmlContent);
366
- const facts = renderBackendTestFacts({ parsed, pytestExitCode, junitRelativePath: "reports/backend-test.junit.xml", htmlRelativePath: "reports/backend-test.html", junitContent, htmlContent });
370
+ const facts = renderBackendTestFacts({ parsed, cases, pytestExitCode, junitRelativePath: "reports/backend-test.junit.xml", htmlRelativePath: "reports/backend-test.html", junitContent, htmlContent });
367
371
  const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
368
372
  const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
369
373
  outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `facts=${factsPath}`, facts);
@@ -601,8 +605,191 @@ async function executePipelineCommands(input, meta, overrideCommands) {
601
605
  }
602
606
  return results;
603
607
  }
608
+ async function executeFrontendVerificationBundle(input, meta) {
609
+ const started = Date.now();
610
+ const shell = input.task.shell;
611
+ const bundle = shell.frontendVerificationBundle;
612
+ const cwd = resolveShellCwd(input.cwd, shell.cwd);
613
+ const results = [];
614
+ let beforeStatus;
615
+ try {
616
+ beforeStatus = await readGitStatusPorcelain(input.cwd);
617
+ }
618
+ catch {
619
+ beforeStatus = undefined;
620
+ }
621
+ const groups = [
622
+ { name: "mock", commands: bundle.mockCommands },
623
+ { name: "static", commands: bundle.staticCommands },
624
+ { name: "behavior", commands: bundle.behaviorCommands },
625
+ ];
626
+ for (const group of groups) {
627
+ for (const command of group.commands) {
628
+ const commandNumber = results.length + 1;
629
+ const result = await executeShellCommand({
630
+ command,
631
+ cwd,
632
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
633
+ envAllowlist: shell.envAllowlist,
634
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
635
+ outputArtifacts: {
636
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
637
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
638
+ },
639
+ });
640
+ results.push(result);
641
+ if (!result.ok)
642
+ break;
643
+ }
644
+ }
645
+ if (beforeStatus !== undefined) {
646
+ const guard = await runShellWriteGuard({
647
+ rootCwd: input.cwd,
648
+ task: input.task,
649
+ beforeStatus,
650
+ });
651
+ if (!guard.ok) {
652
+ return {
653
+ ok: false,
654
+ stdout: "",
655
+ stderr: `write guard failed: ${guard.violations.join(", ")}`,
656
+ failureCategory: "write-guard",
657
+ durationMs: Date.now() - started,
658
+ };
659
+ }
660
+ }
661
+ await writeDagNodeTextArtifact(meta.runDir, input.task.id, "result.summary.md", buildShellResultSummaryMarkdown({
662
+ nodeId: input.task.id,
663
+ runId: meta.runId,
664
+ rootCwd: input.cwd,
665
+ results,
666
+ }));
667
+ const commandResults = results.map((result) => ({
668
+ ok: result.ok,
669
+ exitCode: result.exitCode,
670
+ failureCategory: result.failureCategory,
671
+ command: result.command,
672
+ }));
673
+ const firstFailure = results.find((result) => !result.ok);
674
+ let traceError;
675
+ try {
676
+ await runFrontendVerificationTraceGate({
677
+ runDir: meta.runDir,
678
+ workspaceRoot: input.cwd,
679
+ evidence: {
680
+ static: {
681
+ nodeId: input.task.id,
682
+ commandLabels: bundle.staticEvidence.commandLabels,
683
+ },
684
+ behavior: {
685
+ nodeId: input.task.id,
686
+ commandLabels: bundle.behaviorEvidence.commandLabels,
687
+ },
688
+ },
689
+ });
690
+ }
691
+ catch (error) {
692
+ traceError = error instanceof Error ? error : new Error(String(error));
693
+ }
694
+ if (bundle.mode === "repair") {
695
+ if (firstFailure || traceError) {
696
+ return {
697
+ ok: false,
698
+ stdout: summarizeCommandResults(results).stdout,
699
+ stderr: firstFailure?.stderr || traceError?.message || "frontend reverify failed",
700
+ failureCategory: firstFailure?.failureCategory ?? "invalid-output",
701
+ durationMs: Date.now() - started,
702
+ ...{ commandResults },
703
+ };
704
+ }
705
+ return {
706
+ ok: true,
707
+ stdout: "Frontend reverify bundle: pass",
708
+ stderr: "",
709
+ failureCategory: "success",
710
+ durationMs: Date.now() - started,
711
+ ...{ commandResults },
712
+ };
713
+ }
714
+ const failureFacts = [];
715
+ if (firstFailure) {
716
+ failureFacts.push({
717
+ nodeId: input.task.id,
718
+ record: {
719
+ status: "FINISHED",
720
+ failureCategory: firstFailure.failureCategory,
721
+ stdout: summarizeCommandResults(results).stdout,
722
+ stderr: firstFailure.stderr,
723
+ commandResults,
724
+ },
725
+ });
726
+ }
727
+ if (traceError) {
728
+ failureFacts.push({
729
+ nodeId: "frontend-verification-trace-shell",
730
+ record: {
731
+ status: "FINISHED",
732
+ failureCategory: "invalid-output",
733
+ stderr: traceError.message,
734
+ },
735
+ });
736
+ }
737
+ try {
738
+ const assessment = await runFrontendFailureAssessGate({
739
+ runDir: meta.runDir,
740
+ failureFacts,
741
+ });
742
+ await runFrontendRepairContractGate({ runDir: meta.runDir });
743
+ return {
744
+ ok: true,
745
+ stdout: formatFrontendFailureAssessStdout(assessment),
746
+ stderr: "",
747
+ failureCategory: "success",
748
+ durationMs: Date.now() - started,
749
+ ...{ commandResults },
750
+ };
751
+ }
752
+ catch (error) {
753
+ return {
754
+ ok: false,
755
+ stdout: "",
756
+ stderr: error instanceof Error ? error.message : String(error),
757
+ failureCategory: "invalid-output",
758
+ durationMs: Date.now() - started,
759
+ ...{ commandResults },
760
+ };
761
+ }
762
+ }
604
763
  export async function executeDagShellNode(input, meta) {
605
764
  const shell = input.task.shell;
765
+ if (shell?.frontendPrewriteGate) {
766
+ const started = Date.now();
767
+ try {
768
+ const result = await runFrontendPrewriteGate({
769
+ runDir: meta.runDir,
770
+ config: shell.frontendPrewriteGate,
771
+ sourceBinding: meta.spec.sourceBinding,
772
+ repoRoot: input.cwd,
773
+ });
774
+ return { ok: true, stdout: formatFrontendPrewriteGateStdout(result), stderr: "", failureCategory: "success", durationMs: Date.now() - started };
775
+ }
776
+ catch (error) {
777
+ return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
778
+ }
779
+ }
780
+ if (shell?.frontendVerificationBundle) {
781
+ return executeFrontendVerificationBundle(input, meta);
782
+ }
783
+ if (shell?.frontendReviewContext) {
784
+ const started = Date.now();
785
+ try {
786
+ const result = await runFrontendReviewContextGate({ runDir: meta.runDir, workspaceRoot: input.cwd });
787
+ return { ok: true, stdout: formatFrontendReviewContextStdout(result), stderr: "", failureCategory: "success", durationMs: Date.now() - started };
788
+ }
789
+ catch (error) {
790
+ return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
791
+ }
792
+ }
606
793
  if (shell?.backendTestPipeline) {
607
794
  return executeBackendTestPipelineWithWriteGuard(input, meta);
608
795
  }
@@ -47,6 +47,23 @@ function isSpecFilePath(filePath) {
47
47
  function isKnowledgeBaseTool(toolName) {
48
48
  return KB_CONNECTOR_TOOLS.has(toolName);
49
49
  }
50
+ /** A repo-relative path references <repoRoot>/openspec/**. */
51
+ function isOpenspecPath(filePath) {
52
+ const normalized = filePath.replaceAll(path.sep, "/");
53
+ return normalized === "openspec" || normalized.startsWith("openspec/");
54
+ }
55
+ /** A search query targets the openspec/ directory. */
56
+ function isOpenspecSearch(query, searchPath) {
57
+ const lower = query.toLowerCase();
58
+ const normalizedPath = searchPath?.replaceAll(path.sep, "/").toLowerCase();
59
+ return (normalizedPath === "openspec" ||
60
+ normalizedPath?.startsWith("openspec/") === true ||
61
+ lower === "openspec" ||
62
+ lower === "openspec/" ||
63
+ lower.startsWith("openspec/") ||
64
+ lower.includes("openspec/**") ||
65
+ lower.includes("openspec/*"));
66
+ }
50
67
  function resolveSessionEventsPath(repoRoot, dagRunId, nodeId) {
51
68
  if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
52
69
  return null;
@@ -278,6 +295,9 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
278
295
  }
279
296
  // Search/scan tool calls (grep, find, ls, glob)
280
297
  if (["grep", "find", "ls", "glob"].includes(toolName)) {
298
+ const searchPath = typeof pairedInput.path === "string"
299
+ ? pairedInput.path
300
+ : undefined;
281
301
  const query = typeof pairedInput.pattern === "string"
282
302
  ? pairedInput.pattern
283
303
  : typeof pairedInput.query === "string"
@@ -288,6 +308,7 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
288
308
  searches.push({
289
309
  tool: toolName,
290
310
  query,
311
+ path: searchPath,
291
312
  timestamp: ts,
292
313
  });
293
314
  if (toolCallId)
@@ -341,6 +362,16 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
341
362
  if (status === "no-evidence") {
342
363
  summaryLines.push("未观察到任何规范证据:无 skill 注入、无文件读取、无检索操作。");
343
364
  }
365
+ // Separate openspec/** reads and searches for Dashboard display.
366
+ // Knowledge base and openspec are parallel sources.
367
+ const openspecReads = specReads.filter((r) => isOpenspecPath(r.path));
368
+ const openspecSearches = searches.filter((s) => isOpenspecSearch(s.query, s.path));
369
+ if (openspecReads.length > 0) {
370
+ summaryLines.push(`openspec 已读取 ${openspecReads.length} 个文件:${openspecReads.map((r) => r.path).join("、")}`);
371
+ }
372
+ if (openspecSearches.length > 0) {
373
+ summaryLines.push(`openspec 检索 ${openspecSearches.length} 次。`);
374
+ }
344
375
  return {
345
376
  dagRunId,
346
377
  nodeId,
@@ -352,6 +383,8 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
352
383
  specReads,
353
384
  specSearches: searches,
354
385
  knowledgeBaseQueries: kbQueries,
386
+ openspecReads,
387
+ openspecSearches,
355
388
  summary: summaryLines.join(" "),
356
389
  };
357
390
  }
@@ -233,10 +233,15 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
233
233
  }
234
234
  content.appendChild(statusSection);
235
235
 
236
- const appendListSection = (title, entries, renderEntry) => {
237
- if (!entries?.length) return;
236
+ const appendListSection = (title, entries, renderEntry, emptyMessage = "") => {
237
+ if (!entries?.length && !emptyMessage) return;
238
238
  const section = el("div", "spec-evidence-section");
239
239
  section.appendChild(el("h4", null, title));
240
+ if (!entries?.length) {
241
+ section.appendChild(el("p", "spec-evidence-summary", emptyMessage));
242
+ content.appendChild(section);
243
+ return;
244
+ }
240
245
  const list = el("ul", "spec-evidence-list");
241
246
  for (const entry of entries) list.appendChild(renderEntry(entry));
242
247
  section.appendChild(list);
@@ -273,9 +278,15 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
273
278
  return item;
274
279
  });
275
280
  appendListSection("显式需求编号", evidence.sourceBinding?.requirementIds, (id) => el("li", null, id));
281
+ const openspecReadPaths = new Set(
282
+ (evidence.openspecReads ?? []).map((entry) => entry.path),
283
+ );
284
+ const otherSpecReads = (evidence.specReads ?? []).filter(
285
+ (entry) => !openspecReadPaths.has(entry.path),
286
+ );
276
287
  appendListSection(
277
- `已读取规范文件(${evidence.specReads?.length ?? 0})`,
278
- evidence.specReads,
288
+ `其他已读取规范文件(${otherSpecReads.length})`,
289
+ otherSpecReads,
279
290
  (read) => {
280
291
  const item = el("li", null);
281
292
  const button = document.createElement("button");
@@ -327,6 +338,58 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
327
338
  item.append(icon, document.createTextNode(` ${query.connector}`));
328
339
  return item;
329
340
  },
341
+ "unavailable:未观察到知识库查询;connector 可能未配置或未调用。",
342
+ );
343
+
344
+ // openspec reads are displayed independently from skill/docs reads.
345
+ appendListSection(
346
+ `openspec 已读取(${evidence.openspecReads?.length ?? 0})`,
347
+ evidence.openspecReads,
348
+ (read) => {
349
+ const item = el("li", null);
350
+ const button = document.createElement("button");
351
+ button.type = "button";
352
+ button.className = "spec-evidence-file-btn";
353
+ button.setAttribute("data-source", "read");
354
+ button.setAttribute("data-path", read.path);
355
+ if (read.timestamp) {
356
+ button.setAttribute("data-read-at", read.timestamp);
357
+ }
358
+ button.id = `spec-evidence-btn-read-${read.path}`;
359
+ const icon = el("i", "ri-book-open-line");
360
+ icon.setAttribute("aria-hidden", "true");
361
+ button.append(icon, el("code", null, read.path));
362
+ if (read.timestamp) {
363
+ button.appendChild(
364
+ el(
365
+ "span",
366
+ "spec-evidence-time",
367
+ formatSessionEventTime({ timestamp: read.timestamp }),
368
+ ),
369
+ );
370
+ }
371
+ button.addEventListener("click", () =>
372
+ onSpecEvidenceFileClick(content, dagRunId, nodeId, "read", read.path, button),
373
+ );
374
+ item.appendChild(button);
375
+ return item;
376
+ },
377
+ evidence.openspecSearches?.length
378
+ ? "已检索但未观察到 openspec 成功读取;若生成期存在候选,预写门禁会阻断 writer。"
379
+ : "unavailable:未观察到 openspec 成功读取。",
380
+ );
381
+
382
+ appendListSection(
383
+ `openspec 检索(${evidence.openspecSearches?.length ?? 0})`,
384
+ evidence.openspecSearches,
385
+ (search) => {
386
+ const item = el("li", null);
387
+ const icon = el("i", "ri-search-line");
388
+ icon.setAttribute("aria-hidden", "true");
389
+ item.append(icon, document.createTextNode(` ${search.tool}: ${search.query}`));
390
+ return item;
391
+ },
392
+ "unavailable:未观察到 openspec 检索。",
330
393
  );
331
394
 
332
395
  if (evidence.status === "source-bound" || evidence.status === "spec-injected" || evidence.status === "no-evidence") {