@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.
- package/CHANGELOG.md +19 -0
- package/dist/application/dag/generate-task-dag.js +12 -1
- package/dist/executors/shell-executor.js +189 -2
- package/dist/worker/observe/spec-evidence.js +33 -0
- package/dist/worker/observe/static/views/dag-inspector.js +67 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +163 -41
- package/dist/workflows/dag/backend-test-result-contract.js +30 -7
- package/dist/workflows/dag/frontend-prewrite-gate.js +172 -0
- package/dist/workflows/dag/frontend-project-capability.js +6 -2
- package/dist/workflows/dag/frontend-repair.js +7 -1
- package/dist/workflows/dag/frontend-review-context.js +43 -0
- package/dist/workflows/dag/frontend-verification-trace.js +34 -15
- package/dist/workflows/dag/governance-profile.js +14 -6
- package/dist/workflows/dag/init-hybrid.js +144 -399
- package/dist/workflows/dag/types.js +33 -0
- package/dist/workflows/dag/validate.js +22 -1
- package/docs/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +40 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +23 -192
- package/docs/templates/backend-test-dag.json +8 -8
- package/docs/templates/backend-test-dag.review-cases.prompt.md +22 -75
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +5 -3
- package/skills/frontend-design-review/references/review-checklist.md +3 -2
- package/skills/frontend-implementation/references/design-spec.md +16 -8
- package/skills/frontend-implementation/references/node-contracts.md +6 -8
- package/skills/frontend-review/SKILL.md +12 -9
- package/skills/frontend-review/references/review-findings.md +5 -1
- package/skills/frontend-verification/SKILL.md +5 -3
- package/skills/frontend-verification/references/verification-checklist.md +3 -2
- 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:
|
|
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
|
-
|
|
278
|
-
|
|
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") {
|