@tea-agent/loop-agent 0.17.1 → 0.18.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 (44) hide show
  1. package/CHANGELOG.md +96 -22
  2. package/dist/application/dag/args.js +9 -2
  3. package/dist/executors/dag-pi-executor.js +11 -0
  4. package/dist/executors/shell-executor.js +62 -2
  5. package/dist/governance/spine-audit.js +5 -2
  6. package/dist/worker/console/draft-store.js +81 -3
  7. package/dist/worker/console/interview/grill-me.js +253 -0
  8. package/dist/worker/console/operation-runner.js +2 -1
  9. package/dist/worker/console/operator-actions.js +609 -20
  10. package/dist/worker/console/pi-readiness.js +2 -0
  11. package/dist/worker/console/prd-identity.js +102 -0
  12. package/dist/worker/console/resolve-dag-run-for-task.js +76 -0
  13. package/dist/worker/console/server.js +11 -2
  14. package/dist/worker/console/static/assets/index-KUSib7aM.js +16 -0
  15. package/dist/worker/console/static/assets/index-ucIzpaGJ.css +1 -0
  16. package/dist/worker/console/static/index.html +3 -3
  17. package/dist/worker/console/workflow-kinds.js +46 -0
  18. package/dist/worker/observe/routes.js +8 -1
  19. package/dist/worker/observe/static/index.html +8 -8
  20. package/dist/worker/observe/static/styles.css +296 -181
  21. package/dist/workflows/dag/backend-test-intake-context.js +148 -0
  22. package/dist/workflows/dag/backend-test-markdown-workflow.js +528 -0
  23. package/dist/workflows/dag/init-hybrid.js +93 -109
  24. package/dist/workflows/dag/types.js +4 -0
  25. package/dist/workflows/loop/benchmark.js +1 -1
  26. package/docs/README.md +84 -39
  27. package/docs/architecture/README.md +8 -4
  28. package/docs/architecture/dag-execution.md +8 -1
  29. package/docs/architecture/evolution.md +38 -25
  30. package/docs/architecture/facts-and-state.md +2 -0
  31. package/docs/architecture/system-overview.md +6 -5
  32. package/docs/architecture/worker-and-feature.md +15 -8
  33. package/docs/init-surface.manifest.json +10 -1
  34. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +1 -1
  35. package/docs/templates/backend-test-dag.json +74 -219
  36. package/docs/templates/branch-merge-report.md +1 -1
  37. package/docs/templates/exec-plan.md +2 -0
  38. package/docs/templates/progress-log.md +3 -0
  39. package/package.json +1 -1
  40. package/skills/init-capability-evolution/SKILL.md +1 -1
  41. package/skills/loop-agent/references/command-reference.md +1 -1
  42. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  43. package/dist/worker/console/static/assets/index-BEIdBogJ.js +0 -9
  44. package/dist/worker/console/static/assets/index-Rt0TqimP.css +0 -1
package/CHANGELOG.md CHANGED
@@ -1,32 +1,22 @@
1
1
  # 更新日志
2
2
 
3
- ## [0.17.1] - 2026-07-22
4
-
5
- ### 修复
6
-
7
- - TaskSpec materialize 在写入 task.json、需求和约束后会自动 adopt 或刷新 managed Task Contract,避免生成 v4 DAG 后立刻因 binding drift 无法 validate/run。
8
- - Operator Console 的 Pi readiness 改为真实探测 SDK、认证和可用模型;本机已配置 Pi 时不再误报 `setup-required`,界面同步对齐 Observe 暖白画布。
9
-
10
- ## [0.17.0] - 2026-07-22
11
-
12
- ### 新增
13
-
14
- - streamline report-first workflow (#53)
15
- - add operator console, task contract, and harden bindings (#51)
16
- - condition DAG design and repair branches
17
-
18
- ### 修复
19
-
20
- - synchronize console frontend lockfile (#52)
21
- - tolerate mock strategy format drift
22
- - bound executor output
23
-
24
3
  ## [Unreleased]
25
4
 
26
- - `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。
5
+ - `backend-test` active runtime/template 改为 8 节点 Markdown-first 流程:环境 Shell 先检查 Python/pytest、配置/fixture/target HTML renderer;随后生成并独立 Review Markdown 用例,经过确定性 Markdown/traceability gate 后只执行一次 pytest,同时生成 JUnitself-contained HTML Markdown facts,最终输出 Markdown 测试报告与 L-5。新流程不再要求模型生成 Analysis/Execution/Manifest/Classification/Result/L-5 业务 JSON,历史 JSON artifacts 仍保留读取兼容。
27
6
 
28
7
  ### 修复
29
8
 
9
+ - `backend-test` Markdown 用例生成与独立审查现在默认采用简体中文:用例标题、策略、前置条件、测试数据、步骤、预期结果、自动化说明、表头和证据缺口优先中文表达;Case/AC/REQ/BR ID、固定章节标题、HTTP 方法、路径、字段名、枚举值、命令和代码符号继续保持精确机器契约,不做无意义双语翻译。
10
+ - `backend-test` traceability 现在识别带 `-> None` 等返回类型 annotation 以及可选 `async` 的 pytest 函数,不再把合法 typed test function 全部误报为缺失映射。
11
+ - `backend-test` Markdown gate 只要求 case-level AC;pytest 执行、MD↔pytest 追溯、JUnit/HTML/stdout/stderr 和失败分类等下游证据 AC 交给 traceability、execute 与 report 节点,不再被错误要求写入单条 Markdown case。
12
+ - `backend-test` Markdown gate 的 `Steps` 仍要求编号列表;`Expected Results` 现在接受编号列表或 Markdown bullet 列表,只要保持结构化可断言,不再误拒绝模型生成的合法 bullet 断言;普通散文仍 fail-closed。
13
+ - `backend-test` 的 pre-run Spine 审计不再要求尚未执行的 `artifacts/requirement-coverage.json`:未覆盖需求在该 taskKind 下记为 warning,并交给运行内 Markdown validation 与 traceability gate 确定性阻断;普通实现任务仍保持 error/fail-closed。
14
+ - `backend-test` Markdown 用例门禁现在从 DAG `sourceBinding` 精确识别 `Source References`,支持反引号以及中英文括号/标点,不再将说明文字吞入路径;未绑定引用、路径前缀伪装和绑定文件缺失仍然 fail-closed。
15
+ - `backend-test` Markdown writer/reviewer 的 reference index 现在同时提供 task-relative 引用路径与 repo-relative `readPath`,Pi 会直接只读 `.harness/tasks/<taskId>/source/**` 的绑定文件,不再把 `source/references/**` 误读为仓库根路径,也不会在读取失败后回退到 `docs/**` 替代文件。
16
+ - `backend-test` Markdown 用例门禁会通过 DAG `sourceBinding` 将 `source/需求.md`、`source/执行约束.md` 和 `source/references/**` 解析到对应任务目录,不再错误地相对仓库根查找,也不会接受未绑定的同名仓库文件。
17
+ - `backend-test` traceability 门禁现在优先用 Markdown 中已知 Case ID 匹配 `test_BE_<MODULE>_<NNN>_` 函数前缀,不再把函数描述中的 `100`、`400`、`404` 等三位数字误识别为 Case ID。
18
+ - `backend-test` traceability 门禁允许仅包裹 cleanup 调用的 `except requests.RequestException: pass`,避免 best-effort 资源清理被误报;bare/宽异常、`AssertionError` 及非 cleanup 主体仍然 fail-closed。
19
+
30
20
  - 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 路径也已同步适配该契约。
31
21
  - Pi CLI / SDK executor 不再把单个节点的完整 stdout 累积为一个 JS 字符串;长 `frontend-plan-pi` 或大段结构化输出会保留有界预览并继续流式解析最终 assistant 事件,避免触发 V8 `Invalid string length` 或卡死。
32
22
  - DAG 节点新增 `outputMode: "structured-required"`;前端实现计划/修订节点显式使用该模式,并在 `output-too-large` 时用紧凑结构化输出指令重试,避免直接截断计划后继续执行,也减少长计划输出阻断后续流程的概率。
@@ -34,6 +24,8 @@
34
24
 
35
25
  ### 改进
36
26
 
27
+ - `backend-test` 的 source intake 能力已并入 Markdown 用例生成/Review:派生 `需求.md` / `执行约束.md` 有界内联,reference 使用 path/SHA-256/角色线索索引并按需精确读取;pytest writer 不再读取 `source/**`。
28
+
37
29
  - **Console Phase 3(Observe 协作)**:
38
30
  - Observe `GET /api/health` 升级为 versioned DTO(`schemaVersion: 1`、`repoFingerprint` 复用 Console `repoFingerprintV1`、package name/version、由 ROUTES 派生的 `routeCapabilities`)。
39
31
  - Console `observeLink` fail-closed:仅 health **match**(同仓 fingerprint + schema + 目标 kind 所需 capability)时返回深链;offline / mismatch 返回诊断与 `agent-worker observe serve --repo . --port 8787`,不打开错误 URL。
@@ -42,6 +34,7 @@
42
34
  - Console 与 Observe 仍为独立进程;不 mount、不 proxy、不复制第二套 read model。
43
35
  - 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。
44
36
  - 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。
37
+ - 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` 手动入口未改。
45
38
 
46
39
  - `frontend-implementation` 绿色路径由固定全链改为按需分支:删除重复的首轮 design shell gate,初审通过直接复用原计划,只有 `request-revision` 才执行计划修订与终审;只有可修复失败才执行 repair/reverify/retrace。常规全绿路径因此少执行 3 个 Pi 与 4 个 shell 节点,同时保留 pass-only 写入门禁、验证/评审证据和只读 `frontend-closeout-pi`。原计划/修订计划的结构化合同与需求覆盖使用同一有效来源,且只有主分支产物缺失时才回退;已存在但 malformed/schema-invalid 的产物继续 fail-closed。
47
40
  - 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`(不以当前工作区文件替代)。
@@ -64,7 +57,11 @@
64
57
 
65
58
  ### 修复
66
59
 
60
+ - Init evolution 高影响校验现在覆盖升级后的 init surface notifier、eligibility policy、runtime activity gate 与 CLI entry 接线;这些专用路径单独变化时会触发 `model-review`,不再以 `no trigger matched` 静默跳过审查,同时不扩大到通用 CLI program/command registry 文件。
61
+
67
62
  - Windows DAG Shell/verification 的 clean environment 现在保留 `APPDATA`,使 Python 3.12/pip 可按系统规则发现用户级 `site-packages` 与其中的 pytest;仍不会继承完整宿主环境或透传未授权 token/credential,backend-test 无需硬编码用户路径。
63
+ - Observe 节点检查器的“规范证据”现在会按任务目录解析 `sourceBinding` 中的相对路径,`source/需求.md` 等已绑定文件可正常打开;路径成员校验、目录穿越、绝对路径、二进制文件和符号链接逃逸防护保持不变。
64
+ - Decision Gate 的 Pi 执行指令不再与输出合同冲突:启用 `decisionGate` 的只读节点会明确输出唯一的 `DECISION_ENVELOPE_JSON` fenced block,普通只读节点仍保持原有无代码围栏约束,严格解析规则不放宽。
68
65
 
69
66
  - Backend-test Analysis v2 intake 现在可确定性归一化模型常见的外部依赖 `type` / `target` 与风险 `type` 字段:dependency `target` 映射为 canonical `name`,扩展字段不落盘;dependency/risk 仍使用类别化白名单,未知字段和 `risks[].target` 继续 fail-closed,避免节点 02 因合法近似形态过早中断。
70
67
 
@@ -86,6 +83,83 @@
86
83
  - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
87
84
  - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为唯一 JUnit/initial Result、canonical Result、traceability 与 Observe 投影保留结构化运行证据。
88
85
 
86
+ ## [0.18.0] - 2026-07-22
87
+
88
+ ### 重点更新
89
+
90
+ - 后端测试全面采用 Markdown 优先工作流,整合环境预检、独立审查与确定性门禁,单次执行即可生成完整测试报告。
91
+ - 前端实现与测试流程大幅优化,按需执行计划修订与验证,常规全绿路径执行节点数量显著减少。
92
+ - 修复多项严格校验导致的误中断问题,大幅提升 Markdown 用例生成、追踪映射与异常清理的兼容性。
93
+ - 优化 Pi CLI 与 SDK 执行器,彻底解决长文本输出导致的内存溢出与进程卡死问题。
94
+
95
+ ### 新增
96
+
97
+ - 引入 Markdown 优先的后端测试工作流,支持环境预检、独立审查用例并生成自包含的 HTML 与 Markdown 报告。
98
+ - 新增升级后的初始化面通知器,在检测到版本不一致时自动提示安全更新或引导人工处理。
99
+ - 新增统一的初始化协调命令,支持安全应用更新并智能处理各类阻断情况。
100
+ - 测试报告新增 L-5 指标消费与统一的代码覆盖率报告入口,支持 Python 与 Java 测试数据。
101
+
102
+ ### 改进
103
+
104
+ - 前端实现绿色路径改为按需分支执行,常规无错路径可少执行多个节点,提升执行效率。
105
+ - 前端测试用例生成现可自动解析明确的 baseUrl,不再保留占位符,避免执行异常。
106
+ - 前端测试复盘报告写入路径调整,支持安全禁止整个 docs 目录的写入权限。
107
+ - 夜间自动发布机制优化为双基线检测,手动补丁不再抹除同周期的功能信号。
108
+ - GitHub Actions CI 优化执行策略,合并任务并跳过纯文档变更,有效降低私有仓库分钟消耗。
109
+
110
+ ### 修复
111
+
112
+ - 修复 Markdown 用例追踪映射将 HTTP 状态码等数字误识别为用例 ID,以及无法正确识别带返回类型标注的测试函数的问题。
113
+ - 修复 Markdown 用例门禁误拒绝合法的列表断言,以及无法正确解析相对路径绑定文件的问题。
114
+ - 修复 Pi CLI 执行器将单个节点完整标准输出累积为单一字符串,导致长文本输出时内存溢出或卡死的问题。
115
+ - 修复 Windows 环境下因清理环境变量导致 Python 和 pytest 无法正确发现用户级依赖的问题。
116
+ - 修复初始化演进的高影响校验未覆盖专用路径,导致必要审查被静默跳过的问题。
117
+
118
+ ## [0.17.2] - 2026-07-22
119
+
120
+ ### 改进
121
+
122
+ - Operator Console Happy Path 补齐:主导航(任务/恢复/模型与认证/设置)、newTask / importPrd、grill-me Interview(一次一问 + assessment)、SSE 运行跟随;Pi readiness 可刷新。
123
+ - Operator Console 页面交互文案改为中文(导航、向导步骤、按钮与状态说明);产品名与 CLI 命令仍保留英文术语。
124
+ - Operator Console 界面与交互美化:暖色画布、胶囊导航、步骤进度条、统一按钮层级与状态条,运行面板元信息更清晰。
125
+ - Operator Console Happy Path 增加「流程类型」选择(后端研发 / 前端研发 / 后端测试 / 前端测试),写入 `taskKind`;访谈枚举同步支持 `frontend-implementation` 与 `frontend-test`。
126
+ - Console Happy Path 去掉单独的「创建任务」步骤:导入需求时根据 PRD 标题自动生成任务名称与 canonical taskId,并一键创建任务 + import-prd。
127
+ - Operator Console 需求访谈可跳过:一键按 grill-me 推荐补齐草稿并签发评估,可直接进入契约校验。
128
+ - Operator Console 将「校验契约 / 应用契约 / Spine 审计」合并为「契约落地」:自动确定性校验与差异,人工确认一次应用,成功后自动 Spine。
129
+ - Observe 运行看板视觉对齐 Console:暖色渐变画布、胶囊导航、柔和面板阴影与状态点,减少高饱和色块;进一步统一 badge/表格/风险块/时间线/DAG 图与 inspector、对象关系条与推荐命令等视图组件。
130
+
131
+ ### 修复
132
+
133
+ - Console Draft 在 validate/diff/apply 前会投影为 CLI 严格 schema:补齐 `constraints.invariants`,并去掉仅 Console 使用的 `updatedAt`,避免误报 draft schema validation failed。
134
+ - 修复访谈完成后绑定 `interviewAssessmentRef` 会立刻让 assessment 过期的问题:`draftSha256` 不再纳入该绑定字段;finalize 时先清空 `openQuestions` 再签发 assessment。
135
+ - 修复 Console `contractApply` 默认传入 `expectedObservedHash=unknown` 导致 OBSERVED_HASH_CONFLICT / 页面只见 `exit 1`:改为先 `task contract show` 再传真实 revision/hash;异步操作失败时展示 CLI 错误信息。
136
+ - 修复应用契约后界面卡住:轮询操作结果时误读 `{ ok, operation }` 响应,一直等到超时且无法进入 Spine。
137
+ - 修复 DAG 确认准备失败:不再提交空 stub DAG;`prepareDagConfirmation` 会经 `dag run-task` 生成真实 DAG;`dag validate` 接受 `--json` 兼容 Console。
138
+ - 修复 Console `runDag` 因 `run-dag --json` 未识别而失败:CLI 现接受该旗标(输出本就为 JSON)。
139
+ - 运行 DAG 不再用短超时阻塞整页:`runDag` 改为后台接受 + SSE 跟随;Observe 按钮与任务 busy 解耦,运行中可打开。
140
+ - Console 打开 Observe 时,若只给 taskId,会解析该任务最新 DAG run 并跳转 `#/dag/<runId>`,不再误开 `#/task/<taskId>`。
141
+
142
+ ## [0.17.1] - 2026-07-22
143
+
144
+ ### 修复
145
+
146
+ - TaskSpec materialize 在写入 task.json、需求和约束后会自动 adopt 或刷新 managed Task Contract,避免生成 v4 DAG 后立刻因 binding drift 无法 validate/run。
147
+ - Operator Console 的 Pi readiness 改为真实探测 SDK、认证和可用模型;本机已配置 Pi 时不再误报 `setup-required`,界面同步对齐 Observe 暖白画布。
148
+
149
+ ## [0.17.0] - 2026-07-22
150
+
151
+ ### 新增
152
+
153
+ - streamline report-first workflow (#53)
154
+ - add operator console, task contract, and harden bindings (#51)
155
+ - condition DAG design and repair branches
156
+
157
+ ### 修复
158
+
159
+ - synchronize console frontend lockfile (#52)
160
+ - tolerate mock strategy format drift
161
+ - bound executor output
162
+
89
163
  ## [0.16.26] - 2026-07-22
90
164
 
91
165
  ### 修复
@@ -155,7 +155,7 @@ function parseDagRunTaskProfile(value) {
155
155
  }
156
156
  export function parseDagValidateArgs(args) {
157
157
  if (args.length === 0) {
158
- throw new Error("usage: dag validate --dag <path> [--strict-models] [--strict-governance]");
158
+ throw new Error("usage: dag validate --dag <path> [--strict-models] [--strict-governance] [--json]");
159
159
  }
160
160
  let dagPath;
161
161
  let strictModelMatrix = false;
@@ -191,6 +191,10 @@ export function parseDagValidateArgs(args) {
191
191
  strictSkills = true;
192
192
  continue;
193
193
  }
194
+ if (arg === "--json") {
195
+ // Accepted for operator/console callers; validate always emits JSON stdout.
196
+ continue;
197
+ }
194
198
  if (arg === "--forbid-executor") {
195
199
  const executor = args[++i];
196
200
  if (!executor) {
@@ -243,7 +247,7 @@ export function parseDagValidateArgs(args) {
243
247
  }
244
248
  export function parseRunDagArgs(args, defaultCwd) {
245
249
  if (args.length === 0) {
246
- throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>]");
250
+ throw new Error("usage: run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>] [--json]");
247
251
  }
248
252
  let dagPath;
249
253
  let cwd;
@@ -305,6 +309,9 @@ export function parseRunDagArgs(args, defaultCwd) {
305
309
  else if (arg.startsWith("--events-jsonl=")) {
306
310
  eventsJsonlPath = arg.slice("--events-jsonl=".length);
307
311
  }
312
+ else if (arg === "--json") {
313
+ // Accepted for operator/console callers; run-dag always emits JSON stdout.
314
+ }
308
315
  else if (!arg.startsWith("-")) {
309
316
  if (!dagPath)
310
317
  dagPath = arg;
@@ -140,6 +140,17 @@ export function buildDagPiUserMessage(task, persona, step) {
140
140
  "Do not wrap the output in code fences and do not add conversational preamble.",
141
141
  ].join(" ");
142
142
  }
143
+ // Decision Gate nodes must emit a mandatory DECISION_ENVELOPE_JSON fenced block;
144
+ // do not inject the global no-fence instruction that would conflict with outputContract.
145
+ if (task.decisionGate?.enabled === true) {
146
+ return [
147
+ `You are executing hybrid DAG node "${task.id}" (role=${role}, piPersona=${persona}, piStep=${step}, writePolicy=${writePolicy}).`,
148
+ "The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
149
+ "Use read-only tools only. Do not edit, write, or commit repository files.",
150
+ "Return your conclusion as Markdown that includes exactly one ```DECISION_ENVELOPE_JSON fenced block (info string DECISION_ENVELOPE_JSON, not json) matching the Decision Envelope schema, plus a short evidence/risk summary.",
151
+ "Follow the node outputContract for the mandatory fenced block. Do not add conversational preamble outside the required Markdown structure.",
152
+ ].join(" ");
153
+ }
143
154
  return [
144
155
  `You are executing hybrid DAG node "${task.id}" (role=${role}, piPersona=${persona}, piStep=${step}, writePolicy=${writePolicy}).`,
145
156
  "The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
@@ -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);
@@ -5,11 +5,89 @@ function draftPath(appData, taskId) {
5
5
  return path.join(appData.drafts, `${safe}.json`);
6
6
  }
7
7
  export function computeDraftSha256(draft) {
8
- // Hash a stable projection excluding updatedAt so pure timestamp churn
9
- // does not invalidate assessments still any field change does.
10
- const { updatedAt: _u, ...rest } = draft;
8
+ // Hash a stable projection excluding bookkeeping fields so binding a
9
+ // fresh assessment (interviewAssessmentRef) or timestamp churn does not
10
+ // invalidate the assessment that just certified this Draft content.
11
+ const { updatedAt: _u, interviewAssessmentRef: _ref, ...rest } = draft;
11
12
  return sha256Json(rest);
12
13
  }
14
+ function asStringList(value) {
15
+ if (!Array.isArray(value))
16
+ return [];
17
+ return value
18
+ .map((item) => {
19
+ if (typeof item === "string")
20
+ return item.trim();
21
+ if (item && typeof item === "object") {
22
+ try {
23
+ return JSON.stringify(item);
24
+ }
25
+ catch {
26
+ return String(item);
27
+ }
28
+ }
29
+ return String(item ?? "").trim();
30
+ })
31
+ .filter(Boolean);
32
+ }
33
+ /**
34
+ * Project Console application-data Draft into the strict CLI
35
+ * `taskContractDraftSchema` shape (no updatedAt; invariants required).
36
+ */
37
+ export function toCanonicalDraftForCli(draft) {
38
+ const requirement = draft.requirement ?? {};
39
+ const constraints = draft.constraints ?? {};
40
+ const verification = draft.verification ?? {};
41
+ const canonical = {
42
+ schemaVersion: 1,
43
+ taskId: draft.taskId,
44
+ title: draft.title?.trim() || draft.taskId,
45
+ taskKind: draft.taskKind?.trim() || "standard",
46
+ requirement: {
47
+ objective: (requirement.objective ?? "").trim() || draft.title || draft.taskId,
48
+ scope: Array.isArray(requirement.scope) ? requirement.scope : [],
49
+ nonGoals: Array.isArray(requirement.nonGoals) ? requirement.nonGoals : [],
50
+ acceptanceCriteria: Array.isArray(requirement.acceptanceCriteria)
51
+ ? requirement.acceptanceCriteria
52
+ : [],
53
+ },
54
+ constraints: {
55
+ invariants: Array.isArray(constraints.invariants)
56
+ ? constraints.invariants
57
+ : [],
58
+ allowedPaths: Array.isArray(constraints.allowedPaths)
59
+ ? constraints.allowedPaths
60
+ : [],
61
+ forbiddenPaths: Array.isArray(constraints.forbiddenPaths)
62
+ ? constraints.forbiddenPaths
63
+ : [],
64
+ },
65
+ verification: {
66
+ commands: Array.isArray(verification.commands)
67
+ ? verification.commands
68
+ : [],
69
+ },
70
+ };
71
+ if (draft.featureId?.trim())
72
+ canonical.featureId = draft.featureId.trim();
73
+ if (Array.isArray(draft.references) && draft.references.length > 0) {
74
+ canonical.references = draft.references;
75
+ }
76
+ if (Array.isArray(draft.sourceResolutions) &&
77
+ draft.sourceResolutions.length > 0) {
78
+ canonical.sourceResolutions = draft.sourceResolutions;
79
+ }
80
+ const openQuestions = asStringList(draft.openQuestions);
81
+ if (openQuestions.length > 0)
82
+ canonical.openQuestions = openQuestions;
83
+ const assumptions = asStringList(draft.assumptions);
84
+ if (assumptions.length > 0)
85
+ canonical.assumptions = assumptions;
86
+ if (draft.interviewAssessmentRef) {
87
+ canonical.interviewAssessmentRef = draft.interviewAssessmentRef;
88
+ }
89
+ return canonical;
90
+ }
13
91
  export class DraftStore {
14
92
  appData;
15
93
  constructor(appData) {