@tea-agent/loop-agent 0.17.2 → 0.18.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.18.1] - 2026-07-22
6
+
7
+ ### 重点更新
8
+
9
+ - 放宽后端测试 Markdown 用例的映射与验收标准校验,减少合法用例被误拦截的情况
10
+ - 修复文档审计报错未阻断 CI 的治理漏洞,并全面修正失效链接
11
+ - 清理发布日志中残留的已发布记录,避免将已上线能力误展示为未发布
12
+
13
+ ### 修复
14
+
15
+ - 放宽后端测试 Markdown 用例的每条补充 Case 必须包含 AC 标识的硬性要求,改为确保任务要求的业务 AC 场景至少被一条最终用例覆盖即可,整体缺失必需 AC 时仍会严格阻断
16
+ - 放宽后端测试 traceability 映射校验,Case ID 可位于测试函数名、装饰器、函数体或文档字符串区域,不再强制首行文档字符串或严格一对一映射,避免重复映射等情况导致的误阻断
17
+ - 修复文档审计在发现错误时未返回非零退出码导致 CI 假绿的问题,并将链接门禁覆盖范围扩展至完整的 docs/ 目录
18
+ - 修正文档报告分类迁移后遗留的 269 条失效链接
19
+ - 修复自动发布创建版本区块时未正确消费已发布内容并清理残留记录的问题,移除了随 0.18.0 版本发布的 59 条残留日志
20
+
21
+ ## [0.18.0] - 2026-07-22
22
+
23
+ ### 重点更新
24
+
25
+ - 后端测试全面采用 Markdown 优先工作流,整合环境预检、独立审查与确定性门禁,单次执行即可生成完整测试报告。
26
+ - 前端实现与测试流程大幅优化,按需执行计划修订与验证,常规全绿路径执行节点数量显著减少。
27
+ - 修复多项严格校验导致的误中断问题,大幅提升 Markdown 用例生成、追踪映射与异常清理的兼容性。
28
+ - 优化 Pi CLI 与 SDK 执行器,彻底解决长文本输出导致的内存溢出与进程卡死问题。
29
+
30
+ ### 新增
31
+
32
+ - 引入 Markdown 优先的后端测试工作流,支持环境预检、独立审查用例并生成自包含的 HTML 与 Markdown 报告。
33
+ - 新增升级后的初始化面通知器,在检测到版本不一致时自动提示安全更新或引导人工处理。
34
+ - 新增统一的初始化协调命令,支持安全应用更新并智能处理各类阻断情况。
35
+ - 测试报告新增 L-5 指标消费与统一的代码覆盖率报告入口,支持 Python 与 Java 测试数据。
36
+
37
+ ### 改进
38
+
39
+ - 前端实现绿色路径改为按需分支执行,常规无错路径可少执行多个节点,提升执行效率。
40
+ - 前端测试用例生成现可自动解析明确的 baseUrl,不再保留占位符,避免执行异常。
41
+ - 前端测试复盘报告写入路径调整,支持安全禁止整个 docs 目录的写入权限。
42
+ - 夜间自动发布机制优化为双基线检测,手动补丁不再抹除同周期的功能信号。
43
+ - GitHub Actions CI 优化执行策略,合并任务并跳过纯文档变更,有效降低私有仓库分钟消耗。
44
+
45
+ ### 修复
46
+
47
+ - 修复 Markdown 用例追踪映射将 HTTP 状态码等数字误识别为用例 ID,以及无法正确识别带返回类型标注的测试函数的问题。
48
+ - 修复 Markdown 用例门禁误拒绝合法的列表断言,以及无法正确解析相对路径绑定文件的问题。
49
+ - 修复 Pi CLI 执行器将单个节点完整标准输出累积为单一字符串,导致长文本输出时内存溢出或卡死的问题。
50
+ - 修复 Windows 环境下因清理环境变量导致 Python 和 pytest 无法正确发现用户级依赖的问题。
51
+ - 修复初始化演进的高影响校验未覆盖专用路径,导致必要审查被静默跳过的问题。
52
+
5
53
  ## [0.17.2] - 2026-07-22
6
54
 
7
55
  ### 改进
@@ -47,74 +95,6 @@
47
95
  - tolerate mock strategy format drift
48
96
  - bound executor output
49
97
 
50
- ## [Unreleased]
51
-
52
- - 将 `backend-test` runtime 与打包模板收敛为报告优先的 12 节点 DAG:case review 改为 advisory evidence,移除 pytest semantic review/gates 与最终 outcome gate,保留 pytest 前确定性 traceability hard gate、单次 pytest、canonical context/retrospective/L-5;Analysis v2 对 string evidence gap 与 `id` alias 做窄 canonicalize,同时继续严格拒绝未知键、错类型、secret 与多 JSON envelope。
53
-
54
- ### 修复
55
-
56
- - DAG runtime 的 task-contract 启动门禁现在只将 `writePolicy: "exclusive"` 视为 writer execution:历史只读/static/shell DAG 可继续执行,legacy writer DAG 仍必须升级为 DagSpec v4 并绑定 managed Task Contract;真实 DAG 生成会冻结当前 managed binding。测试执行 fixture、Worker materializer 和 Windows npm/tmp 路径也已同步适配该契约。
57
- - Pi CLI / SDK executor 不再把单个节点的完整 stdout 累积为一个 JS 字符串;长 `frontend-plan-pi` 或大段结构化输出会保留有界预览并继续流式解析最终 assistant 事件,避免触发 V8 `Invalid string length` 或卡死。
58
- - DAG 节点新增 `outputMode: "structured-required"`;前端实现计划/修订节点显式使用该模式,并在 `output-too-large` 时用紧凑结构化输出指令重试,避免直接截断计划后继续执行,也减少长计划输出阻断后续流程的概率。
59
- - `frontend-mock-assess-pi` 的 `MOCK_STRATEGY:` 协议行规范化现在可处理列表、引用、inline code、中文冒号、`Mock strategy` 标签和标签/值分行等常见格式漂移;仍只接受明确 Mock strategy 标签和五态白名单,不从普通正文猜默认策略。
60
-
61
- ### 改进
62
-
63
- - **Console Phase 3(Observe 协作)**:
64
- - Observe `GET /api/health` 升级为 versioned DTO(`schemaVersion: 1`、`repoFingerprint` 复用 Console `repoFingerprintV1`、package name/version、由 ROUTES 派生的 `routeCapabilities`)。
65
- - Console `observeLink` fail-closed:仅 health **match**(同仓 fingerprint + schema + 目标 kind 所需 capability)时返回深链;offline / mismatch 返回诊断与 `agent-worker observe serve --repo . --port 8787`,不打开错误 URL。
66
- - Doctor 的 Observe 探测扩展 match / mismatch / unavailable(Observe 仍非阻塞,doctor `ok` 保持)。
67
- - Recovery CTA 矩阵(report / doctor / decision / resume / reconcile / regenerate / observeLink):**无** Cancel、**无**主 CTA「直接改代码」。
68
- - Console 与 Observe 仍为独立进程;不 mount、不 proxy、不复制第二套 read model。
69
- - Phase 1 Console skeleton:`agent-worker console serve|doctor`(默认 loopback `127.0.0.1:8790`、非 loopback fail-closed)、Vite SPA 静态产物 + Node health/static server、boot capability cookie + mutation Host/Origin 门禁、封闭 ResourceLoader 骨架、sibling `loop-agent` identity doctor、`repoFingerprintV1` / Observe 深链 builder、以及 `UPSTREAM.md`(pi-web 源 commit)与预算 report。
70
- - Phase 0.5 operator surface repair:`buildOperatorCapabilitiesDocument()` 在权威 `src/shared/operator` 上补齐 Console 协商块 `dag`(`generatedSpecVersion: 4`、`requiresTaskContractBinding: true`,与 `dagSpecVersions` 同源常量),并删除冲突的 `src/task/operator` 双实现;CLI `--json` 继续写 `process.stdout`,测试侧改为捕获 stdout。
71
- - Nightly 自动发布改为双基线:`lastTag`(最新稳定 `vX.Y.Z`)只负责未发布增量、breaking 检测与 release notes;当前 minor 的 `vX.Y.0`(缺失则回退 `lastTag`)只累计严格 Conventional Commit `feat:` 以决定 minor/patch。手动 patch 不再抹掉同 minor 周期的 feature 信号;历史 minor 窗口中的 breaking 不再永久阻断后续自动发布。`Publish npm` 手动入口未改。
72
-
73
- - `frontend-implementation` 绿色路径由固定全链改为按需分支:删除重复的首轮 design shell gate,初审通过直接复用原计划,只有 `request-revision` 才执行计划修订与终审;只有可修复失败才执行 repair/reverify/retrace。常规全绿路径因此少执行 3 个 Pi 与 4 个 shell 节点,同时保留 pass-only 写入门禁、验证/评审证据和只读 `frontend-closeout-pi`。原计划/修订计划的结构化合同与需求覆盖使用同一有效来源,且只有主分支产物缺失时才回退;已存在但 malformed/schema-invalid 的产物继续 fail-closed。
74
- - Observe 节点检查器“规范证据”页签新增安全文件预览:已绑定任务源和已读取规范文件条目现以可聚焦按钮呈现,点击后在检查器内下钻查看内容(不新增弹窗),提供“返回规范证据”按钮并恢复触发行焦点。新增只读接口 `GET /api/dag-runs/:runId/nodes/:nodeId/spec-evidence/file?source=binding|read&path=...`,服务端重新校验证据成员身份(binding 必须在 `sourceBinding.sources`,read 必须有成功配对 read),拒 `..`/绝对/目录/二进制/仓库外符号链接;正文上限 64KB 并返回截断标志,统一走 Observe 脱敏规则;binding 返回 `sha256`/`currentSha256`/`hashMatch`,read 返回 session event 中实际读取正文与 `readAt`(不以当前工作区文件替代)。
75
- - 通用 standard/reviewed/supervised 实现 DAG 现在会在目标仓库存在 `AGENTS.md` 时,按本次 writer 的实际变更解析适用的根级/嵌套指令和仓库内代码规范索引:standard 复用 `verify-pi` 与条件 shell gate,reviewed/supervised 复用现有 `review-pi`/review gate,不新增模型节点;无 `AGENTS.md` 或无适用规范时保持原行为,审查节点只读且不会直接改代码。专用测试、前端和知识类 DAG 不受影响。
76
-
77
- - GitHub Actions CI 降低私有仓分钟消耗:合并为单 job(避免两次 `npm ci`)、同分支/`PR` concurrency 取消旧 run、Draft PR 跳过完整 CI,且仅忽略 `docs/reports/**`、`docs/progress/**`、`docs/design/archive/**`、`docs/exec-plans/completed/**` 等纯运营文档路径;治理文档与代码门禁仍全量跑。
78
- - 明确 **Compatibility / Operator Assist**:openCode 等主会话只编排已发布 `loop-agent` / `agent-worker` CLI 与只读诊断,不得绕过 CLI 直接改业务实现,失败只走 doctor / reconcile / human gate / CLI 重跑。`loop-agent init` 写入的 `AGENTS.md` managed block、包内 skills 与 website 快速开始/治理说明已对齐;架构决策见 `docs/decisions/0005-governed-operator-surface.md`(accepted)。已有目标项目用 `loop-agent init check-update` / `init update --apply-safe` 或 `init reconcile` 刷新 managed block。
79
- - 前端测试 DAG 收紧体验:execution preflight 仅硬校验绝对非生产 `baseUrl`;用例 map 缩短为优先用 `playwright-cli` 执行;复盘合并执行证据审查且不依赖 outcome=pass,失败也能出报告。
80
- - 前端测试用例生成现在会解析明确 `baseUrl`:优先读取任务源 `config.md` 中的前端 URL,缺失时默认 `http://localhost:5173`,并写入 RAG `context.md`;生成命令不得再保留 `<base-url>` 占位符。
81
- - 前端浏览器测试 DAG 的复盘报告现写入 `testcase/frontend/reports/**`,不再要求 `docs/test-reports/**` 权限;任务执行约束可以安全禁止整个 `docs/**`,同时仍保留可审计的测试资产。
82
- - `frontend-test` 结果链新增 run-owned `frontend-test-result-v1`、单次用例修订与终审门禁;只有所有浏览器用例和 AC 覆盖通过且结果合同明确为 real/pass 时,Worker 才会投影真实集成。
83
- - 后端测试 DAG 从 24 个收敛为 15 个真实顶层节点;用例与语义 `request-revision` 由确定性 gate 直接失败,pytest 只执行一次,TestBug 保留分类并转为独立 follow-up,不再触发同 run repair 或 final rerun。唯一 initial Result 会确定性复制为 canonical `contracts/backend-test-result.json`,最终 outcome 只认该结构化事实。
84
- - Backend Test Analysis、Case Manifest、Semantic Review 与 Classification 的严格 JSON 契约进一步对齐,生成节点明确字段白名单、`sourceBinding` 与 evidence gap 约束,避免模型自定义字段导致确定性门禁失败。
85
- - 新增升级后 init surface notifier:普通安全仓库命令结束后,当目标项目存在有效 `.harness/init-surface.json` 且 `controllerVersion` 与当前包版本不同时自动检测。surface 缺失/损坏、源仓库、未初始化目录与版本一致时直接跳过;`checkInitUpdate().ok` 时仅在无活跃运行时静默刷新 state;非 TTY 只向 stderr 输出 `loop-agent init reconcile --repo-root ...` 提示且不写入目标;TTY 且无 human decisions、无活跃 DAG/Worker 时可经明确 `y/yes` 同意后应用 deterministic safe actions;只有 model merge 时直接输出有边界指引,不询问“应用 0 个动作”。notifier 遵循全局 `--repo-root`、不写 stdout、不改变原命令退出码,CI、禁用环境变量、help/version、`--json`/`--markdown`、`init`、`run-dag`、`dag`、`loop`、`delegate`、`pi-prompt`、`cursor-prompt` 均跳过。
86
- - 新增统一命令 `loop-agent init reconcile --repo-root <target>`:surface 缺失返回 `needs-baseline` 且零写入,存在 human decisions 返回 `needs-human-decision` 且零写入,活跃 DAG/Worker 或 Worker 状态无法确认时返回 `blocked-active-runtime` 且零写入;其余情况复用现有 `applyInitUpdate({ applySafe: true })` 并复查返回 `clean`/`needs-model-merge`/`needs-safe-update`,支持 `--json`/`--markdown`。
87
- - 测试报告新增 L-5 指标消费与 Python coverage.py JSON、Java JaCoCo XML 的统一 Code Coverage v1 报告入口,覆盖通过率、AC/自动化覆盖率、稳定性、失败原因、缺陷、风险和回归建议;缺失覆盖率证据保持 `unavailable`,不改变 backend-test outcome gate。
88
- - 现有 backend-test DAG 在 retrospective 之后新增独立的 `l5-metrics-pi` 指标节点;它基于前序测试报告和 run-owned 证据输出 `L-5 ready/not-ready`,不创建独立 L-5 DAG,也不改变 Result v1 outcome gate。
89
- - 后端测试 DAG 从 38 个收敛为 24 个真实顶层节点;使用 fail-closed `runIf` 和复合 Shell capability 减少调度,同时保留双合同、Manifest、语义评审、JUnit、Result、分类、修复安全、追踪和最终 outcome 证据。三条可选修订/修复分支仍各最多执行一次。
90
- - Backend Test Analysis、Case Manifest、Semantic Review 与 Classification 的严格 JSON 契约进一步对齐,生成和修订节点明确字段白名单、`sourceBinding` 与 evidence gap 约束,避免模型自定义字段导致确定性门禁失败。
91
-
92
- ### 修复
93
-
94
- - Windows DAG Shell/verification 的 clean environment 现在保留 `APPDATA`,使 Python 3.12/pip 可按系统规则发现用户级 `site-packages` 与其中的 pytest;仍不会继承完整宿主环境或透传未授权 token/credential,backend-test 无需硬编码用户路径。
95
- - Observe 节点检查器的“规范证据”现在会按任务目录解析 `sourceBinding` 中的相对路径,`source/需求.md` 等已绑定文件可正常打开;路径成员校验、目录穿越、绝对路径、二进制文件和符号链接逃逸防护保持不变。
96
- - Decision Gate 的 Pi 执行指令不再与输出合同冲突:启用 `decisionGate` 的只读节点会明确输出唯一的 `DECISION_ENVELOPE_JSON` fenced block,普通只读节点仍保持原有无代码围栏约束,严格解析规则不放宽。
97
-
98
- - Backend-test Analysis v2 intake 现在可确定性归一化模型常见的外部依赖 `type` / `target` 与风险 `type` 字段:dependency `target` 映射为 canonical `name`,扩展字段不落盘;dependency/risk 仍使用类别化白名单,未知字段和 `risks[].target` 继续 fail-closed,避免节点 02 因合法近似形态过早中断。
99
-
100
- - Backend-test 唯一 JSON object 提取器在扫描裸对象前会等长忽略完整 Markdown 行内代码中的对象形状示例(如 `{error:{code,message,type}}`);唯一真实 envelope 可正常物化,多个真实 JSON、fence 外额外对象与未闭合对象继续 fail-closed。
101
- - Backend-test 节点 10 `backend-test-semantic-gate-shell` 不再用 Markdown `first-verdict-line` 读取 raw Pi 输出;改为同一 `semantic-initial` pipeline 按 task id 只读 `contracts/backend-test-semantic-review.json`,仅 `verdict=pass` 放行 pytest,`request-revision`/缺失/malformed 清晰 fail-closed;节点 09 仍只负责 materialize+trace,15 节点与单次 pytest 不变。
102
- - 公开 Case Manifest schema(`docs/templates/backend-test-case-manifest.schema.json`)对齐 materializer dual-scope coverage:`coverageSummary` 在 `additionalProperties:false` 下 required 并声明 `global`/`inScope`(`total`/`covered`/`gapped`/`ratio`)与 `crossDomainGapCount`;回归测试以无依赖轻量 contract 锁定 schema↔canonical 一致性;不改 materializer/15 节点。
103
- - Backend-test Case Manifest materializer 将模型顶层 `coverageSummary` 视为非权威:secret 扫描后剥离,再 strict parse 其余字段,并始终 `computeCaseManifestCoverageSummary` 写入 canonical summary;my-webapp 自定义/部分 summary 不再导致 invalid-output;prompt 禁止模型输出 coverageSummary;15 节点与单次 pytest 不变。
104
- - Backend-test 裸 object 路径现消费 scanner 的 `unclosedObject`:当文本含一个完整裸 JSON object,且其前或后仍有未闭合裸 object 时一律 fail-closed,不再因 `spans.length === 1` 错误接受完整对象;Semantic Review / Classification / contracts envelope 共用该行为。
105
- - Backend-test contracts composite pipeline (`validate-backend-test-contracts-shell`) 使用唯一确定性 envelope 提取:接受 pure JSON、唯一 fenced json、prose+唯一 fence、prose+唯一括号平衡裸 object;对无 JSON、多候选、未闭合、非 object、缺 `analysis`/`execution` fail-closed。提取后仍走 Analysis v2 / Execution v1 schema、source-binding 与 secret 门禁;不引入全局 Pi output rewrite,不将 `invalid-output` 加入自动 retry,不改 15 节点拓扑。
106
- - Backend-test Semantic Review(08→09)与 Classification(12→13)materializer 复用同一 `extractUniqueJsonObject` scanner:接受 pure / whole-fence / prose+唯一 fence / prose+唯一裸 object;多候选、未闭合、非 object 与 strict schema 错误继续 fail-closed;不改 Case Manifest、Pi SDK/retry 或 15 节点拓扑。
107
- - 节点检查器「执行过程」中的事件时间戳现在同时显示本地日期与 24 小时制 `HH:mm:ss`(例如 `2026/7/16 08:09:10`),不再被裁掉时间部分;缺少时间戳时仍显示「未记录时间」,事件字段优先级(`timestamp` → `at` → `recordedAt`)保持不变。
108
- - `frontend-test` 的 case review 现在是 fail-closed browser gate:只有 `VERDICT: pass` 才能物化 manifest 并启动动态 browser map;生成的 case 固定使用默认 browser session,要求每个子场景的 fixture/UI reset 和 fresh snapshot,并由确定性节点校验每个 case 的 `execution.md`、`case-result.json`、`caseId`、`status`、`evidencePaths` 及 blocked `blockedReason`。
109
- - 前端实现计划/修订节点会注入当前包内权威 `frontend-implementation-contract-v1` Schema 与固定 source binding,避免模型猜测字段导致契约门禁失败。
110
- - 前端 Mock 策略节点的 canonical 输出会把首条 `MOCK_STRATEGY:` 协议行提升为第一行,避免解释性前言触发 `first-non-empty` 门禁误判。
111
- - 前端规范回退目录统一为本地 `openspec/`,DAG 能力发现、提示词、Skill 与验证证据检查不再查找大小写不一致的旧目录名。
112
- - 后端测试复合 Shell pipeline 现在与普通 Shell 节点共享 Git write guard;即使命令退出成功,只要越过 `read-only`、`allowedPaths` 或 `forbiddenPaths` 边界,节点仍会 fail-closed。
113
- - Observe 对新 backend-test run 不再伪造 repair attempt/status,只投影 single-run/canonical Result、分类与 coverage;历史 24 节点 run 的 eligibility、repair、final 和 effective evidence 仍可兼容读取。
114
- - Pi SDK 对缺少响应 ID 的累计 Token 生命周期事件改为取本次执行最大快照,避免同一响应的匿名 usage 被重复累加。
115
- - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
116
- - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为唯一 JUnit/initial Result、canonical Result、traceability 与 Observe 投影保留结构化运行证据。
117
-
118
98
  ## [0.16.26] - 2026-07-22
119
99
 
120
100
  ### 修复
@@ -2,4 +2,5 @@ import { auditDocs } from '../governance/checks.js';
2
2
  export async function runDocsAudit(repoRoot) {
3
3
  const result = await auditDocs(repoRoot);
4
4
  console.log(JSON.stringify(result, null, 2));
5
+ process.exitCode = result.summary.errorCount > 0 ? 1 : 0;
5
6
  }
@@ -14,7 +14,8 @@ import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "
14
14
  import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
15
15
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
16
16
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
17
- import { materializeBackendTestResultFromRunDir } from "../workflows/dag/backend-test-result-contract.js";
17
+ 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";
18
19
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
19
20
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
20
21
  import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
@@ -308,7 +309,66 @@ async function executeBackendTestPipeline(input, meta) {
308
309
  if (!pipeline)
309
310
  throw new Error("missing backend-test pipeline id");
310
311
  const outputs = [];
311
- if (pipeline === "contracts") {
312
+ if (pipeline === "markdown-environment") {
313
+ const results = await executePipelineCommands(input, meta);
314
+ if (!results.every((result) => result.ok)) {
315
+ const failure = results.find((result) => !result.ok);
316
+ return { ok: false, stdout: results.map((result) => result.stdout).join("\n"), stderr: failure.stderr, failureCategory: failure.failureCategory, durationMs: Date.now() - started };
317
+ }
318
+ const environment = await inspectBackendTestEnvironment({
319
+ workspaceRoot: input.cwd,
320
+ pythonVersion: results[0]?.stdout ?? "unknown",
321
+ pytestVersion: results[1]?.stdout ?? "unknown",
322
+ pytestHelp: results[2]?.stdout ?? "",
323
+ });
324
+ const reportPath = await writeRunReport(meta.runDir, "backend-test-environment.md", environment.markdown);
325
+ outputs.push(`environment=${reportPath}`, environment.markdown);
326
+ }
327
+ else if (pipeline === "markdown-cases") {
328
+ const environmentMarkdown = await readFile(path.join(meta.runDir, "reports", "backend-test-environment.md"), "utf8");
329
+ const requiredRequirementIds = requiredBackendMarkdownCaseAcIds([
330
+ meta.spec.objective ?? "",
331
+ ...(meta.spec.successCriteria ?? []),
332
+ ]);
333
+ const report = await validateBackendMarkdownCases({
334
+ workspaceRoot: input.cwd,
335
+ environmentMarkdown,
336
+ requiredRequirementIds,
337
+ sourceBinding: meta.spec.sourceBinding,
338
+ });
339
+ const reportPath = await writeRunReport(meta.runDir, "backend-md-case-validation.md", report);
340
+ outputs.push(`caseValidation=${reportPath}`, report);
341
+ }
342
+ else if (pipeline === "markdown-traceability") {
343
+ const report = await validateBackendMarkdownTraceability(input.cwd);
344
+ const reportPath = await writeRunReport(meta.runDir, "backend-test-traceability.md", report);
345
+ outputs.push(`traceability=${reportPath}`, report);
346
+ }
347
+ else if (pipeline === "markdown-execute-html") {
348
+ const results = await executePipelineCommands(input, meta);
349
+ if (!results.every((result) => result.ok)) {
350
+ const failure = results.find((result) => !result.ok);
351
+ return { ok: false, stdout: results.map((result) => result.stdout).join("\n"), stderr: failure.stderr, failureCategory: failure.failureCategory, durationMs: Date.now() - started };
352
+ }
353
+ const reportsDir = path.join(meta.runDir, "reports");
354
+ const junitContent = await readFile(path.join(reportsDir, "backend-test.junit.xml"), "utf8");
355
+ const pytestExitCode = Number.parseInt((await readFile(path.join(reportsDir, "backend-test-pytest-exit.txt"), "utf8")).trim(), 10);
356
+ if (![0, 1].includes(pytestExitCode))
357
+ throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
358
+ const parsed = parseJunitXml(junitContent);
359
+ const htmlContent = renderBackendTestHtml({
360
+ title: meta.spec.title,
361
+ parsed,
362
+ environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8"),
363
+ traceabilitySummary: await readFile(path.join(reportsDir, "backend-test-traceability.md"), "utf8"),
364
+ });
365
+ 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 });
367
+ const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
368
+ const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
369
+ outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `facts=${factsPath}`, facts);
370
+ }
371
+ else if (pipeline === "contracts") {
312
372
  const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
313
373
  const wrapper = JSON.parse(await readFile(wrapperPath, "utf8"));
314
374
  const raw = wrapper.assistantText?.trim() || wrapper.stdout?.trim() || "";
@@ -85,10 +85,13 @@ export async function runSpineAudit(repoRoot, taskId) {
85
85
  }
86
86
  const requirementCoverage = await runRequirementCoverageAudit(repoRoot, taskId);
87
87
  for (const reason of requirementCoverageFailureReasons(requirementCoverage)) {
88
+ const backendTestDeferred = config.taskKind === "backend-test";
88
89
  findings.push({
89
- severity: "error",
90
+ severity: backendTestDeferred ? "warning" : "error",
90
91
  type: "uncovered-requirement",
91
- message: reason,
92
+ message: backendTestDeferred
93
+ ? `${reason}; deferred to backend-test Markdown validation and traceability gates`
94
+ : reason,
92
95
  });
93
96
  }
94
97
  const adapter = await resolveAdapter(repoRoot);
@@ -0,0 +1,148 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const MAX_SOURCE_EXCERPT_CHARS = 2000;
5
+ export const BACKEND_TEST_DISCOVERY_EXCLUDED_DIRECTORIES = [
6
+ ".git",
7
+ ".harness",
8
+ ".env",
9
+ "node_modules",
10
+ "vendor",
11
+ "dist",
12
+ "build",
13
+ "coverage",
14
+ ".venv",
15
+ "venv",
16
+ "__pycache__",
17
+ ];
18
+ const DISCOVERY_CANDIDATES = {
19
+ pytestConfig: ["pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini"],
20
+ conftest: [
21
+ "conftest.py",
22
+ "tests/conftest.py",
23
+ "test/conftest.py",
24
+ "testcase/conftest.py",
25
+ ],
26
+ testRoot: ["tests", "test", "testcase", "src/tests", "app/tests"],
27
+ package: [
28
+ "package.json",
29
+ "pyproject.toml",
30
+ "requirements.txt",
31
+ "requirements-dev.txt",
32
+ "Pipfile",
33
+ "poetry.lock",
34
+ ],
35
+ serverEntry: [
36
+ "server.py",
37
+ "app.py",
38
+ "main.py",
39
+ "manage.py",
40
+ "src/server.py",
41
+ "src/main.py",
42
+ "app/main.py",
43
+ "server.js",
44
+ "server.ts",
45
+ "src/server.js",
46
+ "src/server.ts",
47
+ ],
48
+ };
49
+ function taskRelative(sources, filePath) {
50
+ return path.relative(sources.taskDir, filePath).replaceAll(path.sep, "/");
51
+ }
52
+ function repoRelativeReadPath(sources, filePath) {
53
+ if (!sources.repoRoot) {
54
+ return filePath.replaceAll(path.sep, "/");
55
+ }
56
+ const relative = path.relative(sources.repoRoot, filePath);
57
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
58
+ throw new Error(`backend-test reference escapes repository root: ${filePath}`);
59
+ }
60
+ return relative.replaceAll(path.sep, "/");
61
+ }
62
+ function boundedExcerpt(markdown, sourcePath) {
63
+ const trimmed = markdown.trim();
64
+ if (trimmed.length <= MAX_SOURCE_EXCERPT_CHARS)
65
+ return trimmed;
66
+ const omitted = trimmed.length - MAX_SOURCE_EXCERPT_CHARS;
67
+ return `${trimmed.slice(0, MAX_SOURCE_EXCERPT_CHARS)}\n\n...[truncated for backend-test intake: showing first ${MAX_SOURCE_EXCERPT_CHARS} of ${trimmed.length} chars; omitted ${omitted} chars]\nFull derived source: ${sourcePath}`;
68
+ }
69
+ function inferRoleHints(referencePath) {
70
+ const normalized = referencePath.toLowerCase();
71
+ const hints = [];
72
+ const add = (hint) => {
73
+ if (!hints.includes(hint))
74
+ hints.push(hint);
75
+ };
76
+ if (/(acceptance|验收|criteria|ac[._-])/.test(normalized))
77
+ add("acceptance-criteria");
78
+ if (/(api|openapi|swagger|endpoint|接口)/.test(normalized))
79
+ add("api-contract");
80
+ if (/(schema|model|field|字段|数据)/.test(normalized))
81
+ add("data-contract");
82
+ if (/(rule|constraint|policy|规则|约束)/.test(normalized))
83
+ add("business-rule");
84
+ if (/(test|pytest|测试)/.test(normalized))
85
+ add("test-evidence");
86
+ if (hints.length === 0)
87
+ add("source-fact");
88
+ return hints;
89
+ }
90
+ async function discoverCandidatePaths(repoRoot) {
91
+ const candidates = {
92
+ pytestConfig: [],
93
+ conftest: [],
94
+ testRoot: [],
95
+ package: [],
96
+ serverEntry: [],
97
+ };
98
+ if (!repoRoot) {
99
+ return {
100
+ mode: "deterministic-fallback",
101
+ candidates,
102
+ excludedDirectories: [...BACKEND_TEST_DISCOVERY_EXCLUDED_DIRECTORIES],
103
+ };
104
+ }
105
+ for (const [kind, paths] of Object.entries(DISCOVERY_CANDIDATES)) {
106
+ for (const candidate of paths) {
107
+ try {
108
+ await access(path.join(repoRoot, candidate));
109
+ candidates[kind].push(candidate);
110
+ }
111
+ catch {
112
+ // A missing bounded candidate is not evidence and is omitted.
113
+ }
114
+ }
115
+ }
116
+ return {
117
+ mode: "filesystem",
118
+ candidates,
119
+ excludedDirectories: [...BACKEND_TEST_DISCOVERY_EXCLUDED_DIRECTORIES],
120
+ };
121
+ }
122
+ export async function buildBackendTestIntakeContext(sources) {
123
+ const requirementPath = taskRelative(sources, sources.requirementPath);
124
+ const constraintPath = taskRelative(sources, sources.constraintPath);
125
+ const parts = [
126
+ "## Derived task contract: 需求.md",
127
+ boundedExcerpt(sources.requirementMarkdown, requirementPath),
128
+ ];
129
+ if (sources.constraintMarkdown) {
130
+ parts.push("## Derived execution constraints: 执行约束.md", boundedExcerpt(sources.constraintMarkdown, constraintPath));
131
+ }
132
+ const referenceIndex = (sources.referenceDocuments ?? [])
133
+ .map((reference) => {
134
+ const relativePath = taskRelative(sources, reference.path);
135
+ return {
136
+ path: relativePath,
137
+ readPath: repoRelativeReadPath(sources, reference.path),
138
+ sha256: createHash("sha256").update(reference.markdown, "utf8").digest("hex"),
139
+ roleHints: inferRoleHints(relativePath),
140
+ };
141
+ })
142
+ .sort((left, right) => left.path.localeCompare(right.path));
143
+ return {
144
+ boundedSourceContext: parts.join("\n\n"),
145
+ referenceIndex,
146
+ projectDiscoverySeed: await discoverCandidatePaths(sources.repoRoot),
147
+ };
148
+ }